mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 14:21:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82cf9b83c8 | ||
|
|
2aa6ace036 | ||
|
|
fc21e54214 | ||
|
|
9d37a3ad5f | ||
|
|
60445b58c0 | ||
|
|
d05d26e9c3 | ||
|
|
34d4178952 | ||
|
|
30e5ba4a31 | ||
|
|
6f66917530 | ||
|
|
47de4a4d83 | ||
|
|
30875ed297 | ||
|
|
31da8f5dca | ||
|
|
4ba7c94d4d | ||
|
|
c472ff04c8 | ||
|
|
512ae42f7c | ||
|
|
5b734e427c | ||
|
|
82d7e82926 | ||
|
|
772c9330e9 | ||
|
|
6f8536aed1 | ||
|
|
6cc16bee09 | ||
|
|
95f2860231 | ||
|
|
22ee8d0e9a | ||
|
|
0e4c3e8727 | ||
|
|
cd49a498c8 | ||
|
|
ce2d83b62d | ||
|
|
42bc3dd9ad | ||
|
|
a2edcb12f9 | ||
|
|
8896607268 | ||
|
|
eb8bb8bcd9 | ||
|
|
43685766ae | ||
|
|
3997ca29fd | ||
|
|
1867a8d6a1 | ||
|
|
dd23aa676c | ||
|
|
348ac7ef0e | ||
|
|
d5871c1af1 | ||
|
|
af53393b94 | ||
|
|
77668b2e3c | ||
|
|
14dbd1be5f | ||
|
|
3629d22c76 | ||
|
|
524b091daf | ||
|
|
6edbe9d6ed | ||
|
|
8554b6bc53 | ||
|
|
30a0f3269a | ||
|
|
decfc96c98 | ||
|
|
95ade2d5ee | ||
|
|
f0ee8ce54f | ||
|
|
7349d9112b | ||
|
|
152f7198b9 | ||
|
|
41e9ecb016 | ||
|
|
18a0ec2d47 | ||
|
|
17ebb94433 | ||
|
|
e878547256 | ||
|
|
950c1199dd | ||
|
|
37fe5b5328 | ||
|
|
6aa51c24d8 | ||
|
|
0c8b8dc4d3 | ||
|
|
36beee903b | ||
|
|
318d74d9d2 | ||
|
|
082d59640f | ||
|
|
26cc76347a | ||
|
|
b8349a8144 | ||
|
|
36c11637f5 | ||
|
|
51fdc43c80 | ||
|
|
eecb32abf6 | ||
|
|
885e52052b | ||
|
|
f938412499 | ||
|
|
0ad23b584d | ||
|
|
2ca1b6645f | ||
|
|
3f11e286b4 | ||
|
|
01ec3ac900 | ||
|
|
5a826638b4 | ||
|
|
dd670cac36 | ||
|
|
7d15ef727b | ||
|
|
a8d57b1f00 | ||
|
|
cfe6021eb2 | ||
|
|
025602c2be | ||
|
|
dd088d3324 | ||
|
|
99b3bde00a | ||
|
|
97712287fe | ||
|
|
b0abdeb446 | ||
|
|
aebfd69a2f | ||
|
|
d9fe3019e0 | ||
|
|
3b7162131d | ||
|
|
e12ecf6ac8 | ||
|
|
dadc9e781a | ||
|
|
e6a06be558 | ||
|
|
c92c4540e3 | ||
|
|
bf7b181a2c | ||
|
|
560e4788a1 | ||
|
|
0239cffde7 | ||
|
|
d11ea857df | ||
|
|
2ba4504b71 | ||
|
|
d70df548ad | ||
|
|
b7bb142060 | ||
|
|
242248ceed | ||
|
|
796655c6ff | ||
|
|
88f4146c53 | ||
|
|
5fd4f2670f | ||
|
|
75a39eef3a | ||
|
|
cf3b351c19 | ||
|
|
69743d3a5c | ||
|
|
029e10d9d9 | ||
|
|
32727a7a94 | ||
|
|
c81244d4e9 | ||
|
|
f133c655a8 | ||
|
|
0f461187d2 | ||
|
|
16a369b574 | ||
|
|
5f02176997 | ||
|
|
fc2eb78e0a | ||
|
|
c03efab319 | ||
|
|
7d746a0887 | ||
|
|
a86bc853f3 | ||
|
|
da95ad29d8 | ||
|
|
1e5e84d919 | ||
|
|
6280f653c8 | ||
|
|
c517862ce2 | ||
|
|
4da8bfcbf9 | ||
|
|
9d0b28df54 | ||
|
|
d85ff9dfe9 | ||
|
|
469bdaf891 | ||
|
|
2cf197f894 | ||
|
|
348d65da7b | ||
|
|
bd84d0bc6d | ||
|
|
2282393234 | ||
|
|
a1e4fb4c80 | ||
|
|
3b7a49a6fd | ||
|
|
272c16c767 | ||
|
|
24d011807c | ||
|
|
5a18f83a13 | ||
|
|
354212421e | ||
|
|
abfb05cfbe | ||
|
|
75780af235 | ||
|
|
d3bf912ea0 | ||
|
|
c87a9cb0ce | ||
|
|
bd9a1bc126 | ||
|
|
7f3a091895 | ||
|
|
a8d5a16c35 | ||
|
|
392e32c46b | ||
|
|
ca85ebdee2 | ||
|
|
40eaa123d4 | ||
|
|
6cd29d9850 | ||
|
|
b0711943b2 | ||
|
|
43cd140ecd | ||
|
|
4e1a7c5982 | ||
|
|
7f2c76e18d | ||
|
|
d672a5f443 | ||
|
|
79aa0bb11b | ||
|
|
fcef1b0dbd | ||
|
|
7f49b76680 | ||
|
|
4c2cd14f5f | ||
|
|
158e85bf43 | ||
|
|
a606e3a966 | ||
|
|
aea6e45c64 | ||
|
|
cfb1866ec5 | ||
|
|
a2be1670bb | ||
|
|
73ba449679 |
No files matched your search
@@ -0,0 +1 @@
|
||||
buy_me_a_coffee: strataengine
|
||||
@@ -37,6 +37,7 @@ td.log
|
||||
/strata-*.json
|
||||
/strata-*.log
|
||||
/build-strata.bat
|
||||
/.strata-mcp/
|
||||
|
||||
# image encoder (setup.py --vision): build output and the generated build script
|
||||
build-vision*/
|
||||
@@ -49,3 +50,4 @@ build-vision.bat
|
||||
*.done
|
||||
/third_party/*.zip
|
||||
|
||||
/.rocm-win/
|
||||
@@ -0,0 +1,23 @@
|
||||
# AGENTS.md
|
||||
|
||||
Strata runs the Qwen3.8-Flash-Next mixture-of-experts model (and its Coder, Swift 1.5 and Unsloth variants) on a
|
||||
normal PC: one NVIDIA or AMD graphics card plus system RAM, on Windows or Linux. It has a C++/CUDA/HIP engine
|
||||
(`src/`, `include/`), a Python server with an OpenAI- and Anthropic-compatible API and a web app (`serve/`), and a
|
||||
one-click installer (`setup.py`, started by `START-HERE.bat` / `setup.sh`).
|
||||
|
||||
## Installing Strata for a user
|
||||
|
||||
Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**: check the PC, pick the model by RAM, run setup non-interactively,
|
||||
start and verify the server, and connect the user's apps. Never expose the server beyond `127.0.0.1` without
|
||||
`--api-key`. As an alternative to shell commands, Strata's MCP server ([docs/MCP_SERVER.md](docs/MCP_SERVER.md))
|
||||
offers the same steps as tools.
|
||||
|
||||
## Working on the code
|
||||
|
||||
- How the engine works, every measured number, the API and all settings: [docs/DETAILS.md](docs/DETAILS.md) and
|
||||
the [paper](docs/paper/Strata-Paper.pdf).
|
||||
- AMD (HIP) build and validation: [docs/AMD_HIP.md](docs/AMD_HIP.md); multi-GPU: [docs/MULTI_GPU.md](docs/MULTI_GPU.md).
|
||||
- Setup's own tests run without a GPU or downloads: `python tools/test_setup_<name>.py` (for example
|
||||
`tools/test_setup_amd.py`, `tools/test_setup_choices.py`).
|
||||
- Keep the docs' style: plain words, measured numbers with what they were measured on, no claims without a
|
||||
measurement.
|
||||
+125
-6
@@ -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.31 LANGUAGES CXX)
|
||||
project(strata VERSION 0.1.36 LANGUAGES CXX)
|
||||
add_compile_definitions(STRATA_VERSION="${PROJECT_VERSION}")
|
||||
# issue #31: release builds keep a PDB beside the exe (not shipped in it), so a minidump from a stalled engine can
|
||||
# be read back to the exact source line
|
||||
@@ -33,8 +33,13 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
endif()
|
||||
|
||||
option(STRATA_ENABLE_CUDA "Build the CUDA targets (Phase 2+)" OFF)
|
||||
option(STRATA_ENABLE_HIP "Build the HIP targets for wave32 AMD GPUs (gfx1100, gfx1201)" OFF)
|
||||
option(STRATA_ENABLE_HIP "Build the HIP targets for wave32 AMD GPUs (gfx1100, gfx1201; gfx1030 unvalidated)" OFF)
|
||||
option(STRATA_PREFILL_MMQ "Build the optional GGML MMQ prefill path for HIP" OFF)
|
||||
# CUDA: MMQ prompt kernels for Q4_K / Q5_K / Q5_1 (Unsloth's UD-Q4_K_XL experts). Off by default: the engine loads
|
||||
# every kernel when it starts (CUDA_MODULE_LOADING=EAGER), so these took ~5 MB of VRAM - 1-3 expert slots - from every
|
||||
# model, and the UD-Q4_K_XL prompt is SSD-bound with or without them (docs/UNSLOTH_Q4.md)
|
||||
option(STRATA_MMQ_KQUANTS "Build the CUDA MMQ prompt kernels for Q4_K / Q5_K / Q5_1 experts" OFF)
|
||||
option(STRATA_ORCA_Q4KS_MMQ "Build the GGML Q5_0 MMQ instance for OrcaRouter Q4_K_S" OFF)
|
||||
if(STRATA_PREFILL_MMQ AND NOT STRATA_ENABLE_HIP)
|
||||
message(FATAL_ERROR "STRATA_PREFILL_MMQ requires STRATA_ENABLE_HIP=ON")
|
||||
endif()
|
||||
@@ -269,6 +274,15 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_include_directories(qsa_prompt_attn_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
|
||||
target_link_libraries(qsa_prompt_attn_parity PRIVATE strata_kernels CUDA::cudart)
|
||||
endif()
|
||||
# the QSA selection (block scores on the warp kernel and the tensor-core one, top-k): a GPU bench with a
|
||||
# scorer-vs-scorer comparison and an FP64 accuracy gate (synthetic, no model); the ctest runs a 32K context under a
|
||||
# 262144-cell capacity (the top-k dispatch follows the capacity) and exits non-zero on a failed gate
|
||||
if((STRATA_BUILD_TESTS OR STRATA_PARITY_PROMPT_ATTN) AND STRATA_ENABLE_HIP)
|
||||
add_executable(qsa_select_bench src/kernels/qsa_select_bench.cpp)
|
||||
set_source_files_properties(src/kernels/qsa_select_bench.cpp PROPERTIES LANGUAGE HIP)
|
||||
target_link_libraries(qsa_select_bench PRIVATE strata_kernels strata_hip_runtime)
|
||||
add_test(NAME qsa_select_bench COMMAND qsa_select_bench 32768 256 3 262144)
|
||||
endif()
|
||||
# plan v0.3 P6: llama.cpp's block layouts and codebook grids for the i-quant formats (MIT, third_party/ggml)
|
||||
target_include_directories(strata_kernels PRIVATE third_party/ggml)
|
||||
# Match the pinned llama.cpp CUDA arithmetic contract for this adapter only.
|
||||
@@ -336,6 +350,14 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
add_test(NAME file_expert_source_test COMMAND file_expert_source_test)
|
||||
set_tests_properties(file_expert_source_test PROPERTIES TIMEOUT 60)
|
||||
endif()
|
||||
# #477: the learned expert profile (--expert-profile-save): its order and make_profile.py's file format (CPU only)
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/core/expert_profile_save_test.cpp")
|
||||
enable_testing()
|
||||
add_executable(expert_profile_save_test tests/core/expert_profile_save_test.cpp)
|
||||
target_link_libraries(expert_profile_save_test PRIVATE strata_engine)
|
||||
add_test(NAME expert_profile_save_test COMMAND expert_profile_save_test)
|
||||
set_tests_properties(expert_profile_save_test PROPERTIES TIMEOUT 60)
|
||||
endif()
|
||||
if(STRATA_BUILD_TESTS AND STRATA_NATIVE_EXPERTS)
|
||||
# native_experts.txt v2-v4 (the real packs' copies and the per-role shard column) and the arena filled from
|
||||
# synthetic GGUF shards at the real expert dimensions
|
||||
@@ -343,6 +365,16 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(expert_layout_test PRIVATE strata_engine)
|
||||
add_test(NAME expert_layout_test COMMAND expert_layout_test ${CMAKE_CURRENT_SOURCE_DIR}/tests/data/native_experts)
|
||||
set_tests_properties(expert_layout_test PROPERTIES TIMEOUT 60)
|
||||
# #326: a native pack's BF16 (--compat-bf16) PLE key stays in the arena; a quantized key row loads as before
|
||||
add_executable(native_dense_ple_key_test tests/core/native_dense_ple_key_test.cpp)
|
||||
target_link_libraries(native_dense_ple_key_test PRIVATE strata_engine)
|
||||
add_test(NAME native_dense_ple_key_test COMMAND native_dense_ple_key_test)
|
||||
set_tests_properties(native_dense_ple_key_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||
# #369: per-layer admission on a sized (native pack) cache starts each layer in its own range
|
||||
add_executable(expert_cache_per_layer_test tests/core/expert_cache_per_layer_test.cpp)
|
||||
target_link_libraries(expert_cache_per_layer_test PRIVATE strata_engine)
|
||||
add_test(NAME expert_cache_per_layer_test COMMAND expert_cache_per_layer_test)
|
||||
set_tests_properties(expert_cache_per_layer_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||
endif()
|
||||
if(STRATA_BUILD_CONVERSATION_TESTS)
|
||||
add_executable(conversation_snapshot_test src/core/conversation_snapshot_test.cpp)
|
||||
@@ -402,6 +434,15 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
add_test(NAME hip_handoff COMMAND hip_handoff)
|
||||
list(APPEND _strata_hip_tests hip_handoff)
|
||||
endif()
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/hip/mapped_alias.cpp")
|
||||
# #325: reports what a hipHostGetDevicePointer alias is on this stack (on Windows: the host pointer, and a
|
||||
# D2D copy into it does not land - hip_handoff's timeout there). A diagnosis: exits 0 whenever it ran.
|
||||
add_executable(hip_mapped_alias tests/hip/mapped_alias.cpp)
|
||||
target_link_libraries(hip_mapped_alias PRIVATE strata_kernels strata_hip_runtime)
|
||||
add_test(NAME hip_mapped_alias COMMAND hip_mapped_alias)
|
||||
set_tests_properties(hip_mapped_alias PROPERTIES SKIP_RETURN_CODE 77)
|
||||
list(APPEND _strata_hip_tests hip_mapped_alias)
|
||||
endif()
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/hip/native_qsa_score.cpp")
|
||||
add_executable(hip_native_qsa_score tests/hip/native_qsa_score.cpp)
|
||||
target_link_libraries(hip_native_qsa_score PRIVATE strata_kernels strata_hip_runtime)
|
||||
@@ -414,6 +455,17 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
add_test(NAME hip_prefill_native_batch COMMAND hip_prefill_native_batch)
|
||||
list(APPEND _strata_hip_tests hip_prefill_native_batch)
|
||||
endif()
|
||||
# S6: the AMD fast router against the portable kernel, bitwise
|
||||
add_executable(hip_router_fast tests/hip/router_fast.cpp)
|
||||
target_link_libraries(hip_router_fast PRIVATE strata_kernels strata_hip_runtime)
|
||||
add_test(NAME hip_router_fast COMMAND hip_router_fast)
|
||||
list(APPEND _strata_hip_tests hip_router_fast)
|
||||
# S6: the RDNA4 matrix-core prompt attention (STRATA_HIP_WMMA) against the FP32 kernel and FP64; skips off gfx12
|
||||
add_executable(hip_prompt_attn_wmma src/kernels/qsa_prompt_attn_parity.cpp)
|
||||
target_link_libraries(hip_prompt_attn_wmma PRIVATE strata_kernels strata_hip_runtime)
|
||||
add_test(NAME hip_prompt_attn_wmma COMMAND hip_prompt_attn_wmma 32768 2048 3)
|
||||
set_tests_properties(hip_prompt_attn_wmma PROPERTIES TIMEOUT 600 SKIP_RETURN_CODE 77)
|
||||
list(APPEND _strata_hip_tests hip_prompt_attn_wmma)
|
||||
if(STRATA_HIPBLASLT_AVAILABLE AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/hip/prefill_hipblaslt_gemm.cpp")
|
||||
add_executable(hip_prefill_hipblaslt_gemm tests/hip/prefill_hipblaslt_gemm.cpp)
|
||||
target_include_directories(hip_prefill_hipblaslt_gemm PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/prefill")
|
||||
@@ -536,6 +588,14 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
set_tests_properties(sampler_parity_one_block PROPERTIES ENVIRONMENT "STRATA_OLD_SAMPLER=0;STRATA_SAMPLER_ONE_BLOCK=1")
|
||||
add_test(NAME sampler_parity_old COMMAND sampler_parity --selftest)
|
||||
set_tests_properties(sampler_parity_old PROPERTIES ENVIRONMENT "STRATA_OLD_SAMPLER=1;STRATA_SAMPLER_ONE_BLOCK=0")
|
||||
# S19: the decode QSA top-k and the greedy argmax on thread-block clusters (sm_90+) vs the one-CTA kernels, bitwise,
|
||||
# captured in a graph too (GPU, synthetic, no model; skips below sm_90); --bench: per-call times
|
||||
if(STRATA_ENABLE_CUDA)
|
||||
add_executable(decode_cluster_parity src/kernels/decode_cluster_parity.cpp)
|
||||
target_include_directories(decode_cluster_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(decode_cluster_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME decode_cluster_parity COMMAND decode_cluster_parity --selftest)
|
||||
endif()
|
||||
|
||||
add_executable(rope_parity src/kernels/rope_parity.cpp)
|
||||
target_include_directories(rope_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
@@ -758,6 +818,9 @@ set(STRATA_GGML_DIR "" CACHE PATH "llama.cpp checkout to take ggml from (empty:
|
||||
if(STRATA_PREFILL_MMQ AND NOT STRATA_NATIVE_EXPERTS)
|
||||
message(FATAL_ERROR "STRATA_PREFILL_MMQ requires STRATA_NATIVE_EXPERTS=ON for GGML sources")
|
||||
endif()
|
||||
if(STRATA_ORCA_Q4KS_MMQ AND NOT STRATA_NATIVE_EXPERTS)
|
||||
message(FATAL_ERROR "STRATA_ORCA_Q4KS_MMQ requires STRATA_NATIVE_EXPERTS=ON for GGML sources")
|
||||
endif()
|
||||
if(STRATA_NATIVE_EXPERTS)
|
||||
enable_language(C)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
@@ -806,6 +869,11 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
endif()
|
||||
target_link_libraries(strata_kernels_cpu PUBLIC ggml-cpu ggml-base)
|
||||
target_compile_definitions(strata_kernels_cpu PUBLIC STRATA_NATIVE_EXPERTS=1)
|
||||
set(_strata_mmq_types q2_0 iq2_xxs iq2_xs iq2_s iq3_xxs iq3_s iq4_nl iq4_xs q8_0)
|
||||
# #296: OrcaRouter Q4_K_S's Q5_0 experts (Q4_K and Q5_1 come with STRATA_MMQ_KQUANTS on CUDA, S5's branch)
|
||||
if(STRATA_ORCA_Q4KS_MMQ)
|
||||
list(APPEND _strata_mmq_types q5_0)
|
||||
endif()
|
||||
if(STRATA_ENABLE_CUDA)
|
||||
# prompt-speed plan step 2b: llama.cpp's MMQ kernels (ggml-cuda, MIT) for the prompt path's experts, compiled
|
||||
# from the same checkout as ggml above - only mmq.cuh's instances for the packs' types and the quantizer, with
|
||||
@@ -816,8 +884,18 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
set(_strata_gg ${strata_llamacpp_SOURCE_DIR}/ggml)
|
||||
endif()
|
||||
set(_strata_mmq ${_strata_gg}/src/ggml-cuda)
|
||||
add_library(strata_mmq STATIC src/prefill/moe_mmq.cu src/prefill/ggml_cuda_host.cu ${_strata_mmq}/quantize.cu)
|
||||
foreach(_t q2_0 iq2_xxs iq2_xs iq2_s iq3_xxs iq3_s iq4_nl iq4_xs q8_0) # q8_0: the drafter's matrices (E-9)
|
||||
# #136: the Q2_0 pack's prompt experts on Strata's own int8 kernels (moe_fused.cu, opt-in STRATA_PF_FUSED=1; CUDA
|
||||
# only: the HIP branch below keeps MMQ, and an sm_75 device or an sm_75-only build reports them unavailable)
|
||||
add_library(strata_mmq STATIC src/prefill/moe_mmq.cu src/prefill/moe_fused.cu src/prefill/moe_fused_iq.cu
|
||||
src/prefill/ggml_cuda_host.cu ${_strata_mmq}/quantize.cu)
|
||||
# q8_0: the drafter's matrices (E-9); STRATA_ORCA_Q4KS_MMQ adds q5_0 (#296, in _strata_mmq_types), and
|
||||
# STRATA_MMQ_KQUANTS q4_k q5_k q5_1, the experts of Unsloth's UD-Q4_K_XL (docs/UNSLOTH_Q4.md; the dispatch
|
||||
# follows eddoursul/Strata 7706c1b)
|
||||
set(_strata_mmq_cuda_types ${_strata_mmq_types})
|
||||
if(STRATA_MMQ_KQUANTS)
|
||||
list(APPEND _strata_mmq_cuda_types q4_k q5_k q5_1)
|
||||
endif()
|
||||
foreach(_t IN LISTS _strata_mmq_cuda_types)
|
||||
target_sources(strata_mmq PRIVATE ${_strata_mmq}/template-instances/mmq-instance-${_t}.cu)
|
||||
endforeach()
|
||||
target_include_directories(strata_mmq PRIVATE ${_strata_gg}/include ${_strata_gg}/src ${_strata_mmq}
|
||||
@@ -827,8 +905,33 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
target_compile_options(strata_mmq PRIVATE $<$<COMPILE_LANGUAGE:CUDA>:-Xcompiler=/bigobj>)
|
||||
endif()
|
||||
target_link_libraries(strata_mmq PUBLIC ggml-base ${_strata_gpu_runtime_target})
|
||||
if(STRATA_MMQ_KQUANTS)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_KQUANTS=1)
|
||||
endif()
|
||||
if(STRATA_ORCA_Q4KS_MMQ)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_ORCA_Q4KS_MMQ=1)
|
||||
endif()
|
||||
target_link_libraries(strata_prefill PUBLIC strata_mmq)
|
||||
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1)
|
||||
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1 STRATA_PREFILL_FUSED=1)
|
||||
if(STRATA_BUILD_TESTS)
|
||||
# #136: the fused int8 experts against a double-precision reference and the MMQ path (exit 77 below sm_80)
|
||||
add_executable(prefill_fused_moe_test tests/cuda/prefill_fused_moe_test.cpp)
|
||||
target_link_libraries(prefill_fused_moe_test PRIVATE strata_mmq ggml-base ${_strata_gpu_runtime_target})
|
||||
add_test(NAME prefill_fused_moe_test COMMAND prefill_fused_moe_test)
|
||||
set_tests_properties(prefill_fused_moe_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 300)
|
||||
# #136: the same for the native packs' formats (IQ2_XXS IQ2_XS IQ2_S IQ3_XXS IQ3_S IQ4_XS, down Q2_0 IQ4_NL)
|
||||
add_executable(prefill_fused_iq_test tests/cuda/prefill_fused_iq_test.cpp)
|
||||
target_link_libraries(prefill_fused_iq_test PRIVATE strata_mmq ggml-base ${_strata_gpu_runtime_target})
|
||||
add_test(NAME prefill_fused_iq_test COMMAND prefill_fused_iq_test)
|
||||
set_tests_properties(prefill_fused_iq_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 600)
|
||||
endif()
|
||||
if(STRATA_BUILD_TESTS AND STRATA_MMQ_KQUANTS)
|
||||
# the UD-Q4_K_XL expert formats through MMQ against ggml's dequantized weights (synthetic, a few seconds)
|
||||
add_executable(prefill_mmq_kquant_test tests/cuda/prefill_mmq_kquant_test.cpp)
|
||||
target_link_libraries(prefill_mmq_kquant_test PRIVATE strata_mmq ggml-base ${_strata_gpu_runtime_target})
|
||||
add_test(NAME prefill_mmq_kquant_test COMMAND prefill_mmq_kquant_test)
|
||||
set_tests_properties(prefill_mmq_kquant_test PROPERTIES TIMEOUT 120)
|
||||
endif()
|
||||
elseif(STRATA_PREFILL_MMQ)
|
||||
# Compile only the pinned GGML MMQ and activation-quantizer sources used by prefill. This uses
|
||||
# common.cuh's native HIP path without enabling a second GGML HIP backend.
|
||||
@@ -845,7 +948,7 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
src/prefill/moe_mmq.cu
|
||||
src/prefill/ggml_cuda_host.cu
|
||||
"${_strata_mmq}/quantize.cu")
|
||||
foreach(_t q2_0 iq2_xxs iq2_xs iq2_s iq3_xxs iq3_s iq4_nl iq4_xs q8_0)
|
||||
foreach(_t IN LISTS _strata_mmq_types)
|
||||
list(APPEND _strata_mmq_sources "${_strata_mmq}/template-instances/mmq-instance-${_t}.cu")
|
||||
endforeach()
|
||||
set_source_files_properties(${_strata_mmq_sources} PROPERTIES LANGUAGE HIP)
|
||||
@@ -854,6 +957,9 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
PRIVATE "${_strata_gg}/include" "${_strata_gg}/src" "${_strata_mmq}"
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
target_compile_definitions(strata_mmq PRIVATE GGML_USE_HIP=1)
|
||||
if(STRATA_ORCA_Q4KS_MMQ)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_ORCA_Q4KS_MMQ=1)
|
||||
endif()
|
||||
target_link_libraries(strata_mmq PUBLIC ggml-base hip::host roc::hipblas)
|
||||
target_link_libraries(strata_prefill PUBLIC strata_mmq)
|
||||
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1)
|
||||
@@ -884,11 +990,24 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
set_tests_properties(native_expert_parity_${_name} PROPERTIES TIMEOUT 120)
|
||||
endforeach()
|
||||
add_test(NAME native_expert_parity_q5_1_min COMMAND native_expert_parity --q5_1-min)
|
||||
# #290: the BF16 token embedding (--embd-gguf) gathered and dequantized bit for bit
|
||||
add_test(NAME native_expert_parity_bf16_embd COMMAND native_expert_parity --bf16-embd)
|
||||
# a pair without GPU kernels (Q6_K gate/up) is refused by native_expert_supported, not run
|
||||
add_test(NAME native_expert_parity_refuses_q6_K COMMAND native_expert_parity --synthetic q6_K/q8_0)
|
||||
set_tests_properties(native_expert_parity_refuses_q6_K PROPERTIES WILL_FAIL TRUE)
|
||||
endif()
|
||||
endif()
|
||||
# #296's real-weight checks (OrcaRouter Q4_K_S files; docs/ORCA_Q4_K_S.md) - test programs, not part of a release
|
||||
if(STRATA_ENABLE_CUDA AND STRATA_BUILD_TESTS)
|
||||
add_executable(ple_q5_parity src/kernels/ple_q5_parity.cpp)
|
||||
target_link_libraries(ple_q5_parity PRIVATE strata_kernels strata_artifact ggml-base ${_strata_gpu_runtime_target})
|
||||
add_executable(q5_projection_parity src/kernels/q5_projection_parity.cpp)
|
||||
target_include_directories(q5_projection_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(q5_projection_parity PRIVATE strata_kernels strata_artifact ggml-base ${_strata_gpu_runtime_target})
|
||||
# #291: the FP8 PLE table (tools/ple_fp8_pack.py) through PleTable vs the checkpoint's own values
|
||||
add_executable(ple_fp8_parity src/kernels/ple_fp8_parity.cpp)
|
||||
target_link_libraries(ple_fp8_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(STRATA_ENABLE_CUDA AND STRATA_BUILD_TESTS AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/bench/micro/hit_cpu_order_parity.cu")
|
||||
|
||||
@@ -1,272 +1,185 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
|
||||
<p align="center"><b>Run a 125-billion-parameter AI model on a normal gaming PC</b><br>
|
||||
one NVIDIA card (12-24 GB) + 64 GB of RAM · Windows or Linux · one click to install</p>
|
||||
<p align="center"><b>Run a 125-billion-parameter AI model on your own gaming PC</b><br>
|
||||
NVIDIA or AMD graphics card (12 GB or more) · Windows or Linux · free and open source</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>A voxel pagoda garden, 1 shot prompt running on an RTX 5070 with Strata (IQ3_S, 128K context) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">full video (49 s)</a></sub></p>
|
||||
|
||||
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** - a large, smart AI model that
|
||||
normally needs a server - on your own PC. It writes its answers at **60-95 tokens per second** (a token is about ¾
|
||||
of a word): faster than you can read.
|
||||
|
||||
- **Free and open source.**
|
||||
|
||||
> **Jump to:** [How fast?](#how-fast-is-it) · [Which model?](#which-model-should-i-pick) · [Install](#install) ·
|
||||
> [Using it](#using-it) · [Problems?](#something-went-wrong) · [How it works](#how-does-it-work) ·
|
||||
> [All the details](docs/DETAILS.md)
|
||||
|
||||
---
|
||||
normally needs a server - on a normal PC. It chats, writes code, reads pictures and works with your apps and coding
|
||||
agents, and nothing leaves your PC.
|
||||
|
||||
## How fast is it?
|
||||
|
||||
Measured on an RTX 5070 (12 GB), a Ryzen 5 7600 and 64 GB of RAM:
|
||||
Measured on two ordinary gaming PCs. "Writes answers" is how fast the reply appears in a short chat; "reads your
|
||||
prompt" is how fast it takes in what you send (a 32K-token document, code or chat history). A token is about ¾ of a
|
||||
word, so 60 tokens per second is faster than you can read.
|
||||
|
||||
| Size | Writes answers (short chat) | Writes answers (128K context) | Reads your prompt |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| **Q2_0** | 93 tokens/s | 74 tokens/s | 2,170 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 63 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 49 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 46 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** (IQ1_M) | 55 tokens/s | 43 tokens/s | 2,180 tokens/s |
|
||||
<table>
|
||||
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
|
||||
<tr><td>
|
||||
|
||||
- **Writes answers** = how fast the reply appears (tokens per second).
|
||||
- **Reads your prompt** = how fast it takes in what you send (long documents, code, chat history), measured on a
|
||||
32K-token prompt; a 4K prompt reads at 910-1,580 tokens/s. A 32K prompt takes about 15 seconds with Q2_0.
|
||||
| Size | Writes answers | Reads your prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** | 55 tokens/s | 2,180 tokens/s |
|
||||
|
||||
A card with more VRAM is faster, because more of the model fits on the GPU: an RTX 3090 (24 GB) should do roughly
|
||||
100-140 tokens per second. All measurements, long-context numbers and estimates for other cards are in the
|
||||
[details](docs/DETAILS.md#speed-measured).
|
||||
</td><td>
|
||||
|
||||
Every PC is different: `START-HERE.bat --calibrate` measures a few engine settings on yours and keeps the fastest
|
||||
(about 5-10 minutes; on the PC above it made the Coder 7% faster).
|
||||
| Size | Writes answers | Reads your prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
|
||||
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
|
||||
| **Coder** | 44 tokens/s | 1,420 tokens/s |
|
||||
|
||||
Measured Strata on your own PC? See [Community benchmark results](docs/COMMUNITY_BENCHMARKS.md)
|
||||
for a report template and how to share your results in a pull request.
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
**Two or three NVIDIA cards?** Just run `START-HERE.bat`: it lists your cards, says which ones Strata can use, and
|
||||
asks whether to share the model across them (recommended when two can). An install made on one card asks once at
|
||||
its next start. Or choose yourself: `START-HERE.bat --gpus 0,2` (both, remembered), `--gpus all`, or `--gpu 0` (one
|
||||
card, this start only). Each card keeps the experts of its own layers, and prompts flow through the cards in a
|
||||
pipeline: on an RTX 5080 + RTX 3090 prompts were read 18-20% faster than on the 5080 alone, decoding on par.
|
||||
Every card must be an RTX 20 series or newer with 8 GB or more. See [docs/MULTI_GPU.md](docs/MULTI_GPU.md).
|
||||
A card with more VRAM is faster: an RTX 3090 (24 GB) should write roughly 100-140 tokens per second. Long chats,
|
||||
other cards: [speed of each model](docs/MODELS.md#how-fast-is-each-size), [community results](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
## Which model should I pick?
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
|
||||
<sub>Strata is free. If it runs well on your PC, a coffee keeps the work on it going.</sub></p>
|
||||
|
||||
**The size** (the same model, compressed more or less):
|
||||
## What you need
|
||||
|
||||
| Model | RAM+VRAM Requirements | Speed | Quality |
|
||||
| --- | ---: | --- | --- |
|
||||
| **Q2_0** | 37.6 GB | fastest | good |
|
||||
| **IQ2_XS** | 39.2 GB | fast | better (**recommended**) |
|
||||
| **IQ3_XXS** | 47.0 GB | slower | great |
|
||||
| **IQ3_S** | 54.8 GB | slowest | best: matches the full model on the published tests (original model only) |
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **Graphics card** | **NVIDIA** GeForce RTX 20, 30, 40 or 50 series, or **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 or RX 6800 / 6900 series - with **12 GB of VRAM or more** |
|
||||
| **RAM** | 32 GB or more - how much decides [which model](#which-model-should-i-pick) fits; 64 GB runs every size |
|
||||
| **Disk** | about 80 GB free, on an SSD if you can (the first start is much faster) |
|
||||
| **System** | Windows 10 / 11 or Linux, and a current graphics driver from NVIDIA or AMD |
|
||||
|
||||
**Will it fit?** Shard 1 is the part of the model that gets loaded when it starts: its experts go into your **RAM**,
|
||||
the rest onto your graphics card (the second shard, a 29 GB lookup table, stays on the SSD). So it fits when your
|
||||
**RAM is at least shard 1 + about 10 GB** for Windows and your other programs. With 64 GB of RAM every size fits
|
||||
(IQ3_S with little else open); with 48 GB, Q2_0 and IQ2_XS. A bigger graphics card makes it faster, but it doesn't
|
||||
lower the RAM needed.
|
||||
|
||||
**The version:**
|
||||
|
||||
- **Qwen3.8-Flash-Next** - the original.
|
||||
- **[Coder](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF)** - ISTA-DASLab's coding
|
||||
version: half of the experts removed, keeping the ones that code, tool use and images need (91% of the full model's
|
||||
SWE-bench Verified score, 99% of LiveCodeBench, by its authors). One size (IQ1_M: its experts stored like IQ3_S):
|
||||
shard 1 is **29.6 GB**, so it fits a PC with **32 GB of RAM**, runs 262K context on 64 GB, and reads long prompts
|
||||
the fastest of all. Weaker outside coding.
|
||||
- **[Swift 1.5](https://huggingface.co/ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF)** - a fine-tune by UkisAI
|
||||
that thinks much shorter before answering, so you get the answer sooner, with about the same quality. Same speed per
|
||||
token, and about the same RAM as the same size of the original (no IQ3_S). Its own license applies (see its page).
|
||||
|
||||
Not sure? Take **IQ2_XS** - or the **Coder** if you mainly write code, or have 32-48 GB of RAM. You can add another
|
||||
one later with `SETUP.bat` (the same as `START-HERE.bat --setup`; on Linux `./setup.sh --setup`).
|
||||
|
||||
For **OrcaRouter's Flash-Next Uncensored IQ3_XXS**, see the [manual compatibility setup](docs/ORCA.md).
|
||||
It needs an explicit packing conversion and is not an installer menu option.
|
||||
|
||||
An **AMD Radeon RX 7900 XT / XTX, RX 9070 / 9070 XT or Radeon AI PRO R9700 on Linux** works too (experimental; the
|
||||
RX 7800 XT / 7700 XT and RX 9060 XT were validated by their owners):
|
||||
`./setup.sh --backend hip`, chosen by itself on a PC with no NVIDIA card Strata can use. It installs ROCm without sudo
|
||||
and compiles the engine (no images yet; several cards with `--gpus`). Details: [AMD HIP](docs/AMD_HIP.md).
|
||||
Everything else is installed for you. Two or three cards can share the model ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
The full list: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
|
||||
## Install
|
||||
|
||||
**You need:** an NVIDIA RTX 20, 30, 40 or 50 card with 12 GB of VRAM or more (RTX 20 since 0.1.27), enough RAM for the size you pick (above;
|
||||
a big GPU makes up for less RAM - the [low-RAM mode](docs/DETAILS.md)),
|
||||
~80 GB of free disk space (an SSD makes the first start much faster), and Windows 10/11 or Linux. The only thing you
|
||||
install yourself is a current **NVIDIA driver** ([nvidia.com/drivers](https://www.nvidia.com/drivers) or the NVIDIA
|
||||
App). Everything else - Python, the engine, the model - is set up for you.
|
||||
### Let your AI set it up
|
||||
|
||||
**Windows**
|
||||
Use an AI coding assistant (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Paste this into it:
|
||||
|
||||
1. [Download this project](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it (or `git clone` it).
|
||||
2. Double-click **`START-HERE.bat`**.
|
||||
3. Answer a few questions - or just press Enter each time for the recommended choice:
|
||||
- **Which model and size?** The original or Swift 1.5, and Q2_0, IQ2_XS, IQ3_XXS or IQ3_S - see [above](#which-model-should-i-pick)
|
||||
- **How much context?** How much text it can keep in mind at once (it suggests one for your card). 384K and
|
||||
512K (experimental) extend the model past its trained 262K by rope scaling - the setup turns it on itself (yarn and a
|
||||
covering factor; `--rope-scaling`/`--rope-scale` override) ([details](docs/DETAILS.md))
|
||||
- **Images?** Whether it should also read pictures
|
||||
- **Experimental speed projection?** Off unless you say yes - [read what it does](docs/DETAILS.md#experimental-speed-projection-experimental-off-by-default) first
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
Then it downloads everything (the model is ~70 GB, so the first time takes a while - you can stop and it picks up
|
||||
where it left off) and **starts the model**. Your browser opens the Strata app at `http://127.0.0.1:8080`.
|
||||
It checks your graphics card, RAM and disk, picks the model that fits, installs it, starts it and tells you how to
|
||||
connect your apps. AI tools can also install, start and stop Strata themselves through its
|
||||
[MCP server](docs/MCP_SERVER.md).
|
||||
|
||||
### Or do it yourself
|
||||
|
||||
[Download Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it (or `git clone` it).
|
||||
**Windows:** double-click **`START-HERE.bat`**. **Linux:** run **`./setup.sh`** in the Strata folder.
|
||||
|
||||
The same steps for NVIDIA and AMD: the installer finds your card and sets up the right engine for it. It asks which
|
||||
model, which size, how much context (how much text it keeps in mind) and whether it should read pictures - press
|
||||
Enter each time for the recommended answer. Then it downloads the model (~70 GB; you can stop and it continues where
|
||||
it left off) and starts it. Your browser opens the Strata app at `http://127.0.0.1:8080`.
|
||||
|
||||
> **While the model starts, your PC can be slow or stop responding for 1-3 minutes** (longest the first time): Strata
|
||||
> loads 35-55 GB into your RAM and locks part of it for the graphics card. That's normal - wait, and don't close the
|
||||
> window. The window tells you what it is doing.
|
||||
|
||||
**Next time**, just double-click `START-HERE.bat` again: it starts right away, nothing is downloaded twice. Close its
|
||||
window to stop the model.
|
||||
**Next time**, run `START-HERE.bat` (or `./setup.sh`) again: it starts right away, nothing is downloaded twice. Close
|
||||
its window to stop the model. `UPDATE.bat` (`./update.sh`) updates Strata without starting it. Updating, Docker,
|
||||
several cards, where the files go and every option:
|
||||
[docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
**Updating:** download the new version and unzip it anywhere (or `git pull`), then run `START-HERE.bat` in it. The
|
||||
model files are kept in a `Strata-data` folder next to your Strata folder, so a new copy finds them and sets itself up
|
||||
the same way - nothing big is downloaded again.
|
||||
## Which model should I pick?
|
||||
|
||||
**Linux:** run `./setup.sh` - same questions, same result.
|
||||
The installer recommends one for your RAM. The same model comes in sizes that are compressed more or less: smaller
|
||||
is faster, larger is a bit smarter.
|
||||
|
||||
**Docker (Linux):** the same idea, in a container.
|
||||
| Your RAM | Take | Why |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Coder** | it fits 32 GB, and it is made for code (with a 24 GB card, Q2_0 and IQ2_XS run too) |
|
||||
| **48 GB** | **IQ2_XS** (or Q2_0, the fastest) | the larger sizes do not fit |
|
||||
| **64 GB** | **IQ2_XS** (recommended), or IQ3_XXS / IQ3_S | every size fits; IQ3_S is the best, and the slowest |
|
||||
| **96 GB or more** | **IQ3_S**, or Unsloth's 4-bit (experimental) | room for the largest sizes with everything else open |
|
||||
|
||||
1. Host: Docker with the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
|
||||
and a driver **580 or newer** (CUDA 13.0).
|
||||
2. Build (this compiles the engine into the image, so the container never compiles):
|
||||
`docker build -t strata .`
|
||||
`docker build -t strata --build-arg CUDA_ARCHITECTURES=89 .` builds for one card only (faster).
|
||||
The default covers RTX 30 (86), RTX 40 (89), RTX 50 (120) and A-series (80); a card outside that
|
||||
set needs a rebuild with its own arch. Add `--build-arg BUILD_VISION=0` to skip the image encoder.
|
||||
3. Run (the first start downloads the ~70 GB model, then starts; later starts go straight to serving):
|
||||
`docker run --rm --gpus all -p 8080:8080 --ulimit memlock=-1 -v strata-data:/data strata`
|
||||
- **[Coder](docs/MODELS.md#coder)** - a coding version with half of the experts removed: 91% of the full model's
|
||||
SWE-bench Verified score (by its authors), fits 32 GB of RAM. Weaker outside code, including Chinese and other
|
||||
CJK text (#438): for those, take Q2_0, IQ2_XS or IQ3_S, which keep every expert.
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** - a fine-tune that thinks much shorter before it answers, so you get the
|
||||
answer sooner, at about the same quality.
|
||||
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental) - the closest to the full
|
||||
model, but most of it is read from the SSD while it answers: 7-8.5 tokens/s on a 64 GB PC.
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** - a manual setup, not in the
|
||||
installer's menu.
|
||||
|
||||
The setup choices are env vars: `-e MODEL=IQ2_XS -e FAMILY=qwen -e CONTEXT=32768 -e VISION=no`
|
||||
(or `MODEL=Q2_0|IQ3_XXS|IQ3_S`, `FAMILY=swift|coder`; the defaults above are the recommended ones).
|
||||
`-e VISION=cpu` keeps the image encoder on the CPU. `-e KV=int8|q4_0|k8v4` picks the KV cache
|
||||
precision; `k8v4` is INT8 K with 4-bit V and keeps its KV in VRAM from 64K up.
|
||||
Only the model files, the prepared pack, the MTP layer and the install config live in the
|
||||
`strata-data` volume; the engine is part of the image. Switching between models already on the
|
||||
volume needs no setup pass: `-e MODEL=Q2_0 -e FAMILY=coder` picks that model's config. Add
|
||||
`-e REINSTALL=1` only to change settings for a model already set up (context, vision, KV, host,
|
||||
api_key, LOW_RAM), since those are recorded in its config.
|
||||
Strata loads 32-62 GB into RAM. `--gpus all` on a host with two usable cards takes both: the
|
||||
layer split is setup's recommended default ([docs/MULTI_GPU.md](docs/MULTI_GPU.md)), and a volume
|
||||
set up for one card switches to the pair on its first start there. Pin one card with `-e GPU=0`,
|
||||
or name them with `-e GPUS=0,2` and where the later card's layers start with `-e LAYER_SPLIT=18`.
|
||||
A memory limit needs `-e LOW_RAM=on`, which maps the model's experts from the pack instead of
|
||||
keeping them in RAM: setup.py measures the host's RAM, not the container's limit, so it cannot
|
||||
see a cap. LOW_RAM runs on one card.
|
||||
The server listens on `0.0.0.0:8080` by default; set `-e API_KEY=<secret>` before exposing the port
|
||||
to a network. The image has a `HEALTHCHECK` on `/health`, so `docker ps` shows the container
|
||||
healthy once the model is loaded, and `GET /v1/status` says what it is running.
|
||||
Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). You can add another model later with
|
||||
`SETUP.bat` (Linux: `./setup.sh --setup`).
|
||||
|
||||
## Using it
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="The Strata app's Monitor tab next to a coding agent"><br>
|
||||
<sub>The Strata app's <b>Monitor</b> (left) while a coding agent writes the pagoda garden from the video (right)</sub></p>
|
||||
|
||||
- **In the browser:** `http://127.0.0.1:8080` - the Strata app (it opens by itself when the model starts): **Chat**, a
|
||||
live **Monitor** of the model and your GPU/CPU/RAM, and **About** with the settings and addresses.
|
||||
- **Chat in the terminal:** `.venv\Scripts\python chat.py`
|
||||
- **Your apps and coding agents:** add it as an "OpenAI-compatible" provider with base URL
|
||||
**`http://127.0.0.1:8080/v1`**, any API key and any model name. Apps that use Anthropic's API: `http://127.0.0.1:8080/v1/messages`.
|
||||
- **Thinking:** the model thinks before it answers. Choose **off, low, medium or high** - in the chat page menu, with
|
||||
`/think low` in `chat.py`, or with your app's "reasoning effort" setting. Off is fastest; high is best for hard questions.
|
||||
- **Pictures:** in the chat page click **Picture**; in `chat.py` type `/image <path>`; in apps just attach them.
|
||||
- **From your phone or another PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`, then open the
|
||||
address the server window prints; see the [details](docs/DETAILS.md#using-it).
|
||||
- **Experimental speed projection (off by default):** an experimental control vector that setup can turn on; it
|
||||
changes how the model answers - read [what it does](docs/DETAILS.md#experimental-speed-projection-experimental-off-by-default) first.
|
||||
- **In the browser:** `http://127.0.0.1:8080` - **Chat**, a live **Monitor** of the model and your GPU/CPU/RAM, and
|
||||
**About** with the settings and addresses.
|
||||
- **Your apps and coding agents:** add an "OpenAI-compatible" provider with base URL **`http://127.0.0.1:8080/v1`**,
|
||||
any API key and any model name. Apps that use Anthropic's API: `http://127.0.0.1:8080/v1/messages` (Claude Code:
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
|
||||
- **Thinking:** choose **off, low, medium or high** in the chat menu or your app's "reasoning effort". Off is
|
||||
fastest; high is best for hard questions.
|
||||
- **Pictures:** say yes to "Images?" in setup, then click **Picture** in the chat, or attach them in your app
|
||||
(AMD cards: on Linux through the processor, not on Windows yet).
|
||||
- **From your phone or another PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>` - always with a key.
|
||||
- **Good to know:** it answers one request at a time. The first message of a chat is read in full (about 1 minute
|
||||
per 30,000 tokens); follow-ups start in seconds.
|
||||
|
||||
**Good to know:** it answers one request at a time. The first message of a chat is read in full (about 1 minute per
|
||||
30,000 tokens); after that it keeps the conversation and reads only what is new, so follow-ups start in seconds.
|
||||
|
||||
### Where things are stored
|
||||
|
||||
- **Your chats: only in your browser.** The Chat tab keeps the conversation, its settings and the API key you typed
|
||||
in the browser's local storage (`strata.*` keys) - not on the server and not in the Strata folder. Pictures are not
|
||||
kept, only their names. Another browser or a private window starts empty; clearing the site's data deletes them.
|
||||
- **How the model starts:** `strata-<model>.json` in the Strata folder (context, GPUs, host, API key, ...), written
|
||||
by setup; next to it `run-<model>.bat` / `.sh`, the log `strata-<model>.log` and, when you use "Use for other
|
||||
apps too", `strata-<model>.shared-settings.json`.
|
||||
- **The model files** (`models/`, `packs/`, `mtp/`, 70-120 GB): in **`Strata-data` next to the Strata folder**, or
|
||||
wherever `--data-dir` put them.
|
||||
- **Where that data folder is:** `%APPDATA%\Strata\settings.json` on Windows, `~/.config/strata/settings.json` on
|
||||
Linux ([details](docs/DETAILS.md)).
|
||||
More: [where your chats are stored](docs/INSTALL.md#where-things-are-stored), [the API](docs/DETAILS.md#using-it).
|
||||
|
||||
## Something went wrong?
|
||||
|
||||
**My PC froze, or got very slow, the first time Strata started.**
|
||||
That's normal while it starts, most of all the first time. Strata loads 35-55 GB into your RAM, locks part of it for
|
||||
the graphics card, and works out how much of the model fits on your GPU. The mouse can freeze for a few minutes. **Wait, and don't close the
|
||||
window.** The next starts are much faster. Still frozen after 10 minutes? Restart the PC, close other programs
|
||||
(browsers use a lot of RAM) and try again. If it keeps happening, pick a smaller size (Q2_0 or IQ2_XS).
|
||||
- **My PC froze the first time Strata started.** Normal while it loads the model: wait, don't close the window.
|
||||
Still frozen after 10 minutes? Restart the PC, close other programs and try again, or pick a smaller size.
|
||||
- **It stopped while downloading or installing.** Run `START-HERE.bat` (or `./setup.sh`) again: it continues where
|
||||
it stopped.
|
||||
- **It's very slow and the disk light keeps blinking, or "the engine stopped unexpectedly".** Not enough free RAM:
|
||||
close other programs (browsers use a lot), or pick a smaller size (Q2_0 or IQ2_XS).
|
||||
- **It says port 8080 is already in use.** Strata is already running - look for its window.
|
||||
|
||||
**It stopped while downloading or installing.**
|
||||
Run `START-HERE.bat` again. It continues where it stopped.
|
||||
|
||||
**It says the NVIDIA driver is too old.**
|
||||
Update it (NVIDIA App or [nvidia.com/drivers](https://www.nvidia.com/drivers)), restart the PC, and run
|
||||
`START-HERE.bat` again.
|
||||
|
||||
**It says port 8080 is already in use.**
|
||||
Strata is already running. Look for its window.
|
||||
|
||||
**It's very slow and the disk light keeps blinking.**
|
||||
Your PC is out of free RAM. Close other programs, or pick a smaller size (Q2_0 or IQ2_XS).
|
||||
|
||||
**An answer stopped with "the engine stopped unexpectedly".**
|
||||
Usually not enough RAM (on Linux the system then stops the engine). Just send your message again: Strata starts the
|
||||
engine by itself. If it keeps happening, close other programs or pick a smaller size.
|
||||
|
||||
**It says the prompt exceeds the context.**
|
||||
The conversation is longer than the context you chose. Start a new chat, or run `SETUP.bat` and pick more
|
||||
context.
|
||||
|
||||
**Still stuck?** Look in the [full troubleshooting table](docs/DETAILS.md#troubleshooting), or open an issue and
|
||||
attach `strata-<model>.log` from the Strata folder.
|
||||
More problems and their fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Still stuck? Open an
|
||||
[issue](https://github.com/Niko1221/Strata/issues) and attach `strata-<model>.log` from the Strata folder.
|
||||
|
||||
## How does it work?
|
||||
|
||||
Models like this one normally run on servers with hundreds of gigabytes of graphics memory. Your graphics card has
|
||||
12-24 GB. Strata makes it fit by **sharing the work across your whole PC** - the same idea as a kitchen, where the
|
||||
things you use all the time stay on the counter and the rest waits in the pantry.
|
||||
12-24 GB. Strata makes it fit by **sharing the work across your whole PC** - like a kitchen, where the things you use
|
||||
all the time stay on the counter and the rest waits in the pantry.
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
|
||||
|
||||
- **The model is a team of 24,576 small specialists ("experts"),** and each word it writes needs only 10 of them.
|
||||
So it doesn't have to have all of them on the graphics card at once.
|
||||
- **Your graphics card** does the part of the work needed for every word, and keeps the few thousand experts that
|
||||
are asked most often. It keeps learning which ones those are while you use it.
|
||||
- **Your RAM** holds every expert. When a word needs one the card doesn't have, **your processor** works on it -
|
||||
at the same time as the graphics card, so neither waits for the other.
|
||||
- **Your SSD** holds a big lookup table; the model only reads a few small rows of it per word.
|
||||
- **The model is a team of 24,576 small specialists ("experts"),** and each word needs only 10 of them.
|
||||
- **Your graphics card** keeps the few thousand experts that are asked most often; **your RAM** holds all of them,
|
||||
and **your processor** works on the rest at the same time. **Your SSD** holds a big lookup table.
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
|
||||
|
||||
- **Guess, then check.** A small, fast helper built into the model guesses the next few words, and the big model
|
||||
checks all the guesses in one go. It keeps the ones it agrees with and writes the next word itself - so one step
|
||||
often produces several words. The helper only guesses - the big model decides every word - so you get the same
|
||||
quality answer, 1.6-1.8x sooner.
|
||||
- **Long texts are read in big pieces** (up to 8,192 tokens - pieces of words - at a time), which is why a long
|
||||
document or code base is read at over 1,000 tokens per second.
|
||||
- **Guess, then check:** a small helper guesses the next few words and the big model checks them all at once, so
|
||||
you get the same answer, 1.6-1.8x sooner.
|
||||
- **Long texts are read in big pieces** (up to 8,192 tokens at a time): over 1,000 tokens per second.
|
||||
|
||||
Want the full picture? The [details](docs/DETAILS.md#how-it-works) explain every part and its numbers, and the
|
||||
[paper](docs/paper/Strata-Paper.pdf) tells the whole story, with the measurements behind it.
|
||||
The longer explanation: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Every part and its numbers: [the
|
||||
details](docs/DETAILS.md#how-it-works) and the [paper](docs/paper/Strata-Paper.pdf).
|
||||
|
||||
## Credits
|
||||
## Credits and license
|
||||
|
||||
- Model: [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team; compressed versions by
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF);
|
||||
[Swift 1.5](https://huggingface.co/ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF) by UkisAI. Their licenses apply
|
||||
to the model files.
|
||||
- Built with parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) (MIT). Ideas from
|
||||
[Splash](https://github.com/incoai/splash), [ninfer](https://github.com/Neroued/ninfer) and
|
||||
[HyperQwen](https://github.com/syv-ai/HyperQwen). More in the [details](docs/DETAILS.md#credits-and-licenses).
|
||||
The model is [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team, compressed by
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5) and Unsloth;
|
||||
Strata is built with parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). All credits:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata is open source under the [MIT License](LICENSE); a few
|
||||
parts and every model carry their own licenses ([which ones](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## License
|
||||
## Support Strata
|
||||
|
||||
Strata is open source under the [MIT License](LICENSE). A few parts carry their own licenses: `third_party/ggml`
|
||||
(MIT, llama.cpp / ggml), the web app's font (SIL Open Font License 1.1) and the experimental speed projection's
|
||||
vector in `data/experimental-speed-projection` (Qwen Community License 1.0, from the model's activations). The
|
||||
models are not part of this repository; each model's own license applies to its files.
|
||||
Strata is free and open source. If it is useful to you, you can support its development:
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
@echo off
|
||||
rem Strata for Windows: the first run installs everything and starts the model; later runs just start it.
|
||||
rem Needs only an NVIDIA driver. Python is installed for your user account if it is missing (no admin needed).
|
||||
rem Needs only an NVIDIA or AMD graphics driver. Python is installed for your user account if it is missing (no admin needed).
|
||||
setlocal
|
||||
title Strata
|
||||
cd /d "%~dp0"
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
@echo off
|
||||
rem Update Strata without starting the model (#475): the newest code (git pull, when this folder is a git clone), then
|
||||
rem what START-HERE.bat does before a start - the engine (a new one when this version needs it), the Python packages,
|
||||
rem each installed model's settings and draft subset. The model files are not touched. Start the model later with
|
||||
rem START-HERE.bat. Options are passed on to setup.py.
|
||||
setlocal
|
||||
title Strata - update
|
||||
cd /d "%~dp0"
|
||||
rem All of it in one block: cmd reads a .bat file while it runs it, and the git pull can change this very file.
|
||||
(
|
||||
if exist ".git" (
|
||||
where git >nul 2>nul || (
|
||||
echo This folder is a git clone, but git is not on PATH: install Git for Windows ^(winget install Git.Git^)
|
||||
echo or run "git pull" here yourself, then run UPDATE.bat again.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo Getting the newest Strata ^(git pull^) ...
|
||||
git pull --ff-only || (
|
||||
echo.
|
||||
echo git pull did not succeed ^(the reason is above^): nothing was updated. Files you changed here can stop it:
|
||||
echo "git status" lists them.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
) else (
|
||||
echo This copy of Strata was not made with git, so it cannot fetch new files itself. Download the newest one:
|
||||
echo https://github.com/Niko1221/Strata/archive/refs/heads/main.zip
|
||||
echo unzip it anywhere and run START-HERE.bat ^(or UPDATE.bat^) in it: it finds the model files in Strata-data
|
||||
echo and sets itself up the same way - nothing big is downloaded again.
|
||||
echo Checking this copy's engine and settings meanwhile ...
|
||||
)
|
||||
call "%~dp0START-HERE.bat" --update %*
|
||||
if not errorlevel 1 pause
|
||||
exit /b
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
# BF16 GEMV on Turing tensor cores (sm_75): measured negative
|
||||
|
||||
i7-8700K + RTX 2070 8 GB (sm_75, 36 SMs), engine 0.1.27 tree (`bf16_gemv.cu` is unchanged through 0.1.30),
|
||||
qwen3.8-flash-next (~125.7B MoE, GGUF-verified) Q2_0 in production. Isolated benchmark only -
|
||||
[`bench_tc_gemv.cu`](bench_tc_gemv.cu) touches nothing in the inference path. Question: the QSA prompt attention
|
||||
already rides the Turing tensor cores (`mma.sync.m16n8k8`, the #270 path); can the decode GEMVs do the same?
|
||||
|
||||
## Setup
|
||||
|
||||
- Baseline: the production `bf16_gemv` warp kernel, transcribed verbatim
|
||||
- TC variant: `mma.sync.aligned.m16n8k8.row.col.f32.f16.f16.f32` on BF16->FP16 (the sm_75 MMA has native f16 only).
|
||||
BF16->FP16 conversion is exact (0 / 2,000,000 values inexact - bf16's 7-bit mantissa fits in fp16's 10 bits); the
|
||||
A-fragment packing is the same one #270 validated (`a0` = row `gid`, k columns `2t..2t+1`; broadcast makes
|
||||
`a0 == a1 == {x[2t], x[2t+1]}`)
|
||||
- Three TC variants to cover the conversion overhead from every angle: W preconverted to fp16 once at load
|
||||
(`pc`), W converted in-register every call (`in`), and a fused ks=1 form that writes the output directly and skips
|
||||
the reduce kernel (`fused`)
|
||||
- The k-split across blocks (`ks`) is swept over all values that divide n_in, and the best is reported - the TC
|
||||
kernel gets its best occupancy
|
||||
- Parity against an fp64 host reference; 2000 iterations x 3 reps, warmed CUDA events
|
||||
|
||||
## Results (best TC config per shape)
|
||||
|
||||
| Shape (decode, n_in = 2560, BF16 W) | Baseline us | Best TC us | Speedup | Config |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| ssm_alpha/beta [2560, 48] | 8.73 | 11.32 | 0.77x | pc, ks=16 |
|
||||
| indexer.k_proj [2560, 128] | 7.14 | 12.09 | 0.59x | pc, ks=16 |
|
||||
| indexer.q_proj / router [2560, 512] | 9.48 | 15.78 | 0.60x | pc, ks=4 |
|
||||
| ple_value [2560, 2560] | 35.08 | 77.73 | 0.45x | pc, ks=40 |
|
||||
|
||||
- Numerics pass: TC max error vs fp64 reference 1.7e-6 .. 7.2e-6 (fp16-accumulation level), baseline
|
||||
0.7e-6 .. 2.3e-6. The TC variant computes the same thing, it is just slower.
|
||||
- The warp kernel is already bandwidth-bound at 276-374 GB/s effective W bandwidth (374 GB/s on the 13 MB
|
||||
[2560, 2560] shape); the TC path reaches only 166-169 GB/s (4-byte lane loads, conversion instructions, in-tile
|
||||
launch overhead).
|
||||
- A single-token GEMV broadcasts one token across the whole m16 MMA tile, so 15/16 of the MMA throughput is thrown
|
||||
away - but the baseline is not compute-bound, so the wasted throughput buys nothing. The k-split sweep and the
|
||||
fused variant do not recover it.
|
||||
- One-time W conversion cost is negligible (2 ms for 13 MB), so preconverting W for the TC path does not help.
|
||||
|
||||
## How to rerun
|
||||
|
||||
```
|
||||
nvcc -arch=sm_75 -O3 -o bench_tc_gemv.exe bench_tc_gemv.cu
|
||||
./bench_tc_gemv.exe
|
||||
```
|
||||
|
||||
Prints the table above, per-shape latency/parity and the conversion exactness scan. On a newer card, adjust
|
||||
`-arch`; the packed-A layout comment in the source documents the fragment mapping it relies on.
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Keep the warp kernel. Tensor cores are not a promising direction for the decode GEMVs on sm_75**, and this was a
|
||||
measured loss (1.3-2.2x slower), not a failed attempt to optimize. The Turing MMA stays worthwhile where the input
|
||||
really has m16 rows of work: the QSA prompt-attention tiles, which is where #270 uses it. Someone looking to
|
||||
re-evaluate on future hardware (a card with native bf16 or denser GEMV batching) can rerun this instead of
|
||||
rebuilding it.
|
||||
@@ -0,0 +1,305 @@
|
||||
// bench_tc_gemv.cu - ISOLATED benchmark: BF16 GEMV baseline vs Tensor Core (mma.m16n8k8, BF16->FP16) on sm_75.
|
||||
// Nothing in the engine changes. Shapes are the REAL Strata decode GEMVs (BF16 weights, n_in = n_embd = 2560):
|
||||
// ssm_alpha / ssm_beta [2560, 48] project_bf16(split=true) -> bf16_gemv_split(TPR=32) -> warp kernel
|
||||
// indexer.k_proj [2560, 128] project_bf16(split=false) -> bf16_gemv -> warp kernel
|
||||
// indexer.q_proj / ffn_gate_inp / mlp.gate [2560, 512] warp / split(TPR=32) -> warp kernel
|
||||
// ple_value [2560, 2560] warp
|
||||
// Baseline = bf16_gemv_warp_kernel transcribed verbatim from src/kernels/cuda/bf16_gemv.cu (production).
|
||||
// TC variant = same maths; A = x broadcast down m16 (single-token GEMV), k-chunk = 8;
|
||||
// (a) W preconverted bf16->fp16 once, x converted in-kernel (timed) [tc_preconv]
|
||||
// (b) fully inline: W read as bf16 and converted in-register [tc_inline]
|
||||
// Numerics: fp64 host reference; max abs err for baseline and both TC variants; bf16->fp16 exactness scan.
|
||||
#include <cuda_runtime.h>
|
||||
#include <cuda_fp16.h>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <algorithm>
|
||||
|
||||
#define CHECK(e) do { cudaError_t err_ = (e); if (err_ != cudaSuccess) { \
|
||||
fprintf(stderr, "CUDA %s:%d %s\n", __FILE__, __LINE__, cudaGetErrorString(err_)); exit(1); } } while (0)
|
||||
|
||||
__device__ __forceinline__ float bf16_f32(uint16_t v) { return __int_as_float((int) ((unsigned) v << 16)); }
|
||||
static float bf16_f32_host(uint16_t v) { unsigned u = (unsigned) v << 16; float f; memcpy(&f, &u, 4); return f; }
|
||||
static uint16_t f32_bf16_host(float f) {
|
||||
unsigned u, r; memcpy(&u, &f, 4);
|
||||
r = ((u >> 16) ^ (u >> 15)) & 1; u += 0x7fff + r;
|
||||
return (uint16_t) (u >> 16);
|
||||
}
|
||||
|
||||
// ============ baseline: production warp kernel (verbatim semantics from bf16_gemv.cu) ============
|
||||
__global__ void gemv_warp(const uint16_t* __restrict__ x, const uint16_t* __restrict__ w,
|
||||
float* __restrict__ y, long long n_in, long long n_out) {
|
||||
const long long o = (long long) blockIdx.x * (blockDim.x >> 5) + (threadIdx.x >> 5);
|
||||
if (o >= n_out) return;
|
||||
const int lane = threadIdx.x & 31;
|
||||
const uint16_t* row = w + o * n_in;
|
||||
float acc = 0.0f;
|
||||
for (long long i = lane; i < n_in; i += 32)
|
||||
acc += bf16_f32(x[i]) * bf16_f32(row[i]);
|
||||
for (int off = 16; off > 0; off >>= 1) acc += __shfl_down_sync(0xFFFFFFFFu, acc, off);
|
||||
if (lane == 0) y[o] = acc;
|
||||
}
|
||||
|
||||
// ============ TC: A(x) broadcast, k-split over blocks, partial D row 0 -> scratch ============
|
||||
// A fragment (m16n8k8, .f16): lane l: a0 = A[m=l>>2][k=2(l&3)], a1 = A[m][k=2(l&3)+1],
|
||||
// a2/a3 = same cols at row m+8. Broadcast => a2=a0, a3=a1.
|
||||
// B fragment (.f16, col-major): lane l: b0 = B[k=2(l&3)][n=(l>>2)], b1 = B[k=2(l&3)+1][n].
|
||||
// B[k][n] = W[n][k], so lane l loads W[n][2(l&3)], W[n][2(l&3)+1] - coalesced within the row.
|
||||
// D fragment: c0/c1 = D[m=l>>2][cols 2(l&3), +1], c2/c3 = row m+8. All m rows identical -> row 0.
|
||||
__global__ void tc_preconv(const uint16_t* __restrict__ x_b, const uint16_t* __restrict__ w_f,
|
||||
float* __restrict__ partials, long long n_in, long long n_out, int ks) {
|
||||
const int tile = blockIdx.x, ks_i = blockIdx.y, lane = threadIdx.x & 31;
|
||||
const long long n_base = (long long) tile * 8;
|
||||
const long long k_per = n_in / (long long) ks;
|
||||
const long long k0 = (long long) ks_i * k_per;
|
||||
const int t2 = 2 * (lane & 3);
|
||||
float c0 = 0.f, c1 = 0.f, c2 = 0.f, c3 = 0.f;
|
||||
for (long long kk0 = k0; kk0 < k0 + k_per; kk0 += 8) {
|
||||
// A (2 x f16x2): a0 = {A[m][2t], A[m+8][2t]}, a1 = {A[m][2t+1], A[m+8][2t+1]} (broadcast -> dup halves)
|
||||
unsigned xh = (unsigned) x_b[kk0 + t2] | ((unsigned) x_b[kk0 + t2 + 1] << 16);
|
||||
// fp16 conversion in-register: bf16 -> f32 -> f16 (exact within fp16 range)
|
||||
__half2 ha = __floats2half2_rn(bf16_f32((uint16_t) (xh & 0xFFFF)), bf16_f32((uint16_t) (xh >> 16)));
|
||||
__half2 hb = __floats2half2_rn(bf16_f32((uint16_t) (xh & 0xFFFF)), bf16_f32((uint16_t) (xh >> 16)));
|
||||
unsigned a0 = *reinterpret_cast<unsigned*>(&ha);
|
||||
unsigned a1 = *reinterpret_cast<unsigned*>(&hb);
|
||||
// B is already fp16 here: load as __half2 directly (bf16 path must NOT convert twice)
|
||||
__half2 wbh = *reinterpret_cast<const __half2*>(&w_f[(n_base + (lane >> 2)) * n_in + kk0 + t2]);
|
||||
unsigned b0 = *reinterpret_cast<unsigned*>(&wbh);
|
||||
asm volatile("mma.sync.aligned.m16n8k8.row.col.f32.f16.f16.f32 {%0,%1,%2,%3}, {%4,%5}, {%6}, {%0,%1,%2,%3};\n"
|
||||
: "+f"(c0), "+f"(c1), "+f"(c2), "+f"(c3)
|
||||
: "r"(a0), "r"(a1), "r"(b0));
|
||||
}
|
||||
if (lane < 4) { float* p = partials + (size_t) (tile * ks + ks_i) * 8; p[t2] = c0; p[t2 + 1] = c1; }
|
||||
}
|
||||
|
||||
// same, but W stays bf16 and converts in-register (fully fair per-call cost)
|
||||
__global__ void tc_inline(const uint16_t* __restrict__ x_b, const uint16_t* __restrict__ w_b,
|
||||
float* __restrict__ partials, long long n_in, long long n_out, int ks) {
|
||||
const int tile = blockIdx.x, ks_i = blockIdx.y, lane = threadIdx.x & 31;
|
||||
const long long n_base = (long long) tile * 8;
|
||||
const long long k_per = n_in / (long long) ks;
|
||||
const long long k0 = (long long) ks_i * k_per;
|
||||
const int t2 = 2 * (lane & 3);
|
||||
float c0 = 0.f, c1 = 0.f, c2 = 0.f, c3 = 0.f;
|
||||
for (long long kk0 = k0; kk0 < k0 + k_per; kk0 += 8) {
|
||||
unsigned xh = (unsigned) x_b[kk0 + t2] | ((unsigned) x_b[kk0 + t2 + 1] << 16);
|
||||
__half2 ha = __floats2half2_rn(bf16_f32((uint16_t) (xh & 0xFFFF)), bf16_f32((uint16_t) (xh >> 16)));
|
||||
__half2 hb = __floats2half2_rn(bf16_f32((uint16_t) (xh & 0xFFFF)), bf16_f32((uint16_t) (xh >> 16)));
|
||||
unsigned a0 = *reinterpret_cast<unsigned*>(&ha);
|
||||
unsigned a1 = *reinterpret_cast<unsigned*>(&hb);
|
||||
const uint16_t* wbv = w_b + (n_base + (lane >> 2)) * n_in + kk0 + t2;
|
||||
__half2 wbh = __floats2half2_rn(bf16_f32(wbv[0]), bf16_f32(wbv[1]));
|
||||
unsigned b0 = *reinterpret_cast<unsigned*>(&wbh);
|
||||
asm volatile("mma.sync.aligned.m16n8k8.row.col.f32.f16.f16.f32 {%0,%1,%2,%3}, {%4,%5}, {%6}, {%0,%1,%2,%3};\n"
|
||||
: "+f"(c0), "+f"(c1), "+f"(c2), "+f"(c3)
|
||||
: "r"(a0), "r"(a1), "r"(b0));
|
||||
}
|
||||
if (lane < 4) { float* p = partials + (size_t) (tile * ks + ks_i) * 8; p[t2] = c0; p[t2 + 1] = c1; }
|
||||
}
|
||||
|
||||
__global__ void tc_reduce(const float* __restrict__ partials, float* __restrict__ y, long long n_out, int ks) {
|
||||
const long long o = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (o >= n_out) return;
|
||||
float s = 0.f;
|
||||
for (int q = 0; q < ks; ++q) s += partials[(size_t) ((o >> 3) * ks + q) * 8 + (o & 7)];
|
||||
y[o] = s;
|
||||
}
|
||||
|
||||
// variant with the OTHER candidate A layout: a0 = {A[gid][2t], A[gid+8][2t]} (same k-col, two rows),
|
||||
// a1 = same for col 2t+1. Broadcast => both halves of a0 hold x[2t], both halves of a1 hold x[2t+1].
|
||||
__global__ void tc_dupcol(const uint16_t* __restrict__ x_b, const uint16_t* __restrict__ w_b,
|
||||
float* __restrict__ partials, long long n_in, long long n_out, int ks) {
|
||||
const int tile = blockIdx.x, ks_i = blockIdx.y, lane = threadIdx.x & 31;
|
||||
const long long n_base = (long long) tile * 8;
|
||||
const long long k_per = n_in / (long long) ks;
|
||||
const long long k0 = (long long) ks_i * k_per;
|
||||
const int t2 = 2 * (lane & 3);
|
||||
float c0 = 0.f, c1 = 0.f, c2 = 0.f, c3 = 0.f;
|
||||
for (long long kk0 = k0; kk0 < k0 + k_per; kk0 += 8) {
|
||||
__half2 ha = __floats2half2_rn(bf16_f32(x_b[kk0 + t2]), bf16_f32(x_b[kk0 + t2]));
|
||||
__half2 hb = __floats2half2_rn(bf16_f32(x_b[kk0 + t2 + 1]), bf16_f32(x_b[kk0 + t2 + 1]));
|
||||
unsigned a0 = *reinterpret_cast<unsigned*>(&ha);
|
||||
unsigned a1 = *reinterpret_cast<unsigned*>(&hb);
|
||||
const uint16_t* wbv = w_b + (n_base + (lane >> 2)) * n_in + kk0 + t2;
|
||||
__half2 wbh = __floats2half2_rn(bf16_f32(wbv[0]), bf16_f32(wbv[1]));
|
||||
unsigned b0 = *reinterpret_cast<unsigned*>(&wbh);
|
||||
asm volatile("mma.sync.aligned.m16n8k8.row.col.f32.f16.f16.f32 {%0,%1,%2,%3}, {%4,%5}, {%6}, {%0,%1,%2,%3};\n"
|
||||
: "+f"(c0), "+f"(c1), "+f"(c2), "+f"(c3)
|
||||
: "r"(a0), "r"(a1), "r"(b0));
|
||||
}
|
||||
if (lane < 4) { float* p = partials + (size_t) (tile * ks + ks_i) * 8; p[t2] = c0; p[t2 + 1] = c1; }
|
||||
}
|
||||
|
||||
// FUSED, ks=1 only: with a single k-fragment the partial IS the answer -> write y directly, no reduce kernel,
|
||||
// no scratch. Fully inline bf16 W conversion (fairest per-call TC cost).
|
||||
__global__ void tc_fused(const uint16_t* __restrict__ x_b, const uint16_t* __restrict__ w_b,
|
||||
float* __restrict__ y, long long n_in, long long n_out) {
|
||||
const int tile = blockIdx.x, lane = threadIdx.x & 31;
|
||||
const long long n_base = (long long) tile * 8;
|
||||
const int t2 = 2 * (lane & 3);
|
||||
float c0 = 0.f, c1 = 0.f, c2 = 0.f, c3 = 0.f;
|
||||
for (long long kk0 = 0; kk0 < n_in; kk0 += 8) {
|
||||
__half2 ha = __floats2half2_rn(bf16_f32(x_b[kk0 + t2]), bf16_f32(x_b[kk0 + t2 + 1]));
|
||||
__half2 hb = ha; // broadcast: row gid+8 = row gid (single-token GEMV)
|
||||
unsigned a0 = *reinterpret_cast<unsigned*>(&ha);
|
||||
unsigned a1 = *reinterpret_cast<unsigned*>(&hb);
|
||||
const uint16_t* wbv = w_b + (n_base + (lane >> 2)) * n_in + kk0 + t2;
|
||||
__half2 wbh = __floats2half2_rn(bf16_f32(wbv[0]), bf16_f32(wbv[1]));
|
||||
unsigned b0 = *reinterpret_cast<unsigned*>(&wbh);
|
||||
asm volatile("mma.sync.aligned.m16n8k8.row.col.f32.f16.f16.f32 {%0,%1,%2,%3}, {%4,%5}, {%6}, {%0,%1,%2,%3};\n"
|
||||
: "+f"(c0), "+f"(c1), "+f"(c2), "+f"(c3)
|
||||
: "r"(a0), "r"(a1), "r"(b0));
|
||||
}
|
||||
if (lane < 4) { y[n_base + t2] = c0; y[n_base + t2 + 1] = c1; }
|
||||
}
|
||||
|
||||
int main() {
|
||||
CHECK(cudaSetDevice(0));
|
||||
cudaDeviceProp prop; CHECK(cudaGetDeviceProperties(&prop, 0));
|
||||
printf("device: %s, SMs %d, capability %d.%d\n", prop.name, prop.multiProcessorCount, prop.major, prop.minor);
|
||||
|
||||
struct Shape { const char* name; long long n_in, n_out; };
|
||||
Shape shapes[] = {
|
||||
{"ssm_alpha/beta [2560,48]", 2560, 48},
|
||||
{"indexer.k_proj [2560,128]", 2560, 128},
|
||||
{"q_proj/router [2560,512]", 2560, 512},
|
||||
{"ple_value [2560,2560]", 2560, 2560},
|
||||
};
|
||||
auto rng = [](unsigned& s) { s = s * 1664525u + 1013904223u; return (float) ((s >> 8) & 0xFFFF) / 65535.0f - 0.5f; };
|
||||
const int ITERS = 2000, WARM = 300;
|
||||
|
||||
printf("\n%-26s %13s | %s\n", "shape", "baseline us", "tc variants (pc = W preconverted, in = inline conversion, fused = no reduce), speedup vs baseline");
|
||||
printf("%s\n", std::string(150, '-').c_str());
|
||||
|
||||
for (auto& sh : shapes) {
|
||||
const long long n_in = sh.n_in, n_out = sh.n_out;
|
||||
const size_t wn = (size_t) n_out * n_in;
|
||||
std::vector<uint16_t> hw(wn), hx(n_in), hf(wn);
|
||||
std::vector<float> hyb(n_out), hypc(n_out), hyin(n_out), hyfu(n_out);
|
||||
std::vector<double> href(n_out);
|
||||
unsigned seed = 991u + (unsigned) (n_out & 0xFFF);
|
||||
for (size_t i = 0; i < wn; ++i) hw[i] = f32_bf16_host(rng(seed) * 0.3f);
|
||||
for (long long i = 0; i < n_in; ++i) hx[i] = f32_bf16_host(rng(seed) * 3.0f);
|
||||
for (long long o = 0; o < n_out; ++o) {
|
||||
double s = 0.0;
|
||||
for (long long i = 0; i < n_in; ++i) s += (double) bf16_f32_host(hx[i]) * (double) bf16_f32_host(hw[(size_t) o * n_in + i]);
|
||||
href[o] = s;
|
||||
}
|
||||
// preconvert W to fp16 (bf16 -> fp16 is exact within fp16 range)
|
||||
for (size_t i = 0; i < wn; ++i) { __half h = __float2half_rn(bf16_f32_host(hw[i])); memcpy(&hf[i], &h, 2); }
|
||||
|
||||
uint16_t *dx, *dw, *dwf; float *dy, *dyp;
|
||||
CHECK(cudaMalloc(&dx, n_in * 2));
|
||||
CHECK(cudaMalloc(&dw, wn * 2)); CHECK(cudaMalloc(&dwf, wn * 2));
|
||||
CHECK(cudaMalloc(&dy, n_out * 4));
|
||||
CHECK(cudaMalloc(&dyp, (size_t)(n_out / 8) * 65 * 8 * 4));
|
||||
CHECK(cudaMemcpy(dx, hx.data(), n_in * 2, cudaMemcpyHostToDevice));
|
||||
CHECK(cudaMemcpy(dw, hw.data(), wn * 2, cudaMemcpyHostToDevice));
|
||||
CHECK(cudaMemcpy(dwf, hf.data(), wn * 2, cudaMemcpyHostToDevice));
|
||||
|
||||
// one-time W conversion cost (host->device, reported separately; the engine would fold this
|
||||
// into the weight load exactly like Gemm::bf16 does for sm<80)
|
||||
{
|
||||
cudaEvent_t e0, e1; CHECK(cudaEventCreate(&e0)); CHECK(cudaEventCreate(&e1));
|
||||
CHECK(cudaEventRecord(e0));
|
||||
CHECK(cudaMemcpy(dwf, hf.data(), wn * 2, cudaMemcpyHostToDevice));
|
||||
CHECK(cudaEventRecord(e1)); CHECK(cudaEventSynchronize(e1));
|
||||
float ms = 0; CHECK(cudaEventElapsedTime(&ms, e0, e1));
|
||||
printf("%-26s one-time W convert: %.2f ms (%.1f MB), tiles=%lld\n", sh.name, ms, wn * 2 / 1e6, n_out / 8);
|
||||
}
|
||||
|
||||
int tiles = (int) (n_out / 8);
|
||||
// valid ks must divide n_in so each k-chunk is a multiple of 8; sweep ALL of them, report the best
|
||||
const int cands[11] = {1, 2, 4, 5, 8, 10, 16, 20, 32, 40, 64};
|
||||
|
||||
auto timeit = [&](const char* tag, const std::function<void()>& fn) -> double {
|
||||
for (int i = 0; i < WARM; ++i) fn();
|
||||
CHECK(cudaDeviceSynchronize());
|
||||
double best = 1e30;
|
||||
for (int rep = 0; rep < 3; ++rep) {
|
||||
cudaEvent_t e0, e1; CHECK(cudaEventCreate(&e0)); CHECK(cudaEventCreate(&e1));
|
||||
CHECK(cudaEventRecord(e0));
|
||||
for (int i = 0; i < ITERS; ++i) fn();
|
||||
CHECK(cudaEventRecord(e1)); CHECK(cudaEventSynchronize(e1));
|
||||
float ms = 0; CHECK(cudaEventElapsedTime(&ms, e0, e1));
|
||||
double us = ms * 1000.0 / ITERS;
|
||||
if (us < best) best = us;
|
||||
}
|
||||
(void) tag; return best;
|
||||
};
|
||||
|
||||
double t_base = timeit("base", [&]{ gemv_warp<<<(unsigned) ((n_out + 7) / 8), 256>>>(dx, dw, dy, n_in, n_out); });
|
||||
gemv_warp<<<(unsigned) ((n_out + 7) / 8), 256>>>(dx, dw, dy, n_in, n_out);
|
||||
CHECK(cudaMemcpy(hyb.data(), dy, n_out * 4, cudaMemcpyDeviceToHost));
|
||||
|
||||
// occupancy sweep over k-chunk count for the preconverted-W TC kernel
|
||||
int best_ks = 1; double t_pc = 1e30;
|
||||
for (int i = 0; i < 11; ++i) {
|
||||
const int ksv = cands[i];
|
||||
if (n_in % ksv != 0) continue;
|
||||
dim3 g((unsigned) tiles, (unsigned) ksv);
|
||||
double t = timeit("pcs", [&]{ tc_preconv<<<g, 32>>>(dx, dwf, dyp, n_in, n_out, ksv);
|
||||
tc_reduce<<<(unsigned) ((n_out + 255) / 256), 256>>>(dyp, dy, n_out, ksv); });
|
||||
if (t < t_pc) { t_pc = t; best_ks = ksv; }
|
||||
}
|
||||
dim3 gtc((unsigned) tiles, (unsigned) best_ks);
|
||||
tc_preconv<<<gtc, 32>>>(dx, dwf, dyp, n_in, n_out, best_ks);
|
||||
tc_reduce<<<(unsigned) ((n_out + 255) / 256), 256>>>(dyp, dy, n_out, best_ks);
|
||||
CHECK(cudaMemcpy(hypc.data(), dy, n_out * 4, cudaMemcpyDeviceToHost));
|
||||
|
||||
double t_ti = timeit("tci", [&]{ tc_inline<<<gtc, 32>>>(dx, dw, dyp, n_in, n_out, best_ks);
|
||||
tc_reduce<<<(unsigned) ((n_out + 255) / 256), 256>>>(dyp, dy, n_out, best_ks); });
|
||||
tc_inline<<<gtc, 32>>>(dx, dw, dyp, n_in, n_out, best_ks);
|
||||
tc_reduce<<<(unsigned) ((n_out + 255) / 256), 256>>>(dyp, dy, n_out, best_ks);
|
||||
CHECK(cudaMemcpy(hyin.data(), dy, n_out * 4, cudaMemcpyDeviceToHost));
|
||||
|
||||
double t_fu = timeit("tcf", [&]{ tc_fused<<<(unsigned) tiles, 32>>>(dx, dw, dy, n_in, n_out); });
|
||||
tc_fused<<<(unsigned) tiles, 32>>>(dx, dw, dy, n_in, n_out);
|
||||
CHECK(cudaMemcpy(hyfu.data(), dy, n_out * 4, cudaMemcpyDeviceToHost));
|
||||
|
||||
double best_tc = t_pc; const char* best_var = "preconv";
|
||||
if (t_ti < best_tc) { best_tc = t_ti; best_var = "inline"; }
|
||||
if (t_fu < best_tc) { best_tc = t_fu; best_var = "fused"; }
|
||||
|
||||
double e_tc = 0, e_base = 0, sc = 0;
|
||||
if (strcmp(best_var, "preconv") == 0) {
|
||||
for (long long o = 0; o < n_out; ++o) { e_tc = std::max(e_tc, std::fabs((double) hypc[o] - href[o])); }
|
||||
} else if (strcmp(best_var, "inline") == 0) {
|
||||
for (long long o = 0; o < n_out; ++o) { e_tc = std::max(e_tc, std::fabs((double) hyin[o] - href[o])); }
|
||||
} else {
|
||||
for (long long o = 0; o < n_out; ++o) { e_tc = std::max(e_tc, std::fabs((double) hyfu[o] - href[o])); }
|
||||
}
|
||||
for (long long o = 0; o < n_out; ++o) {
|
||||
e_base = std::max(e_base, std::fabs((double) hyb[o] - href[o]));
|
||||
sc = std::max(sc, std::fabs(href[o]));
|
||||
}
|
||||
double gb_base = (double) (wn * 2) / t_base / 1e3;
|
||||
double gb_tc = (double) (wn * 2) / best_tc / 1e3;
|
||||
printf("%-26s %13.3f | pc %13.3f (ks=%d) | in %13.3f | fused %13.3f | BEST %s %7.3fx | maxerr tc %.2e base %.2e (scale %.1f, BW base %.0f tc %.0f GB/s)\n",
|
||||
sh.name, t_base, t_pc, best_ks, t_ti, t_fu, best_var, t_base / best_tc,
|
||||
e_tc, e_base, sc, gb_base, gb_tc);
|
||||
|
||||
CHECK(cudaFree(dx)); CHECK(cudaFree(dw)); CHECK(cudaFree(dwf)); CHECK(cudaFree(dy)); CHECK(cudaFree(dyp));
|
||||
}
|
||||
|
||||
{ // bf16 -> fp16 exactness scan
|
||||
unsigned seed = 777u; double worst = 0; int bad = 0, n = 2000000;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
float f = (rng(seed) * 2.0f - 1.0f) * 100.0f;
|
||||
uint16_t b = f32_bf16_host(f);
|
||||
__half h = __float2half_rn(bf16_f32_host(b));
|
||||
if ((double) __half2float(h) != (double) bf16_f32_host(b)) {
|
||||
bad++; worst = std::max(worst, std::fabs((double) __half2float(h) - (double) bf16_f32_host(b)));
|
||||
}
|
||||
}
|
||||
printf("\nbf16->fp16 conversion exactness: %d / %d inexact, worst %.3g\n", bad, n, worst);
|
||||
}
|
||||
printf("BENCH_DONE\n");
|
||||
return 0;
|
||||
}
|
||||
+25
-9
@@ -1,17 +1,25 @@
|
||||
# Opt-in HIP configuration. Strata's CUDA-shaped kernels target wave32 RDNA3 / RDNA4 (64 KiB LDS per workgroup,
|
||||
# the signed dot4 instruction). CMake/compiler discovery stays machine-independent; pass CMAKE_HIP_COMPILER when it
|
||||
# Opt-in HIP configuration. Strata's CUDA-shaped kernels target wave32 RDNA2 / RDNA3 / RDNA4 (64 KiB LDS per
|
||||
# workgroup, a signed dot4 instruction). CMake/compiler discovery stays machine-independent; pass CMAKE_HIP_COMPILER when it
|
||||
# is not on PATH.
|
||||
if(NOT DEFINED CMAKE_HIP_ARCHITECTURES OR CMAKE_HIP_ARCHITECTURES STREQUAL "")
|
||||
set(CMAKE_HIP_ARCHITECTURES gfx1100 CACHE STRING "Strata HIP target architecture(s), e.g. gfx1100 or gfx1100;gfx1201")
|
||||
endif()
|
||||
# Validated on real cards: gfx1100 (RX 7900 XT / XTX) and gfx1201 (RX 9070 / 9070 XT, Radeon AI PRO R9700) by the
|
||||
# maintainers; gfx1101 (RX 7800 XT, #254) and gfx1200 (RX 9060 XT, #256) by their owners. gfx1102 (RX 7600) has the
|
||||
# same LDS limit and dot4 instruction and passed ctest (#192), but no model run has been reported yet.
|
||||
# same LDS limit and dot4 instruction and passed ctest (#192), but no model run has been reported yet. RDNA2 gfx1030 (RX 6800 / 6900) has the
|
||||
# same LDS limit and wave32 but an older dot4 instruction (v_dot4_i32_i8, hip_compat/intrinsics.hpp); a community
|
||||
# report ran it (#311), the maintainers have not.
|
||||
set(_strata_hip_validated gfx1100 gfx1201)
|
||||
set(_strata_hip_community gfx1101 gfx1200)
|
||||
set(_strata_hip_unvalidated gfx1102)
|
||||
set(_strata_hip_unvalidated gfx1102 gfx1030)
|
||||
# CMake hands HIP a ';' list, but a -DCMAKE_HIP_ARCHITECTURES typed by hand (or ROCm's own Windows tooling) may use
|
||||
# spaces, which foreach(IN LISTS) would otherwise treat as one element.
|
||||
string(REPLACE " " ";" _strata_hip_norm "${CMAKE_HIP_ARCHITECTURES}")
|
||||
set(STRATA_HIP_ARCH_LIST "")
|
||||
foreach(_arch IN LISTS CMAKE_HIP_ARCHITECTURES)
|
||||
foreach(_arch IN LISTS _strata_hip_norm)
|
||||
if(_arch STREQUAL "")
|
||||
continue()
|
||||
endif()
|
||||
string(REGEX REPLACE ":.*$" "" _base "${_arch}") # gfx1100:xnack- -> gfx1100
|
||||
if(_base IN_LIST _strata_hip_validated)
|
||||
elseif(_base IN_LIST _strata_hip_community)
|
||||
@@ -60,11 +68,19 @@ target_include_directories(strata_hip_runtime BEFORE INTERFACE
|
||||
"${STRATA_HIP_COMPAT_INCLUDE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||
target_compile_definitions(strata_hip_runtime INTERFACE STRATA_USE_HIP=1 "STRATA_HIP_ARCHS=\"${STRATA_HIP_ARCHS}\"")
|
||||
target_link_libraries(strata_hip_runtime INTERFACE hip::host)
|
||||
foreach(_language IN ITEMS CXX HIP)
|
||||
# The shim renames the CUDA runtime to HIP, force-included into every host and device source. On Windows the host
|
||||
# compiler is ROCm's clang++ too (tools/hip/build_windows.bat: CMake refuses to mix cl.exe with Clang HIP), which takes
|
||||
# -include like it does on Linux; an MSVC-style front end (cl / clang-cl) takes /FI instead. Forward slashes, so /FI
|
||||
# does not read the path's backslashes as escapes.
|
||||
file(TO_CMAKE_PATH "${STRATA_HIP_COMPAT_INCLUDE_DIR}/cuda_runtime.h" _strata_hip_force)
|
||||
if(MSVC)
|
||||
target_compile_options(strata_hip_runtime INTERFACE "$<$<COMPILE_LANGUAGE:CXX>:/FI${_strata_hip_force}>")
|
||||
else()
|
||||
target_compile_options(strata_hip_runtime INTERFACE
|
||||
"$<$<COMPILE_LANGUAGE:${_language}>:-include>"
|
||||
"$<$<COMPILE_LANGUAGE:${_language}>:${STRATA_HIP_COMPAT_INCLUDE_DIR}/cuda_runtime.h>")
|
||||
endforeach()
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:-include>" "$<$<COMPILE_LANGUAGE:CXX>:${_strata_hip_force}>")
|
||||
endif()
|
||||
target_compile_options(strata_hip_runtime INTERFACE
|
||||
"$<$<COMPILE_LANGUAGE:HIP>:-include>" "$<$<COMPILE_LANGUAGE:HIP>:${_strata_hip_force}>")
|
||||
|
||||
# CMake does not infer HIP from Strata's existing CUDA-shaped .cu suffixes.
|
||||
file(GLOB_RECURSE _strata_hip_sources CONFIGURE_DEPENDS
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,196 @@
|
||||
# Installing Strata for a user (instructions for AI agents)
|
||||
|
||||
This page is written for an AI coding assistant (Claude Code, Cursor, Codex, GitHub Copilot, ...) that a user asked to
|
||||
set up Strata on their PC. Strata runs the Qwen3.8-Flash-Next model locally on one NVIDIA or AMD graphics card plus
|
||||
system RAM, and serves an OpenAI- and Anthropic-compatible API on `http://127.0.0.1:8080`. The human-oriented
|
||||
version of everything below is [INSTALL.md](INSTALL.md).
|
||||
|
||||
Work through the steps in order. Tell the user what you are doing in plain words; they may not be technical.
|
||||
|
||||
## 0. Ground rules
|
||||
|
||||
- **Never expose the server without an API key.** Keep the default `--host 127.0.0.1`. Only if the user asks for
|
||||
access from other devices, use `--host 0.0.0.0` **together with** `--api-key <a long random secret>`, and give the
|
||||
user the key. Never put a tunnel or port forward in front of a server without a key.
|
||||
- Do not change the user's system beyond what setup does (setup installs Python for the user account if needed,
|
||||
and everything else inside the Strata folder and its `Strata-data` folder). Ask before installing drivers.
|
||||
- The model download is ~70 GB (up to 111 GB for Unsloth's 4-bit). Confirm the user is fine with that before you
|
||||
start, especially on a metered connection.
|
||||
- Setup is long-running (an hour or more on a slow connection). Run it in the background or with a long timeout and
|
||||
poll its output; do not kill it because it is quiet for a while. It is resumable: running the same command again
|
||||
continues where it stopped.
|
||||
|
||||
## 1. Check the PC
|
||||
|
||||
The quickest way is setup's own check, which prints the GPU(s), driver, RAM, CPU, free disk and whether this PC can run
|
||||
Strata, and installs nothing except Python and the `.venv` it needs to run (step 2 gets the repository first):
|
||||
|
||||
```
|
||||
Windows: START-HERE.bat --check
|
||||
Linux: ./setup.sh --check
|
||||
```
|
||||
|
||||
To check by hand:
|
||||
|
||||
| What | Windows (PowerShell) | Linux |
|
||||
| --- | --- | --- |
|
||||
| OS | `[Environment]::OSVersion` | `cat /etc/os-release` |
|
||||
| NVIDIA GPU, VRAM, driver | `nvidia-smi --query-gpu=index,name,memory.total,driver_version --format=csv` | the same |
|
||||
| AMD GPU | `Get-CimInstance Win32_VideoController \| Select-Object Name, AdapterRAM` (AdapterRAM is capped at 4 GB; trust the model name) | `lspci \| grep -i -E 'vga\|display'`; VRAM: `cat /sys/class/drm/card*/device/mem_info_vram_total` (bytes); `rocm-smi` only if ROCm is installed |
|
||||
| RAM | `(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB` | `free -g` |
|
||||
| Free disk | `Get-PSDrive -PSProvider FileSystem` | `df -h .` |
|
||||
|
||||
Requirements (details: [INSTALL.md](INSTALL.md#what-you-need)):
|
||||
|
||||
- **GPU:** NVIDIA RTX 20, 30, 40 or 50 series, or AMD Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT,
|
||||
RX 9070 / 9070 XT, Radeon AI PRO R9700, RX 6800 / 6900 series; 12 GB of VRAM or more (an NVIDIA card with 8 GB runs,
|
||||
slowly). GTX 10 series and older, and integrated GPUs, are not supported.
|
||||
- **Driver:** NVIDIA 580 or newer. AMD on Linux: the kernel's amdgpu driver; on Windows: a current AMD Adrenalin
|
||||
driver. If the driver is missing or too old, tell the user to update it (NVIDIA App / nvidia.com/drivers, or AMD
|
||||
Software) and restart; do not install drivers yourself unless they ask.
|
||||
- **RAM:** 32 GB or more (see step 3). **Disk:** ~80 GB free, ideally on an NVMe SSD. **CPU:** x86-64 with AVX2.
|
||||
- **OS:** Windows 10/11 or Linux (Ubuntu 22.04/24.04 are fully automatic).
|
||||
|
||||
If the PC does not meet them, say which part is missing and stop.
|
||||
|
||||
## 2. Get Strata
|
||||
|
||||
```
|
||||
git clone https://github.com/Niko1221/Strata.git
|
||||
```
|
||||
|
||||
Without git: download https://github.com/Niko1221/Strata/archive/refs/heads/main.zip and unzip it. Pick a drive with
|
||||
enough free space: the model files go to a `Strata-data` folder **next to** the Strata folder (or `--data-dir <path>`
|
||||
on another drive).
|
||||
|
||||
## 3. Pick the model
|
||||
|
||||
By the PC's RAM (ask the user whether they mainly want it for code - then the Coder is a good pick at any RAM size):
|
||||
|
||||
| RAM | `--family` / `--model` | Notes |
|
||||
| --- | --- | --- |
|
||||
| 32 GB | `--family coder` (size IQ1_M) | for code; with a 24 GB card Q2_0 and IQ2_XS also run (setup picks the low-RAM mode) |
|
||||
| 48 GB | `--family qwen --model IQ2_XS` | or `Q2_0` (fastest) |
|
||||
| 64 GB | `--family qwen --model IQ2_XS` (recommended) | `IQ3_XXS` / `IQ3_S` are slower and a bit better |
|
||||
| 96 GB+ | `--family qwen --model IQ3_S` | `--family unsloth --model UD-Q4_K_XL` is experimental: NVIDIA only, NVMe SSD, 7-8.5 tokens/s on 64 GB |
|
||||
|
||||
`--family swift` (Swift 1.5, a fine-tune that thinks shorter; sizes Q2_0, IQ2_XS, IQ3_XXS) is the alternative to
|
||||
`qwen`. With `--yes` and no `--model`, setup picks the recommended size for the RAM itself. More: [MODELS.md](MODELS.md).
|
||||
|
||||
## 4. Run setup without questions
|
||||
|
||||
From the Strata folder (Windows: in `cmd`, or `cmd /c START-HERE.bat ...` from PowerShell):
|
||||
|
||||
```
|
||||
Windows: START-HERE.bat --yes --family qwen --model IQ2_XS --no-start
|
||||
Linux: ./setup.sh --yes --family qwen --model IQ2_XS --no-start
|
||||
```
|
||||
|
||||
The flags (all of them: `START-HERE.bat --help`):
|
||||
|
||||
| Flag | Meaning |
|
||||
| --- | --- |
|
||||
| `--yes` | take the recommended answer to every question (no prompts) |
|
||||
| `--family qwen\|swift\|coder\|unsloth` | the model version |
|
||||
| `--model Q2_0\|IQ2_XS\|IQ3_XXS\|IQ3_S\|IQ1_M\|UD-Q4_K_XL` | the size (the Coder is IQ1_M, Unsloth UD-Q4_K_XL) |
|
||||
| `--context N` | context in tokens; default by VRAM: 32768 under 14 GB, 65536 under 20 GB, else 131072 |
|
||||
| `--vision yes\|no\|gpu\|cpu` | read pictures; `--yes` leaves images off. AMD cards: `cpu` |
|
||||
| `--gpu N` / `--gpus 0,1` / `--gpus all` | one card, or several sharing the model (default: the card with the most VRAM) |
|
||||
| `--backend cuda\|hip` | NVIDIA or AMD engine; chosen by itself on a PC with only one kind of card |
|
||||
| `--data-dir PATH` | where the 70-120 GB of model files go |
|
||||
| `--port N` | the server port (default 8080) |
|
||||
| `--host H --api-key K` | listen beyond this PC; **only together with a key** |
|
||||
| `--no-start` | install only, do not start the server |
|
||||
| `--setup` | install another model or change settings of an installed one |
|
||||
| `--check` | only check the PC |
|
||||
|
||||
`--no-start` is recommended for agents: the server runs in the foreground until its window is closed, which would
|
||||
block your shell. Start it separately in step 6.
|
||||
|
||||
Notes:
|
||||
- **Linux:** setup uses `sudo apt` (or dnf/pacman) to install Python with venv if it is missing, and on AMD may need
|
||||
`build-essential` and `git`. You cannot type the user's password: if a `sudo` step is needed, ask the user to run it
|
||||
(e.g. `sudo apt install python3-venv build-essential git`) and then rerun setup.
|
||||
- **AMD on Linux:** setup installs ROCm into `.venv` (~10 GB, no sudo) unless a system ROCm 7 exists, and compiles the
|
||||
engine for the card (10-20 minutes, once).
|
||||
- **AMD on Windows (new in 0.1.34):** setup downloads the ready-made AMD engine (~550 MB, ROCm included) - nothing
|
||||
is compiled and only the AMD driver is needed. Before the model download it runs `engine\strata-device.exe
|
||||
--list-devices`; if that does not list the card, the driver is the problem (tell the user to update AMD Software).
|
||||
One card per model and no images on Windows for now. It is new: ask the user to report how it runs (docs/AMD_HIP.md).
|
||||
- **NVIDIA with no ready-made engine for the card:** setup offers to install build tools and compile (20-40 minutes);
|
||||
`--yes` accepts.
|
||||
|
||||
## 5. While it downloads, tell the user
|
||||
|
||||
- It downloads about 70 GB from Hugging Face; how long depends on their connection (at 100 Mbit/s roughly 1.5-2
|
||||
hours). It can be stopped and continues where it left off.
|
||||
- Then it prepares the model for their PC (a few minutes) and, on AMD on Linux, compiles the engine.
|
||||
- **When the model starts, the PC can be slow or stop responding for 1-3 minutes** (longest the first time): Strata
|
||||
loads 35-55 GB into RAM and locks part of it for the graphics card. That is normal; they should not close the window.
|
||||
- Nothing leaves their PC: the model runs locally.
|
||||
|
||||
## 6. Start the server
|
||||
|
||||
Setup prints the start script it wrote (`start script: run-<model>.bat`). The name is the family tag plus the size,
|
||||
lower case: `run-iq2_xs`, `run-swift-iq2_xs`, `run-coder-iq1_m`, `run-unsloth-ud-q4_k_xl`.
|
||||
|
||||
```
|
||||
Windows (PowerShell): Start-Process -FilePath ".\run-iq2_xs.bat" (opens its own window)
|
||||
Linux: nohup ./run-iq2_xs.sh > strata-server.out 2>&1 &
|
||||
```
|
||||
|
||||
Or `START-HERE.bat` / `./setup.sh` without flags, which starts the installed model (it asks which one when several are
|
||||
installed; add `--yes` to take the first). The server opens the user's browser on `http://127.0.0.1:8080` when it is
|
||||
ready. Closing its window (or stopping the process) stops the model.
|
||||
|
||||
## 7. Verify
|
||||
|
||||
The HTTP server answers once the model is loaded (30-90 s on later starts, a few minutes the first time). Poll:
|
||||
|
||||
```
|
||||
curl http://127.0.0.1:8080/health
|
||||
```
|
||||
|
||||
It returns `{"status": "ok", "model": ..., "max_context": ..., "images": ..., "api_key": ..., "loaded": true, ...}`.
|
||||
Then:
|
||||
|
||||
```
|
||||
curl http://127.0.0.1:8080/v1/models
|
||||
curl http://127.0.0.1:8080/v1/chat/completions -H "Content-Type: application/json" -d "{\"model\": \"strata\", \"messages\": [{\"role\": \"user\", \"content\": \"Say hello in five words.\"}], \"max_tokens\": 64, \"reasoning_effort\": \"none\"}"
|
||||
```
|
||||
|
||||
With an API key, add `-H "Authorization: Bearer <key>"`. The server window prints a `ready: http://127.0.0.1:8080/v1`
|
||||
line when it is ready; the engine log is `strata-<model>.log` in the Strata folder.
|
||||
|
||||
## 8. Connect the user's apps
|
||||
|
||||
- **Browser:** `http://127.0.0.1:8080` - Chat, a live Monitor, and About (settings and addresses).
|
||||
- **Any OpenAI-compatible app or agent:** base URL `http://127.0.0.1:8080/v1`, any API key (or the configured one),
|
||||
any model name.
|
||||
- **Anthropic-compatible apps:** `http://127.0.0.1:8080/v1/messages`.
|
||||
- **Claude Code:** `ANTHROPIC_BASE_URL=http://127.0.0.1:8080`, `ANTHROPIC_MODEL` set to a Claude model name it knows
|
||||
(Strata ignores the name), and any `ANTHROPIC_AUTH_TOKEN` (or the configured key).
|
||||
- **Thinking level:** `"reasoning_effort": "none" | "low" | "medium" | "high"` (default high).
|
||||
- Strata answers one request at a time. API details: [DETAILS.md](DETAILS.md#using-it).
|
||||
|
||||
## 9. When something fails
|
||||
|
||||
| Symptom | What to do |
|
||||
| --- | --- |
|
||||
| `the NVIDIA driver is too old` | The user updates the driver (NVIDIA App or nvidia.com/drivers) and restarts; rerun setup. |
|
||||
| Download or install stopped | Rerun the same setup command; it continues. |
|
||||
| `port 8080 is already in use` | Strata is already running (check `/health`), or use `--port 8081`. |
|
||||
| Very slow, disk busy, or "the engine stopped unexpectedly" | Not enough free RAM: close programs, or set up a smaller size (`--setup --model Q2_0` or IQ2_XS). |
|
||||
| `ExpertCache: cudaMalloc(...) failed: out of memory` with free VRAM (Windows) | The page file is off or tiny: set it to "System managed" and restart. |
|
||||
| `prompt ... exceeds the context` | Rerun setup with `--setup --context <bigger>`. |
|
||||
| No AMD GPU found (Linux) | The amdgpu driver lists no GPU; check the card and driver. Integrated GPUs are not supported. |
|
||||
| Python or build tools could not be installed | Install what the message names (links are printed), then rerun. |
|
||||
|
||||
More: [TROUBLESHOOTING.md](TROUBLESHOOTING.md) and the [full table](DETAILS.md#troubleshooting). If it still fails,
|
||||
collect `strata-<model>.log` and the setup output, and suggest an issue at https://github.com/Niko1221/Strata/issues.
|
||||
|
||||
## Alternative: the MCP server
|
||||
|
||||
Strata also has an MCP server, so an AI tool can check, install, start and stop Strata through tool calls
|
||||
(`strata_status`, `strata_models`, `strata_install`, `strata_start`, `strata_stop`, `strata_logs`) instead of the
|
||||
shell commands above. See [MCP_SERVER.md](MCP_SERVER.md).
|
||||
+143
-12
@@ -1,13 +1,15 @@
|
||||
# Experimental AMD HIP backend (gfx1100, gfx1101, gfx1200, gfx1201)
|
||||
# AMD Radeon: the HIP backend (gfx1100, gfx1101, gfx1200, gfx1201, gfx1030)
|
||||
|
||||
This is a Linux source build for the RX 7900 XT / XTX (RDNA3, gfx1100) and the
|
||||
Strata runs on AMD Radeon cards through its HIP backend, the same engine as on NVIDIA compiled for AMD. This page
|
||||
covers the build on Linux (on Windows a ready-made engine, see [Windows](#windows)) for the RX 7900 XT / XTX (RDNA3, gfx1100) and the
|
||||
RX 9070 / 9070 XT / Radeon AI PRO R9700 (RDNA4, gfx1201; see [RDNA4](#rdna4-gfx1201)). The RX 7800 XT / 7700 XT
|
||||
(gfx1101) and the RX 9060 XT (gfx1200) were validated by their owners (see [Community-validated
|
||||
cards](#community-validated-cards)). It is opt-in; the NVIDIA installer and CUDA build remain the default. Other AMD
|
||||
architectures, wave64, Windows HIP, and mixed AMD/NVIDIA execution are outside this contribution.
|
||||
cards](#community-validated-cards)); the RX 6800 / 6900 series (RDNA2, gfx1030) builds and runs too, reported by a community machine and not yet validated by the maintainers (see [RDNA2](#rdna2-gfx1030)). Setup chooses it by itself on a PC with no NVIDIA card Strata can use (`--backend hip` on a PC with both); the
|
||||
install steps for users are in [INSTALL.md](INSTALL.md#amd-cards). Other AMD architectures, wave64, and mixed
|
||||
AMD/NVIDIA execution in one run are not supported.
|
||||
|
||||
The backend maps the CUDA-shaped runtime and BLAS calls to HIP/hipBLAS, uses
|
||||
RDNA3/RDNA4's signed integer dot instruction for quantized kernels, and supplies
|
||||
RDNA2/RDNA3/RDNA4's signed integer dot instruction for quantized kernels, and supplies
|
||||
wave32 shuffle/packed-byte operations. CUDA-only QSA matrix instructions have
|
||||
an ordered FP32 fallback. Prefill supports both dequantization plus hipBLAS GEMM and opt-in HIP ggml MMQ.
|
||||
An optional, calibrated hipBLASLt path accelerates dense projections (per-architecture tables in `tools/hip`).
|
||||
@@ -28,7 +30,9 @@ the kernel's amdgpu driver (no ROCm install needed):
|
||||
- **ROCm:** a system ROCm 7 in `/opt/rocm` (or `$ROCM_PATH`) with hipcc and hipBLAS is used when present. Otherwise
|
||||
(or when it is older than 7.0) ROCm is installed into `.venv` from AMD's TheRock wheels (~10 GB, no sudo), pinned
|
||||
to the version this backend was tested with, from the card family's index: `gfx110X-dgpu` for gfx1100 / gfx1101,
|
||||
`gfx120X-all` for gfx1200 / gfx1201 (`STRATA_ROCM_VERSION` / `STRATA_ROCM_INDEX` override them).
|
||||
`gfx120X-all` for gfx1200 / gfx1201, `gfx103X-all` for gfx1030 (`STRATA_ROCM_VERSION` /
|
||||
`STRATA_ROCM_INDEX` override them; the gfx1030 index is not checked to carry the pinned version: a system
|
||||
ROCm 7 is the tested path there).
|
||||
- **Engine:** compiled on your PC for the card's architecture (10-20 minutes, once; again after a `git pull` that
|
||||
changes it, or when you pick a card of another architecture). This needs a C++ compiler and git
|
||||
(`sudo apt install build-essential git`).
|
||||
@@ -42,10 +46,69 @@ the kernel's amdgpu driver (no ROCm install needed):
|
||||
architectures above; the engine is compiled for each of them (cards of two families, e.g. gfx1100 + gfx1201, need
|
||||
a system ROCm 7: AMD's wheels hold one family). A split pays only when no single card holds the model's experts
|
||||
(see RDNA4 below).
|
||||
- **Limits for now:** no images, no calibration. The Monitor shows no GPU statistics.
|
||||
- **Limits for now:** images only through the CPU encoder (`--vision cpu`, 0.1.32), no calibration. The Monitor
|
||||
shows the card's load, VRAM, temperature and power from Linux sysfs (0.1.32).
|
||||
|
||||
The rest of setup is the same as on NVIDIA: the model download, the start script, the server.
|
||||
|
||||
## Windows
|
||||
|
||||
Since 0.1.34 an AMD card on Windows is set up like an NVIDIA one: download Strata, double-click `START-HERE.bat`.
|
||||
On a PC with no NVIDIA card Strata can use, the AMD card is chosen by itself; with both, setup asks
|
||||
(`START-HERE.bat --backend hip` picks AMD directly).
|
||||
|
||||
- **You need:** Windows 10 or 11 (64-bit), one of the cards above, and a current AMD driver ([AMD Software:
|
||||
Adrenalin Edition](https://www.amd.com/en/support/download/drivers.html)). Nothing else: no ROCm or HIP SDK
|
||||
install, no compiler, no admin rights.
|
||||
- **Detection:** setup reads the display adapters Windows lists (their PCI ids; the VRAM size from the display
|
||||
driver's registry entry). An integrated Radeon is listed as not supported.
|
||||
- **Engine:** the ready-made `strata-windows-x64-hip.zip` from the release (built by `tools\hip\build_windows.bat`
|
||||
for gfx1100, gfx1101, gfx1102, gfx1200, gfx1201 and gfx1030) goes into `engine\`. It carries the ROCm libraries the
|
||||
engine loads (`engine\rocm\bin`: the HIP runtime, hipBLAS / rocBLAS / hipBLASLt with their kernels for these cards,
|
||||
amd_comgr and the Microsoft C++ runtime; ROCm 10.2.0a20260930 from AMD's TheRock builds, licenses in
|
||||
`engine\rocm\licenses`). The HIP runtime works through the AMD driver's own components, so the driver is the one
|
||||
thing it needs from the PC.
|
||||
- **The HIP runtime next to `strata.exe` (0.1.35, #468 #461):** `amdhip64_7.dll` and `amd_comgr.dll` are also put in
|
||||
`engine\` (setup copies them there on every start). Windows looks in the program's folder before System32, where
|
||||
some AMD drivers install their own `amdhip64_7.dll`; with that one, the bundled libraries crashed on the first
|
||||
prompt (an access violation, or `hipErrorInvalidDeviceFunction`). The engine's log names the runtime it loaded
|
||||
(`strata generate: HIP runtime ...`).
|
||||
- **Before the ~60 GB model download** setup runs `engine\strata-device.exe --list-devices` (with `engine\rocm\bin` on
|
||||
the PATH): if the HIP runtime does not see the card, setup stops there and points to the driver. It also gives the
|
||||
card's HIP number: with an integrated Radeon that is device 1, not 0 (#325). From then on setup lists the AMD cards
|
||||
as HIP numbers them, so `--gpu N` and the config's `"gpu"` are HIP numbers.
|
||||
- **Differences from Windows-on-NVIDIA and Linux-on-AMD:** no images yet (the CPU image encoder is Linux-only for
|
||||
now), one card per model (`--gpus` is Linux-only for now), no calibration.
|
||||
- Two Windows-only engine details (#247, #325): hipBLAS can return success and still leave `hipErrorInvalidValue`
|
||||
set after some BF16/FP16 GEMMs (seen on gfx1201); the engine clears that one stale error after a GEMM that
|
||||
succeeded, on Windows only. `hipHostGetDevicePointer` returns the host pointer itself on Windows: kernels read
|
||||
mapped memory through it correctly, but a device-to-device copy into it does not land, which is why
|
||||
`tests/hip/handoff` times out there (the engine does not use that copy; `tests/hip/mapped_alias` reports it).
|
||||
|
||||
**What is validated (0.1.34):** #325's author ran the engine of this port on an RX 9070 XT (Windows 11, ROCm
|
||||
10.2.0a20260930 in `.venv`, compiled on the PC): Coder IQ1_M at 32K, 29.2 tok/s decode, ~181 tok/s prefill,
|
||||
correct answers; ctest 42 of 46. The maintainers have no Windows AMD card: the release zip was built on an NVIDIA PC,
|
||||
and checked there on the Ryzen CPU's integrated Radeon (gfx1036, a test build of the same tree): with only the zip's
|
||||
libraries on the PATH, `strata-device` lists the card and a hipBLAS BF16 GEMM matches the CPU; the HIP ctest passes
|
||||
52 of 56, the 4 failures the same as on the RX 9070 XT (`hip_handoff`, and three tests that need a pack fixture).
|
||||
The ready-made zip itself has not run a model on a discrete card yet - please report.
|
||||
|
||||
**Reporting a Windows AMD run** (an issue, or on #325): your card and driver version (AMD Software > System), then
|
||||
|
||||
```bat
|
||||
engine\strata-device.exe --list-devices
|
||||
engine\strata-device.exe --selftest
|
||||
```
|
||||
|
||||
(from the Strata folder, after `set PATH=%CD%\engine\rocm\bin;%PATH%`), the end of `strata-<model>.log`, and the
|
||||
speed lines the server window prints for a first answer.
|
||||
|
||||
**Building it yourself:** `tools\hip\build_windows.bat` (Visual Studio 2022 Build Tools with the C++ workload, Python,
|
||||
git; no admin, no AMD GPU) installs ROCm from AMD's TheRock wheels into `.rocm-win`, builds, and packages
|
||||
`dist\strata-windows-x64-hip.zip`; `START-HERE.bat --backend hip --prebuilt dist\` installs that one.
|
||||
`tools\hip\build_windows.bat tests` also builds the HIP tests (`ctest` in `build-hip-win`, with
|
||||
`.rocm-win\Lib\site-packages\_rocm_sdk_devel\bin` on the PATH). `STRATA_HIP_ARCHS` picks other architectures.
|
||||
|
||||
## Build
|
||||
|
||||
Requirements: a working ROCm driver/runtime, HIP development headers and
|
||||
@@ -63,7 +126,7 @@ cmake --build build-hip --target strata -j2
|
||||
|
||||
`CMAKE_HIP_ARCHITECTURES` is `gfx1100`, `gfx1101`, `gfx1200`, `gfx1201`, or a list such as `"gfx1100;gfx1201"`
|
||||
(one binary for both). gfx1102 (the same wave32, 64 KiB LDS and dot4 instruction) builds with a warning: it passed
|
||||
ctest (#192) but no model run has been reported. At startup the engine and `strata-device` compare each GPU they use
|
||||
ctest (#192) but no model run has been reported; so does gfx1030 (RDNA2: the older `v_dot4_i32_i8`, a community run in #311). At startup the engine and `strata-device` compare each GPU they use
|
||||
(`gcnArchName` up to the `:` feature suffix) with the architectures the binary was compiled for, and require
|
||||
wave32. A binary carried to another card stops with the card's name, its architecture and the build's list,
|
||||
instead of failing later with "invalid device function".
|
||||
@@ -120,7 +183,7 @@ The worker count above was used on a 16-core CPU; measure it for your CPU.
|
||||
The 4K context is a smoke-test starting point, not a model limit. The expert cache
|
||||
sizes itself automatically and leaves 1 GiB of VRAM headroom.
|
||||
|
||||
The installer supports this backend (see "Install with setup" above). The vision helper is NVIDIA-only for now.
|
||||
The installer supports this backend (see "Install with setup" above). Images run through the CPU encoder for now (`--vision cpu`).
|
||||
Setup installs one AMD card, or several with `--gpus` (the engine's layer split; see RDNA4 below).
|
||||
|
||||
## RDNA4 (gfx1201)
|
||||
@@ -154,9 +217,11 @@ an RX 9070 XT 16 GB and a Radeon AI PRO R9700 32 GB (both gfx1201), a Ryzen 9 39
|
||||
The engine's resident memory was about 26 GB in every run. Since 0.1.31 `__byte_perm` is one `v_perm_b32` and the
|
||||
packed byte subtracts/compare work on four lanes at once (#262, ttio2tech): decode +15% on the R9700 (46.0 -> 53.0
|
||||
tok/s on a 4K prompt, 52.0 -> 60.5 warm) and +5-7% on the 9070 XT, prompts unchanged, the same tokens.
|
||||
- **hipBLASLt:** there is no gfx1201 table in `tools/hip`. A table calibrated on the R9700 at the engine's shapes
|
||||
(hipBLASLt 1.4.1; 0.98-1.76x per GEMM over hipBLAS) changed the end-to-end prompt speed by 0-3%, within noise,
|
||||
so none is shipped: on gfx1201 the plain hipBLAS path is already close.
|
||||
- **hipBLASLt:** the validation above used hipBLASLt 1.4.1. A table calibrated on the R9700 at the engine's shapes
|
||||
with that version (0.98-1.76x per GEMM over hipBLAS) changed the end-to-end prompt speed by 0-3%, within noise,
|
||||
so none was shipped for it: there the plain hipBLAS path is already close. For hipBLASLt 1.5.0 (ROCm
|
||||
10.2.0a nightly) `tools/hip/gfx1201-hipblaslt-100500.txt` is shipped (see "Tuning table" below); on one R9700 it
|
||||
measured +3.9% prompt speed on 4,210-token prompts (1,590 vs 1,531 tok/s), a modest gain.
|
||||
- **Both cards in one run (layer split, engine 0.1.30):** the config's `"backend": "hip", "gpu": [1, 0]` (R9700
|
||||
first) runs through `serve/server.py` (setup writes it with `--gpus 1,0` since 0.1.31). Auto split put layers 0-27 on the R9700 and
|
||||
28-47 on the 9070 XT. With every expert on the GPUs the split gives exactly the tokens of the R9700 alone (4K and
|
||||
@@ -176,6 +241,26 @@ an RX 9070 XT 16 GB and a Radeon AI PRO R9700 32 GB (both gfx1201), a Ryzen 9 39
|
||||
starts. `"split_skip_if_fits": true` in the config (0.1.31, opt-in) runs such a pair on the first card alone when it
|
||||
holds every profiled expert: with the R9700 first, 4K prompts 1,776 tok/s (split: 1,244) and decode ~60 tok/s
|
||||
(split: ~51), the tokens of the R9700 alone (docs/MULTI_GPU.md).
|
||||
- **Speed switches (engine 0.1.32, measured on the R9700 / 9070 XT with the Coder IQ1_M pack):**
|
||||
- the MoE router (`router_top10`) runs a HIP kernel without its serial FP64 sum and block barriers by default: the
|
||||
same ids and weights bit for bit (`hip_router_fast` checks 65,536 rows), 39 -> 9-12 us per call, the same greedy
|
||||
tokens (5 + 5 starts). Decode: 62.4 -> 70.0 tok/s on the R9700 and +4% on the 9070 XT in one A/B here (ROCm 10.2
|
||||
nightly); a user's repeated A/B/A/B with setup's TheRock 7.10 wheels and an i5-12600K measured +1-4%, inside a
|
||||
12-20% run-to-run spread (#432) - how much it gains depends on how much of decode the router is on that setup.
|
||||
`STRATA_HIP_ROUTER_OLD=1` runs the portable kernel.
|
||||
- `STRATA_HIP_WMMA=1` (opt-in, gfx12 only, int8 KV): the prompt path's QSA attention on RDNA4 matrix cores
|
||||
(`v_wmma_f32_16x16x16_f16`, FP16 hi + lo halves like the CUDA tensor-core kernel; `hip_prompt_attn_wmma` bounds it
|
||||
against the FP32 kernel and FP64). 7.2-7.5x the portable kernel; R9700 prompts 4K 1,784 -> 2,427 tok/s, 16K
|
||||
1,797 -> 2,700 (a user with TheRock 7.10: +29-34%, #432). Not bitwise: greedy text differs from token ~50 on, as with the CUDA tensor-core attention. With it
|
||||
the prompt path's expert ring is 96 slots (as STRATA_PREFILL_RING=96): with the default 384 the 9070 XT's 4K
|
||||
prompts fell to 718 tok/s; with 96 they gain (1,017 -> 1,211; 16K 1,518 -> 2,032). PR #329
|
||||
(bsorensen110) contributed an equivalent gfx12 WMMA kernel of the same speed (within 1%); this one also masks KV
|
||||
pages that KV streaming has not made resident, as the decode kernel does.
|
||||
- The prompt path's QSA top-k picks its kernel by the blocks a query actually has, not the cache's capacity (#337,
|
||||
bsorensen110): the same ids, on by default with AMD (NVIDIA keeps its capacity rule: there the 64K prompts read
|
||||
1-3% slower with it). `STRATA_SELECT_WMMA=1` (opt-in, gfx12) adds #337's
|
||||
matrix-core block scorer; it selects slightly differently (254 of 256 queries the same) and gained +1.5% on 16K
|
||||
prompts at a 262K context on the R9700.
|
||||
- **Known:** rarely (about 1 start in 10) a HIP run's greedy output differs from another start's at some token, on
|
||||
one card or two and on engine 0.1.29 as well; not yet explained.
|
||||
- **Not validated:** images, long contexts beyond 16K, answer-quality benchmarks.
|
||||
@@ -203,6 +288,38 @@ is shipped for them (make one with [Tuning table](#tuning-table) and compare the
|
||||
Greedy output was the same across runs. For comparison, llama.cpp's HIP build measured 20 tok/s decode and
|
||||
450 tok/s prompt on that card.
|
||||
|
||||
## RDNA2 (gfx1030)
|
||||
|
||||
The RX 6800 / 6800 XT / 6900 XT / 6950 XT run the same kernels: wave32 and 64 KiB of LDS per workgroup. The one
|
||||
difference is the dot instruction: RDNA2 has no `v_dot4_i32_iu8` (gfx11 and newer), so
|
||||
`dp4a` uses the plain signed `v_dot4_i32_i8` through `__builtin_amdgcn_sdot4`, which compiles to a single
|
||||
`v_dot4c_i32_i8` (same signed x signed byte products, modulo 2^32). There is no WMMA; the QSA scorer takes the
|
||||
same ordered FP32 fallback as gfx1100. CMake lists gfx1030 as unvalidated (the build warns) until a maintainer has
|
||||
run it; the report below is from a community machine: an RX 6900 XT 16 GB (gfx1030), an i7-13700KF (8 P-cores and
|
||||
8 E-cores, AVX2, no AVX-512), 63 GB RAM, NixOS, ROCm 7.2.3 from nixpkgs (clang 22, hipBLAS 3.2).
|
||||
|
||||
- **Build and tests** (engine 0.1.26): a complete HIP build for gfx1030, made by hand with cmake and ROCm's own
|
||||
`clang++` (the nixpkgs ROCm is not an `/opt/rocm` tree, so setup's `build_engine_hip` was not exercised; the
|
||||
binary was placed in `engine/` for setup to use). All 28 registered ctest tests pass on the card, including
|
||||
`hip_device_selftest`. An engine 0.1.30 build of this branch configures, builds and runs clean on the same card;
|
||||
the speeds below are measured on it.
|
||||
- **End to end** (Swift 1.5 IQ3_XXS, `--context 131072` with `--kv-resident 32768 --adapt-every 1
|
||||
--vram-reserve-mib 1024`, 200 greedy tokens): 38-42 tok/s decode with the default 15 CPU pool workers (one per
|
||||
physical core except the host thread; 8 workers: 36; 24 = every logical core: 27), consistent even with the
|
||||
131,072-token context full.
|
||||
- **Prefill** (the same 15-worker configuration): 246 tok/s on a 2,000-token prompt, 330-339 tok/s at the auto
|
||||
8,192-token chunk (7,997 and 15,967 tokens; time to first token 8.9 and 48.3 s), 133 tok/s on a 522-token
|
||||
prompt - short prompts are fixed overhead (19-25 tok/s on 34 tokens). Decode after a 16K prefill holds at
|
||||
45.5 tok/s.
|
||||
- **16 GB card:** the expert cache holds 6,310 slots (10.2 GiB) at 16K context and 5,247 slots at 131,072, where
|
||||
setup keeps the KV cache in VRAM because IQ3_XXS needs about 60 GB of RAM plus the cache to stream it. The
|
||||
`--pcie-frac 0` and `--adapt-every 0` of the 7900 XTX configuration in
|
||||
[AMD_HIP_PERFORMANCE.md](AMD_HIP_PERFORMANCE.md) cost 8.6 and 13.6 tok/s here (30 with the defaults): keep the
|
||||
defaults on a 16 GB card.
|
||||
- **hipBLASLt:** ROCm's hipBLASLt ships no gfx1030 kernels, so there is no table and the plain hipBLAS path runs.
|
||||
- **Not validated:** gfx1031 / gfx1032 (the same `dp4a` path, no hardware report), setup's own build path and the
|
||||
`gfx103X-all` wheels on gfx1030, images, answer-quality benchmarks.
|
||||
|
||||
## Tuning table
|
||||
|
||||
A hipBLASLt table holds solution ids that are valid only for one GPU architecture and one hipBLASLt version, so it
|
||||
@@ -219,6 +336,20 @@ The file's second line names the architecture and version (`STRATA_HIPBLASLT_TUN
|
||||
as `tools/hip/<arch>-hipblaslt-<version>.txt` for setup, or point `STRATA_HIPBLASLT_TUNING` at it. Compare the
|
||||
prompt speed with and without it before keeping it.
|
||||
|
||||
Shipped tables:
|
||||
|
||||
- `gfx1100-hipblaslt-100100.txt`, `gfx1100-hipblaslt-100200.txt`: RX 7900 XTX.
|
||||
- `gfx1201-hipblaslt-100500.txt`: Radeon AI PRO R9700 (gfx1201, 32 GB), calibrated with ROCm 10.2.0a20260914
|
||||
(AMD's `gfx120X-all` nightly, hipBLASLt 1.5.0, library build `d3164197`). 16 dense GEMM geometries at T=4096 and
|
||||
T=8192, 32 rows. setup uses it only when the installed hipBLASLt reports 1.5.0 (it is found in `/opt/rocm`
|
||||
when that is a system ROCm 7 or newer). The version number is the only thing the engine can check, so another
|
||||
1.5.0 build could number its solutions differently. `hip_prefill_hipblaslt_gemm` (with `STRATA_HIPBLASLT_TUNING`
|
||||
set) is a smoke test: it refuses a table for another architecture or version and checks that one BF16 and one
|
||||
F16 row exist and agree with hipBLASEx, which covers 2 of the 32 rows. It does not prove that a solution id is
|
||||
valid: the engine falls back to hipBLASEx for an id the library rejects, and the test still passes. Run it with
|
||||
`STRATA_HIPBLASLT_VERBOSE=1` and look for `fallbacks=0` in its summary line, and recalibrate with
|
||||
`tune_hipblaslt` before using this table with a different 1.5.0 build.
|
||||
|
||||
## Original backend validation (PR #94)
|
||||
|
||||
The following is historical validation of the original backend, not a fresh
|
||||
|
||||
+161
-17
@@ -1,11 +1,13 @@
|
||||
# Strata - the details
|
||||
|
||||
The technical side of Strata: every measured number, the API, images, all settings and how the engine works.
|
||||
New here? Start with the [README](../README.md) - it has everything you need to install and use it.
|
||||
New here? Start with the [README](../README.md); installing step by step is in [INSTALL.md](INSTALL.md), the models in
|
||||
[MODELS.md](MODELS.md), common problems in [TROUBLESHOOTING.md](TROUBLESHOOTING.md).
|
||||
|
||||
> **On this page:** [Speed](#speed-measured) · [Other GPUs](#other-gpus-estimated) · [Which model?](#which-model) ·
|
||||
> [Requirements](#before-you-start) · [Windows](#windows) · [Linux](#linux) · [API](#using-it) ·
|
||||
> [MCP tools](#tools-from-mcp-servers) · [Images](#images-vision) ·
|
||||
> [MCP tools](#tools-from-mcp-servers) · [MCP server](#manage-strata-from-your-ai-assistant-mcp-server) ·
|
||||
> [Images](#images-vision) ·
|
||||
> [Troubleshooting](#troubleshooting) · [How it works](#how-it-works)
|
||||
|
||||
---
|
||||
@@ -17,6 +19,15 @@ RTX 5070 **12 GB**, Ryzen 5 7600 (6 cores), 64 GB DDR5-5200, Windows, engine 0.1
|
||||
MTP speculative decoding on. "262K" is the model's full context window (a 259,943-token prompt). The IQ2_XS row was
|
||||
measured with Swift 1.5's IQ2_XS, which runs at the original's speed.
|
||||
|
||||
**Engine 0.1.36 (#136), the same PC:** Q2_0's prompt experts run on fused int8 tensor-core kernels (RTX 30 and newer):
|
||||
4K 1,294 -> 1,570, 32K 2,170 -> 2,653, 128K 2,123 -> 2,468 tokens/s (+16-22%), as close to an FP16 reference as the
|
||||
previous kernels (closer at 32K: teacher-forced KL 0.009 vs 0.012). The decode path's block selection and greedy
|
||||
argmax run on thread-block clusters (RTX 50, sm_90+; other cards keep the previous kernels; the same tokens): Q2_0 output at 4K 89 -> 93.5, at 128K
|
||||
64.5 -> 76.4 tokens/s. `STRATA_PF_FUSED=0` keeps the previous prompt kernels (byte-identical answers to 0.1.35);
|
||||
`STRATA_PF_FUSED=1` also runs the native IQ packs' fused kernels (opt-in: IQ2_XS prompts +12% at 4K, +3% at 32K, the
|
||||
IQ3 packs about even); `STRATA_QSA_CLUSTER=0` / `STRATA_ARGMAX_MULTI=0` turn the decode kernels off. The tables
|
||||
below are 0.1.26's.
|
||||
|
||||
### Prompt processing (tokens/s)
|
||||
|
||||
| Model | 1K | 4K | 32K | 64K | 128K | 262K |
|
||||
@@ -50,14 +61,17 @@ accepted, so a different answer to the same prompt moves it by several percent.
|
||||
[`bench/results/2026-09-28-speed-0114`](../bench/results/2026-09-28-speed-0114/README.md).
|
||||
|
||||
IQ3_XXS and IQ3_S at 262K are not measured: with their 43 / 50 GB of experts, a 260K-token context brings a 64 GB PC
|
||||
to its memory limit. Use up to 128K with them on 64 GB (setup caps it). IQ3_S (engine 0.1.4 or newer) is only published
|
||||
for the original model, not for Swift 1.5.
|
||||
to its memory limit by setup's estimate (the experts + the context's KV cache + 24 GB), so setup recommends up to 128K
|
||||
with them on 64 GB. A longer context you choose (`--context 262144`, or a pick in its list) is kept, with a note: users
|
||||
ran IQ3_S at 256K on 64 GB with RAM to spare (#406). In the low-RAM mode the KV cache stays in VRAM and the context
|
||||
does not count against RAM. IQ3_S (engine 0.1.4 or newer) is only published for the original model, not for Swift 1.5.
|
||||
|
||||
**KV streaming (engine 0.1.5):** at 64K and more, setup keeps the context's KV cache in RAM and only the part the
|
||||
attention reads in VRAM (`--kv-resident 32768`), so more experts fit on the GPU. Q2_0 at 262K: 50.9 -> 62.6 tokens/s
|
||||
(1,589 -> 3,872 experts in VRAM); at 128K about +6%. The attention reads exactly the same values (only where the KV lives changes); it
|
||||
costs ~13.7 KB of RAM per context token (1.7 GB at 128K). Existing installs: run `START-HERE.bat --setup` once to turn
|
||||
it on.
|
||||
it on. Setup turns it on when the RAM has room for it; `--kv-streaming on|off` overrides that (on past the RAM test with a
|
||||
note; never with `--kv k8v4` or under WSL, which cannot stream).
|
||||
|
||||
**4-bit KV cache (engine 0.1.8, optional):** `START-HERE.bat --setup` asks above 8K context (or pass `--kv q4_0`). It
|
||||
halves the KV cache's memory with a Hadamard rotation before 4-bit rounding (PR #21), about 4% faster at 128K, but it
|
||||
@@ -76,14 +90,27 @@ slightly differently. How many tokens share an expert depends on the drafts in a
|
||||
at temperature 0 can end in a different (equally good) answer when the drafting, the cache state or a resumed
|
||||
conversation differ (issue #152). `STRATA_IQ_MT_MIN=1` (in the config's `env`) uses the multi-token kernels for
|
||||
every group: the answer then no longer depends on the drafting. Measured on a Ryzen 7600 (AVX-512): IQ3_S decode
|
||||
-1..-3%, the other models the same; the default stays the fastest rule.
|
||||
-1..-3%, the other models the same; the default stays the fastest rule. Through the server, two more things carry
|
||||
over from one request to the next (#410): the adaptive tier moves experts between RAM and VRAM (the GPU and the CPU
|
||||
round an expert differently), and the prompt cache resumes a repeated prompt and reads only its tail through the
|
||||
decode path. For byte-identical repeats add `--prompt-cache 0 --adapt-swaps 0 --pcie-frac 0` to the engine's args
|
||||
as well (#410): the PCIe share of the missed experts (computed on the GPU instead of the CPU) still made the first
|
||||
answer after a start differ from the next ones. Measured here (IQ3_XXS, a 3.6K-token prompt, 4 repeats): with all
|
||||
three switches 1 answer of 4, without `--pcie-frac 0` 2 of 4 (the first one differs), with the defaults 2 of 4.
|
||||
`--pcie-frac 0` costs decode speed (the missed experts all run on the CPU), so keep it for A/B runs.
|
||||
|
||||
**The draft layer's tokens (0.1.27, `--draft-vocab`):** the MTP draft layer can only propose tokens from a subset
|
||||
of the vocabulary (`mtp/rt/draft_vocab.bin`). Since 0.1.27 the subset includes every Chinese, Japanese and Korean
|
||||
token (106,299 ids), so answers in those languages are 15-38% faster (Q2_0, RTX 5070). Its head takes ~180 MiB of
|
||||
VRAM, which the expert cache leaves free for it (0.1.28). `START-HERE.bat --setup --draft-vocab en` keeps the
|
||||
English/code subset from before (40,525 ids, ~110 MiB less VRAM, English answers 1-2% faster; CJK answers get
|
||||
almost no drafts). `tools/draft_vocab.py` builds and inspects subsets.
|
||||
almost no drafts). `--draft-vocab cyrillic` takes the English/code subset plus the whole Cyrillic script (58,963
|
||||
ids): the shipped subsets hold 142 of the vocabulary's 18,580 Cyrillic tokens, so Ukrainian or Russian answers got
|
||||
1.4 tokens a round; with it 2.1, and 83 -> 109 tokens/s (RTX 5090, the NVFP4 fork), English unchanged.
|
||||
`tools/draft_vocab.py` builds and inspects subsets. When the start stops with "the draft head does not fit" (a
|
||||
12 GB card with a long context, #474), the engine says how much the head needs, how much VRAM is free and which
|
||||
smaller subset fits, and the server's start error repeats it; setup suggests `--draft-vocab en` on cards under
|
||||
14 GB (only a suggestion: nothing changes unless you pass it).
|
||||
|
||||
**Low-RAM mode (engine 0.1.26, chosen by setup):** normally all of a model's experts are copied into RAM (23-50 GB,
|
||||
pinned) and the GPU holds a copy of the most-used ones. On a PC whose RAM cannot hold them beside the system (the
|
||||
@@ -111,6 +138,12 @@ other ~18 GB), a 32 GB PC with a 12-16 GB GPU the Coder; IQ3_XXS on a 32 GB PC s
|
||||
not hold do not fit, it says so and runs the plain mapped mode. The server log shows, per request, how many expert
|
||||
reads went to the file (`resident RAM: ... blob reads from the file`: 0 in steady use).
|
||||
- `--low-ram resident|mmap` forces one variant (also on a PC with enough RAM, e.g. to try it).
|
||||
- Several GPUs (#364, #384): setup recommends one GPU in the low-RAM mode (the resident variant has no layer split
|
||||
yet), and asks; `--gpus 0,1` (or answering 2) shares the model across them with the mapped variant
|
||||
(`--mmap-experts`): the cards together hold more of the experts, and two users measured it 1.3-1.6x faster than
|
||||
one card, but the OS file cache can fill the RAM to 0 free during long prompts. `--yes` keeps one GPU. A config
|
||||
with `--resident-experts` started with `--gpus` switches to `--mmap-experts` with a note, and the engine runs that
|
||||
pair as `--mmap-experts` with a warning instead of refusing it.
|
||||
|
||||
**Low-RAM mode without `experts.bin` (engine 0.1.31):** for the native packs (IQ2_XS, IQ3_XXS, IQ3_S, the Coder, Swift,
|
||||
Q2_0 packed by `tools/iq_pack.py`; not the canonical Q2_0 pack setup makes for AVX-512 CPUs) the mapped mode no longer
|
||||
@@ -125,7 +158,9 @@ engine fetches a layer's missing experts on 8 threads (`STRATA_FETCH_THREADS`) w
|
||||
**A RAM budget (engine 0.1.31, `--resident-budget-gib N`):** the resident variant for a model whose experts do not all
|
||||
fit: the N GiB of experts the GPU cache does not hold that the expert profile ranks hottest are copied into RAM at
|
||||
start (locked; page-locked when the driver allows the whole budget), and the rest are read from the files through the
|
||||
OS file cache. It implies `--mmap-experts` and leaves 4 GB of free RAM (a larger N is clamped, with a message). With
|
||||
OS file cache. It implies `--mmap-experts` and leaves 4 GB of free RAM (a larger N is clamped to that less 256 MiB,
|
||||
with a message; #403: a clamped budget no longer fails the safety check that follows, and a budget that cannot be
|
||||
kept at all is a warning, with every expert read from the files). Setup sets N with `--resident-budget-gib N`. With
|
||||
the GGUF read in place it also warms the next layer's likely experts: while the CPU works on a layer, a thread applies
|
||||
the next layer's router to this layer's input and asks the OS for the pages of the predicted experts that neither the
|
||||
GPU nor the RAM budget holds (only pages - the experts computed are the same; `STRATA_LOOKAHEAD=0` turns it off). This
|
||||
@@ -136,7 +171,8 @@ RTX 5070, against ~3 tokens/s before these changes.
|
||||
RAM copy, blobs and MB from the files, the time spent reading them; `routing prefetch`: how many of the file reads had
|
||||
been warmed). The server log has the same per request (`expert tiers: GPU ... hits ...; RAM ... blobs, files ...
|
||||
blobs ... MB read`), and `GET /metrics` lists `ram_blobs`, `file_blobs` and `file_mb` for each recent request (with
|
||||
engine 0.1.31 or newer).
|
||||
engine 0.1.31 or newer). It also lists each request's speculative drafts, `drafts_offered` and `drafts_accepted`
|
||||
(`null` when the engine did not report them), and their sums since the server started in `totals` (#457).
|
||||
|
||||
Time to first token is prompt length / prompt speed: with Q2_0 about 4 s at 4K, 25 s at 32K, under 2 minutes at 128K
|
||||
and 4.5 minutes at 262K (engine 0.1.13 made long prompts about twice as fast, below).
|
||||
@@ -221,20 +257,24 @@ thought about for 1,524 tokens. Not a benchmark, but consistent with the claim.
|
||||
START-HERE.bat --setup --family swift --model IQ2_XS
|
||||
```
|
||||
|
||||
### Experimental: Unsloth's UD-Q4_K_XL (manual import)
|
||||
### Experimental: Unsloth's UD-Q4_K_XL
|
||||
|
||||
A 4-bit quantization of the same model (111 GB, 72 GiB of experts). Not in setup: packing it and the server
|
||||
configuration are in **[docs/UNSLOTH_Q4.md](UNSLOTH_Q4.md)**. On a 64 GB PC with a 12 GB RTX 5070 it writes 7-8.5
|
||||
tokens/s, most experts read from the SSD; quality has not been measured against llama.cpp yet.
|
||||
A 4-bit quantization of the same model (111 GB, 72 GiB of experts). Setup offers it from engine 0.1.32
|
||||
(`--family unsloth --model UD-Q4_K_XL`: a RAM budget of your RAM less 24 GB, the rest read from the SSD); what it
|
||||
does and the manual workflow are in **[docs/UNSLOTH_Q4.md](UNSLOTH_Q4.md)**. On a 64 GB PC with a 12 GB RTX 5070 it writes 7-8.5
|
||||
tokens/s, most experts read from the SSD; it picks the same tokens as llama.cpp on the same file at 97.5-99% of
|
||||
the positions of short greedy answers, 90-91% after a 16K prompt, differing mostly at near-ties
|
||||
([measured](UNSLOTH_Q4.md#quality-against-llamacpp-on-the-same-file)).
|
||||
|
||||
## Before you start
|
||||
|
||||
You need **only an NVIDIA driver** (version 580 or newer; update it with the NVIDIA App or from
|
||||
[nvidia.com/drivers](https://www.nvidia.com/drivers)). Everything else is installed for you the first time.
|
||||
You need **only a graphics driver**: NVIDIA 580 or newer (update it with the NVIDIA App or from
|
||||
[nvidia.com/drivers](https://www.nvidia.com/drivers)), or for AMD the one in [INSTALL.md](INSTALL.md#what-you-need).
|
||||
Everything else is installed for you the first time.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| GPU | NVIDIA **RTX 20, 30, 40 or 50 series**, **12 GB VRAM or more** (8 GB runs, slowly). Measured on an RTX 5070 and an RTX 3090; RTX 20 (Turing, since 0.1.27) was tested by a contributor on an RTX 2070. |
|
||||
| GPU | NVIDIA **RTX 20, 30, 40 or 50 series**, **12 GB VRAM or more** (8 GB runs, slowly). Measured on an RTX 5070 and an RTX 3090; RTX 20 (Turing, since 0.1.27) was tested by a contributor on an RTX 2070. Or AMD **Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700, RX 6800 / 6900 series**: [AMD_HIP.md](AMD_HIP.md). |
|
||||
| RAM | **64 GB** recommended (see the table above). |
|
||||
| CPU | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. |
|
||||
| Disk | ~70-80 GB free for the model, ~6 GB for the MTP layer (+1 GB with images). **Q2_0 on an AVX-512 CPU** also writes a one-time ~40 GB copy of its experts for the fast CPU kernel. An NVMe SSD is strongly recommended. |
|
||||
@@ -373,6 +413,17 @@ the OS file cache, so loading again takes seconds while that RAM is not needed e
|
||||
16 GB with Q2_0 in the low-RAM mode: unloading takes ~0.3 s, and a request to an unloaded model answered after
|
||||
4.6 s (text) or 14.7 s (a picture, image encoder on the CPU).
|
||||
|
||||
**Keep what the expert cache learned across restarts (opt-in, engine 0.1.36, #477):** a start fills the GPU's expert
|
||||
cache from the shipped profile, and the adaptive tier (`--adapt-every`) then moves in the experts your requests use.
|
||||
With `"expert_profile_save": "expert-profile-learned.bin"` in `strata-<model>.json` the engine saves that as a
|
||||
profile - the experts in VRAM first, then the routing it counted since the start, then the shipped order - on a
|
||||
clean exit and every 10 minutes between requests (`"expert_profile_save_every": 5` for another interval, `0` for
|
||||
exit only), written to a temporary file and renamed, so a crash never leaves half a file. The next start begins from
|
||||
it instead of the config's `--expert-profile` when it is a profile of the same model (else from the config's, as
|
||||
before). A relative path is in the Strata folder; one file per model, and a profile per project works the same way
|
||||
(point the key at another file). The file is a fingerprint of what you used the model for: it stays on your PC.
|
||||
Without the key nothing is counted or written. Setup rewrites the config when run again: add the key again then.
|
||||
|
||||
---
|
||||
|
||||
## Using it
|
||||
@@ -421,6 +472,10 @@ print(r.choices[0].message.content)
|
||||
part of the thinking the client sees and counts as output tokens. `"reasoning_budget_tokens": N` in
|
||||
`strata-<model>.json` sets it for every request; a request's own value wins, and `0` means no budget. Off by default;
|
||||
Anthropic's `"thinking": {"budget_tokens": N}` still only chooses the level, as above.
|
||||
- **Anthropic requests that don't ask for thinking (opt-in, 0.1.32, #278).** By default a `/v1/messages` request
|
||||
with no `"thinking"`, effort or budget thinks as the model's template does. `"anthropic_thinking": "on_request"` in
|
||||
`strata-<model>.json` renders such a request without thinking - Anthropic's own rule, and what Claude Code's short
|
||||
helper calls (a session title in a few dozen tokens) need; its real turns ask for thinking when it is on there.
|
||||
- **Streaming.** With `"stream": true` everything arrives as it is made: the thinking, the answer, and tool calls
|
||||
(the tool's name first, then its arguments piece by piece, like OpenAI and Anthropic do). While the model reads a
|
||||
long prompt the stream sends keep-alives, so agents do not time out; the server window prints progress every
|
||||
@@ -434,6 +489,9 @@ print(r.choices[0].message.content)
|
||||
`max_tokens` would run past the context is refused too (400); agents that always ask for their full output cap
|
||||
can instead get it shortened to the room left: add `"fit_max_tokens": true` to `strata-<model>.json` (or pass
|
||||
`--fit-max-tokens` to `serve/server.py`). A prompt that leaves no room at all is still refused.
|
||||
- **Model aliases** (0.1.32). `"aliases": ["qwen", "local-model"]` in `strata-<model>.json` lists the model under
|
||||
those names too in `/v1/models` (each with its own `id`, and in the model's `aliases`), like llama-server's
|
||||
`--alias`; a request naming one is answered under that name. Any other name is still served, as before.
|
||||
- **From other devices on your network.** The server listens on your PC only (`127.0.0.1`) unless you say otherwise:
|
||||
run setup with `START-HERE.bat --setup --host 0.0.0.0 --api-key some-long-secret` (or add `"host": "0.0.0.0"` and
|
||||
`"api_key": "..."` to `strata-<model>.json`). The server window then prints this PC's addresses
|
||||
@@ -444,7 +502,12 @@ print(r.choices[0].message.content)
|
||||
- **From the internet.** Put a tunnel in front of it, for example [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/):
|
||||
`cloudflared tunnel --url http://127.0.0.1:8080`. **Set a key first**, or anyone with the link can use your PC:
|
||||
add `"api_key": "some-long-secret"` to `strata-<model>.json` (or set the `STRATA_API_KEY` environment variable);
|
||||
clients then send it as their API key.
|
||||
clients then send it as their API key. Streamed answers carry `X-Accel-Buffering: no`, so nginx-style proxies pass
|
||||
each token on at once. The web app's settings and MCP tools only answer Strata's own page: when you open it through
|
||||
a proxy or tunnel whose address differs, add that address, e.g. `"trusted_origins": ["https://strata.example.com"]`.
|
||||
- **From web apps in a browser (CORS).** Off by default. `"cors_origins": ["https://chat.example.com"]` lets pages of
|
||||
those origins call `/v1/*` from the browser (Open WebUI's direct connections, browser extensions); `["*"]` lets any
|
||||
page do it - only sensible with an API key. It never opens `/settings`, `/unload` or the MCP tools.
|
||||
|
||||
**Conversation cache.** A request that continues a chat reads only the part after what the engine already holds: the
|
||||
live session, or one of the checkpoints it keeps in RAM (up to 6, ~118 MB each, taken at the start of each new
|
||||
@@ -591,6 +654,29 @@ unmeasured: all the runs above are text.
|
||||
|
||||
---
|
||||
|
||||
## Manage Strata from your AI assistant (MCP server)
|
||||
|
||||
`tools/strata_mcp.py` is an MCP server for Claude Code, Claude Desktop, Cursor, VS Code, Codex and other assistants.
|
||||
Once it is added, you can ask your assistant "install Strata for this PC", "start Strata" or "is Strata running?".
|
||||
In Claude Code, add it with:
|
||||
|
||||
```bash
|
||||
claude mcp add strata -- python C:\Users\you\Strata\tools\strata_mcp.py
|
||||
```
|
||||
|
||||
It has eight tools: status (the running model, what is installed, the hardware, a recommended size), the model
|
||||
list, install, start, stop, logs, a speed test, and connection settings for other apps.
|
||||
|
||||
Install runs `setup.py` with `--yes` in the background. Before it downloads anything, it shows the plan and waits
|
||||
for your OK. Start and stop work like the run scripts and the server's own unload. The MCP server only ends
|
||||
processes it started itself. It uses only Python's standard library, so it works before `.venv` exists.
|
||||
|
||||
The config snippets for every client, the tool arguments and the safety rules are in
|
||||
[docs/MCP_SERVER.md](MCP_SERVER.md). This is the opposite direction from
|
||||
[Tools from MCP servers](#tools-from-mcp-servers) above, where the Strata model calls *your* MCP tools.
|
||||
|
||||
---
|
||||
|
||||
## Images (vision)
|
||||
|
||||
The model has a vision encoder: [`mmproj-Qwen3.8-Flash-Next-BF16.gguf`](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)
|
||||
@@ -606,6 +692,11 @@ helper (`strata-vision`, from llama.cpp's `mtmd` library) and adds it to your st
|
||||
A picture becomes up to 1,024 tokens of the context (a 640x480 photo: 300). The same picture sent again, as chat apps
|
||||
do on every turn, is encoded only once.
|
||||
|
||||
**A spare GPU for the encoder (0.1.33, #408):** with a card the engine doesn't use, add `"cuda_device": 2` (numbered
|
||||
like `nvidia-smi`) to the `"vision"` section of `strata-<model>.json`: the encoder then runs on that card alone. Lower
|
||||
`--vram-reserve-mib` in `"args"` to 700 as well, so the engine's cards keep that VRAM for the expert cache. The
|
||||
encoder's card needs code in the ready-made encoder (RTX 20/30/40/50).
|
||||
|
||||
### Sending a picture
|
||||
|
||||
**Terminal chat:** type `/image <path to a picture>`, press Enter, then type your question.
|
||||
@@ -770,3 +861,56 @@ Strata itself: [MIT](../LICENSE). The model files are not part of it; their lice
|
||||
`serve/web/fonts/OFL.txt`). Its Monitor tab started from @code-martin's dashboard idea (PR #22).
|
||||
- The experimental speed projection's vector (`data/experimental-speed-projection/`): Qwen Community License 1.0,
|
||||
made from the model's activations (see its README).
|
||||
|
||||
### Start the text API without occupying the GPU
|
||||
|
||||
`serve/server.py --engine strata --config strata-<model>.json --lazy` (or `"lazy_load": true` in that
|
||||
config) starts the lightweight HTTP API without spawning the native engine. The first generation request
|
||||
loads it through the existing reload path, including `before_load` and `min_free_vram_mib`. Eager startup
|
||||
remains the default. This option is text-only: a vision configuration with lazy startup is rejected explicitly.
|
||||
|
||||
`POST /v1/load` and `/v1/unload` are JSON control aliases for integrations, accepting `{}` or
|
||||
`{"model":"<configured model>"}` and returning model status. They require the configured API key,
|
||||
`application/json`, and no foreign browser Origin. They return **409** while a request is active or queued,
|
||||
and **404** for an unknown model. Existing `/load` and `/unload` behavior is preserved. `/api/health` aliases
|
||||
`/health`; `/v1/status` exposes `loaded` and `auto_load`. The unloaded model remains discoverable.
|
||||
|
||||
Unloading and shutdown close the native engine's stdin after sending `QUIT`, allowing Windows' detached
|
||||
stdin reader to see EOF. Cleanup waits for process exit before releasing handles; if forced shutdown still
|
||||
times out, the server keeps ownership and reports an error rather than claiming the model was unloaded.
|
||||
|
||||
### JSON response formats
|
||||
|
||||
`POST /v1/chat/completions` accepts `response_format: {"type":"json_object"}` or
|
||||
`{"type":"json_schema","json_schema":{"name":"answer","strict":true,"schema":{"type":"object","properties":{"answer":{"type":"integer"}},"required":["answer"],"additionalProperties":false}}}`.
|
||||
The schema must describe an object at its root. Local `#` references work; remote references are refused.
|
||||
`json_schema` is checked with the Python package `jsonschema` when it is installed (`python -m pip install
|
||||
"jsonschema>=4.23,<5"`; setup does not add it); without it the answer is only checked to be one JSON object, and the
|
||||
server says so once.
|
||||
|
||||
This is **schema prompting followed by server validation**, not grammar-constrained decoding. One generation
|
||||
is made per request, with no hidden retry. Successful responses contain a validated JSON object. Malformed JSON,
|
||||
duplicate keys, non-finite numbers, schema violations and incomplete generations return **502** with
|
||||
`error.code: structured_output_failed`; invalid request schemas return **400**. JSON formats combined with
|
||||
tools/MCP are refused explicitly. Without `response_format`, ordinary text and tool behavior stays the same.
|
||||
|
||||
Structured SSE buffers the answer while sending keep-alive comments. It emits content only after validation,
|
||||
then usage/timings and `[DONE]`; failures emit an SSE error and `[DONE]` without invalid content deltas.
|
||||
`/v1/status.structured_output` advertises the formats, validation method and buffered streaming behavior.
|
||||
|
||||
### API request monitor
|
||||
|
||||
Off by default, since it keeps prompts and answers in memory: turn it on with `"api_monitor": true` in
|
||||
`strata-<model>.json` (or `serve/server.py --api-monitor`); otherwise nothing is recorded and the two endpoints below
|
||||
answer 404.
|
||||
Open `/api-monitor` to inspect API traffic without opening a chat. It shows the model state, safe
|
||||
load/unload controls, active/queued requests, original request bodies, output, separate reasoning and
|
||||
non-stream response bodies. Total wall-clock includes FIFO waits and automatic loading; load, queue,
|
||||
first-token, prompt/output tokens and engine decode timing are shown separately.
|
||||
|
||||
`GET /api/requests` returns compact summaries; `GET /api/requests?id=<id>` returns one retained request.
|
||||
Both use the existing API-key check. The monitor retains the newest **100 requests in memory** until restart,
|
||||
with **262,144 characters per input/output/reasoning/response field** and visible truncation flags. The actual API
|
||||
responses are unaffected. Headers are not recorded, and the monitor key is kept in this tab's session storage.
|
||||
Treat request history as sensitive input/output when exposing Strata on a network: set an API key as above.
|
||||
The page uses relative URLs and works through the existing host binding or a reverse proxy.
|
||||
@@ -0,0 +1,77 @@
|
||||
# How does Strata work?
|
||||
|
||||
Models like Qwen3.8-Flash-Next normally run on servers with hundreds of gigabytes of graphics memory. Your graphics
|
||||
card has 12-24 GB. Strata makes it fit by **sharing the work across your whole PC** - the same idea as a kitchen, where
|
||||
the things you use all the time stay on the counter and the rest waits in the pantry. Back to the
|
||||
[README](../README.md#how-does-it-work).
|
||||
|
||||
## Who does what
|
||||
|
||||
<p align="center"><img src="media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
|
||||
|
||||
- **The model is a team of 24,576 small specialists ("experts"),** and each word it writes needs only 10 of them.
|
||||
So it doesn't have to have all of them on the graphics card at once.
|
||||
- **Your graphics card** does the part of the work needed for every word, and keeps the few thousand experts that
|
||||
are asked most often. It keeps learning which ones those are while you use it.
|
||||
- **Your RAM** holds every expert. When a word needs one the card doesn't have, **your processor** works on it -
|
||||
at the same time as the graphics card, so neither waits for the other.
|
||||
- **Your SSD** holds a big lookup table; the model only reads a few small rows of it per word.
|
||||
|
||||
The same in technical terms:
|
||||
|
||||
- **GPU (VRAM):** attention and DeltaNet mixers, the gated-residual weights, routers, shared experts, output head, the
|
||||
MTP draft layer, the KV cache (from 64K: only its most-read part, the rest streams from RAM), and an **expert
|
||||
cache** that fills the rest of VRAM with the most-used experts (it adapts to the conversation while you chat).
|
||||
More VRAM matters more than a faster GPU: every extra GB holds ~700 more experts, and every expert on the GPU is one
|
||||
the CPU does not have to compute.
|
||||
- **RAM:** all 24,576 experts, pinned. The CPU computes the experts that are not on the GPU **in place**, at the same
|
||||
time as the GPU works on the cached ones (AVX-512 / AVX2 kernels, ggml's for the i-quants). On a PC with little RAM
|
||||
and a big card, the [low-RAM mode](MODELS.md#a-big-graphics-card-and-little-ram) keeps in RAM only what the GPU does
|
||||
not hold.
|
||||
- **SSD:** the 28.8 GB n-gram table, read a few rows per token through the OS cache.
|
||||
|
||||
This works the same on NVIDIA (CUDA) and AMD (HIP) cards: the same engine, compiled for each.
|
||||
|
||||
## Guess, then check
|
||||
|
||||
<p align="center"><img src="media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
|
||||
|
||||
- **Guess, then check.** A small, fast helper built into the model guesses the next few words, and the big model
|
||||
checks all the guesses in one go. It keeps the ones it agrees with and writes the next word itself - so one step
|
||||
often produces several words. The helper only guesses - the big model decides every word - so you get the same
|
||||
quality answer, 1.6-1.8x sooner.
|
||||
- In technical terms: the model's own MTP layer drafts up to 3 tokens; one pass over all 48 layers checks them, 2.4-3.2
|
||||
tokens per pass on average. When the reply repeats the context (code edits, quoted text), **prompt lookup** drafts
|
||||
up to 5 tokens from the earlier copy, but only where its measured acceptance and cost say it pays: code edits 6-11%
|
||||
faster, other text unchanged. The drafts are checked like the MTP's, so the output is the same.
|
||||
|
||||
## Reading long texts
|
||||
|
||||
**Long texts are read in big pieces** (up to 8,192 tokens - pieces of words - at a time), which is why a long
|
||||
document or code base is read at over 1,000 tokens per second. The experts of the next layer stream to the GPU over
|
||||
PCIe while the current layer's attention runs. After the first message, Strata keeps the conversation and reads only
|
||||
what is new, so follow-ups start in seconds.
|
||||
|
||||
## More
|
||||
|
||||
The [details](DETAILS.md#how-it-works) explain every part and its numbers, and the
|
||||
[paper](paper/Strata-Paper.pdf) tells the whole story, with the measurements behind it.
|
||||
|
||||
## Credits
|
||||
|
||||
- Model: [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team; compressed versions by
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF);
|
||||
[Swift 1.5](https://huggingface.co/ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF) by UkisAI; the experimental
|
||||
[UD-Q4_K_XL](https://huggingface.co/unsloth/Qwen3.8-Flash-Next-GGUF) by Unsloth (its support follows
|
||||
[eddoursul/Strata](https://github.com/eddoursul/Strata)). Their licenses apply to the model files.
|
||||
- Built with parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) (MIT). Ideas from
|
||||
[Splash](https://github.com/incoai/splash), [ninfer](https://github.com/Neroued/ninfer) and
|
||||
[HyperQwen](https://github.com/syv-ai/HyperQwen). More in the [details](DETAILS.md#credits-and-licenses).
|
||||
- The AMD (HIP) backend: [AMD_HIP.md](AMD_HIP.md) names the contributors and the machines it was validated on.
|
||||
|
||||
## License
|
||||
|
||||
Strata is open source under the [MIT License](../LICENSE). A few parts carry their own licenses: `third_party/ggml`
|
||||
(MIT, llama.cpp / ggml), the web app's font (SIL Open Font License 1.1) and the experimental speed projection's
|
||||
vector in `data/experimental-speed-projection` (Qwen Community License 1.0, from the model's activations). The
|
||||
models are not part of this repository; each model's own license applies to its files.
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
# Installing Strata
|
||||
|
||||
Everything about installing, starting, updating and configuring Strata, on Windows and Linux, with an NVIDIA or an
|
||||
AMD graphics card. The short version is in the [README](../README.md#install); an AI coding assistant can do all of
|
||||
this for you with [AI_SETUP.md](AI_SETUP.md).
|
||||
|
||||
> **On this page:** [What you need](#what-you-need) · [Windows](#windows) · [Linux](#linux) ·
|
||||
> [AMD cards](#amd-cards) · [Several cards](#two-or-three-cards) · [Docker](#docker-linux) · [Updating](#updating) ·
|
||||
> [Where things are stored](#where-things-are-stored) · [Setup's questions](#setups-questions) ·
|
||||
> [Tuning](#tuning-for-your-pc) · [All options](#options-without-questions)
|
||||
|
||||
## What you need
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **GPU** | **NVIDIA** RTX 20, 30, 40 or 50 series, **12 GB VRAM or more** (8 GB runs, slowly). Measured on an RTX 5070 and an RTX 3090; RTX 20 (Turing, since 0.1.27) was tested by a contributor on an RTX 2070. **AMD** Radeon RX 7900 XT / XTX, RX 9070 / 9070 XT and Radeon AI PRO R9700 (validated), RX 7800 XT / 7700 XT and RX 9060 XT (validated by their owners), RX 6800 / 6900 series (community-reported), with 12 GB of VRAM or more. See [AMD cards](#amd-cards). |
|
||||
| **RAM** | Enough for the size you pick ([which model](MODELS.md#pick-by-ram)); **64 GB** runs every size. A big GPU makes up for less RAM - the [low-RAM mode](MODELS.md#a-big-graphics-card-and-little-ram). |
|
||||
| **CPU** | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. |
|
||||
| **Disk** | ~70-80 GB free for the model, ~6 GB for the MTP layer (+1 GB with images). **Q2_0 on an AVX-512 CPU** also writes a one-time ~40 GB copy of its experts for the fast CPU kernel. On Linux with an AMD card, ROCm takes ~10 GB more when setup installs it. An NVMe SSD is strongly recommended: it makes the first start much faster. |
|
||||
| **OS** | Windows 10/11, or Linux (Ubuntu 22.04/24.04 get everything installed automatically). |
|
||||
| **Driver** | **NVIDIA:** a current driver, version 580 or newer ([nvidia.com/drivers](https://www.nvidia.com/drivers) or the NVIDIA App). **AMD:** on Linux the kernel's amdgpu driver (no ROCm install needed); on Windows a current AMD Software: Adrenalin Edition driver ([amd.com/support](https://www.amd.com/en/support)). |
|
||||
|
||||
The driver is the only thing you install yourself. Everything else - Python, the engine, the model - is set up for
|
||||
you the first time: Python 3.12 if you have none (for your user account, no admin), a private Python environment in
|
||||
`.venv/`, the Strata engine, the model and the MTP draft layer. On NVIDIA it uses the ready-made engine for RTX
|
||||
20/30/40/50 and NVIDIA's CUDA libraries from pip (~0.4 GB); if no ready-made engine fits your PC, it offers to install
|
||||
the build tools (Visual Studio Build Tools + CUDA Toolkit on Windows, `build-essential` + CUDA on Ubuntu) and compiles
|
||||
the engine for your GPU (asks first; 20-40 minutes once). More: [details](DETAILS.md#before-you-start).
|
||||
|
||||
## Windows
|
||||
|
||||
1. [Download this project](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it (or
|
||||
`git clone` it).
|
||||
2. Double-click **`START-HERE.bat`**.
|
||||
3. Answer a few questions - or just press Enter each time for the recommended choice ([the questions](#setups-questions)).
|
||||
|
||||
Then it downloads everything (the model is ~70 GB, so the first time takes a while - you can stop and it picks up
|
||||
where it left off) and **starts the model**. Your browser opens the Strata app at `http://127.0.0.1:8080`.
|
||||
|
||||
> **While the model starts, your PC can be slow or stop responding for 1-3 minutes** (longest the first time): Strata
|
||||
> loads 35-55 GB into your RAM and locks part of it for the graphics card. That's normal - wait, and don't close the
|
||||
> window. The window tells you what it is doing.
|
||||
|
||||
**Next time**, just double-click `START-HERE.bat` again: it starts right away (30-90 s to load the model), nothing is
|
||||
downloaded twice. Close its window to stop the model. `SETUP.bat` (the same as `START-HERE.bat --setup`) installs
|
||||
another model or changes the settings. Starting Strata from Task Scheduler at logon needs two task settings, or the
|
||||
start is 24x slower: [Running it at startup](DETAILS.md#running-it-at-startup-task-scheduler).
|
||||
|
||||
## Linux
|
||||
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
The same questions, the same automatic install (it uses `sudo apt` for Python and, only if it has to compile, for
|
||||
the build tools), and the same start: `http://127.0.0.1:8080`. Later runs of `./setup.sh` (or `./run-<model>.sh`)
|
||||
start the model directly; `./setup.sh --setup` installs another model or changes the settings. Other distributions,
|
||||
WSL and compiling: [details](DETAILS.md#linux).
|
||||
|
||||
## AMD cards
|
||||
|
||||
The steps are the same as with NVIDIA: `START-HERE.bat` on Windows, `./setup.sh` on Linux. Setup finds the Radeon
|
||||
card and chooses the AMD (HIP) engine by itself on a PC with no NVIDIA card Strata can use; `--backend hip` chooses
|
||||
it on a PC that has both. Supported cards: RX 7900 XT / XTX (gfx1100), RX 7800 XT / 7700 XT (gfx1101), RX 9060 XT
|
||||
(gfx1200), RX 9070 / 9070 XT and Radeon AI PRO R9700 (gfx1201), and the RX 6800 / 6900 series (gfx1030). Integrated
|
||||
Radeon GPUs are listed as not supported.
|
||||
|
||||
On Linux setup uses a system ROCm 7 when there is one, or installs ROCm into `.venv` from AMD's wheels (~10 GB, no
|
||||
sudo), and compiles the engine on your PC for the card (10-20 minutes, once; it needs a C++ compiler and git:
|
||||
`sudo apt install build-essential git`). Several AMD cards share the model with `--gpus`, as on NVIDIA.
|
||||
|
||||
What differs from NVIDIA for now: pictures are read by the image encoder on the CPU (`--vision cpu`, 10-30 s per
|
||||
picture), `--calibrate` is NVIDIA-only, and Unsloth's 4-bit model needs an NVIDIA card. Measurements per card, the
|
||||
build by hand and the tuning tables: [AMD_HIP.md](AMD_HIP.md).
|
||||
|
||||
## Two or three cards
|
||||
|
||||
**Two or three NVIDIA cards?** Just run `START-HERE.bat`: it lists your cards, says which ones Strata can use, and
|
||||
asks whether to share the model across them (recommended when two can). An install made on one card asks once at
|
||||
its next start. Or choose yourself: `START-HERE.bat --gpus 0,2` (both, remembered), `--gpus all`, or `--gpu 0` (one
|
||||
card, this start only). Each card keeps the experts of its own layers, and prompts flow through the cards in a
|
||||
pipeline: on an RTX 5080 + RTX 3090 prompts were read 18-20% faster than on the 5080 alone, decoding on par.
|
||||
Every card must be an RTX 20 series or newer with 8 GB or more. See [MULTI_GPU.md](MULTI_GPU.md).
|
||||
|
||||
**Several AMD cards:** setup takes one card (the one with the most VRAM, or `--gpu N`) unless you name more:
|
||||
`--gpus 1,0` splits the model's layers across them, the first one the main card (numbers as setup lists them). A
|
||||
split pays only when no single card holds the model's experts ([AMD_HIP.md](AMD_HIP.md#rdna4-gfx1201)).
|
||||
|
||||
## Docker (Linux)
|
||||
|
||||
The same idea, in a container (NVIDIA cards).
|
||||
|
||||
1. Host: Docker with the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
|
||||
and a driver **580 or newer** (CUDA 13.0).
|
||||
2. Build (this compiles the engine into the image, so the container never compiles):
|
||||
`docker build -t strata .`
|
||||
`docker build -t strata --build-arg CUDA_ARCHITECTURES=89 .` builds for one card only (faster).
|
||||
The default covers RTX 30 (86), RTX 40 (89), RTX 50 (120) and A-series (80); a card outside that
|
||||
set needs a rebuild with its own arch. Add `--build-arg BUILD_VISION=0` to skip the image encoder.
|
||||
3. Run (the first start downloads the ~70 GB model, then starts; later starts go straight to serving):
|
||||
`docker run --rm --gpus all -p 8080:8080 --ulimit memlock=-1 -v strata-data:/data strata`
|
||||
|
||||
The setup choices are env vars: `-e MODEL=IQ2_XS -e FAMILY=qwen -e CONTEXT=32768 -e VISION=no`
|
||||
(or `MODEL=Q2_0|IQ3_XXS|IQ3_S`, `FAMILY=swift|coder`; the defaults above are the recommended ones).
|
||||
`-e VISION=cpu` keeps the image encoder on the CPU. `-e KV=int8|q4_0|k8v4` picks the KV cache
|
||||
precision; `k8v4` is INT8 K with 4-bit V and keeps its KV in VRAM from 64K up.
|
||||
Only the model files, the prepared pack, the MTP layer and the install config live in the
|
||||
`strata-data` volume; the engine is part of the image. Switching between models already on the
|
||||
volume needs no setup pass: `-e MODEL=Q2_0 -e FAMILY=coder` picks that model's config. Add
|
||||
`-e REINSTALL=1` only to change settings for a model already set up (context, vision, KV, host,
|
||||
api_key, LOW_RAM), since those are recorded in its config.
|
||||
Strata loads 32-62 GB into RAM. `--gpus all` on a host with two usable cards takes both: the
|
||||
layer split is setup's recommended default ([MULTI_GPU.md](MULTI_GPU.md)), and a volume
|
||||
set up for one card switches to the pair on its first start there. Pin one card with `-e GPU=0`,
|
||||
or name them with `-e GPUS=0,2` and where the later card's layers start with `-e LAYER_SPLIT=18`.
|
||||
A memory limit needs `-e LOW_RAM=on`, which maps the model's experts from the pack instead of
|
||||
keeping them in RAM: setup.py measures the host's RAM, not the container's limit, so it cannot
|
||||
see a cap. LOW_RAM runs on one card unless `GPUS` names several (then the experts the cards do not hold are
|
||||
read through the OS file cache, which can fill the RAM during long prompts).
|
||||
The server listens on `0.0.0.0:8080` by default; set `-e API_KEY=<secret>` before exposing the port
|
||||
to a network. The image has a `HEALTHCHECK` on `/health`, so `docker ps` shows the container
|
||||
healthy once the model is loaded, and `GET /v1/status` says what it is running.
|
||||
|
||||
## Updating
|
||||
|
||||
**`UPDATE.bat`** (Linux: `./update.sh`) updates Strata without starting the model - for when the GPU is busy with
|
||||
something else, or you just want the new version ready. In a `git clone` it runs `git pull`, then does what
|
||||
`START-HERE.bat` does before a start: the engine (a new ready-made one when the new version needs it; on Linux a
|
||||
compiled engine is compiled again when its source changed), the Python packages, and each installed model's settings
|
||||
and draft subset. The model files are not touched (at most a new engine is downloaded) and no question is asked.
|
||||
Close the model's window first (a running engine cannot be replaced); start the model later with `START-HERE.bat` as
|
||||
usual. In a copy that was downloaded as a zip it says to download the new zip (below): it cannot fetch new files
|
||||
itself.
|
||||
|
||||
Or by hand: download the new version and unzip it anywhere (or `git pull`), then run `START-HERE.bat` (Linux:
|
||||
`./setup.sh`) in it. The model files are kept in a `Strata-data` folder next to your Strata folder, so a new copy
|
||||
finds them and sets itself up the same way - nothing big is downloaded again. On Linux after a `git pull`, setup
|
||||
compiles the engine again when its source changed (a minute or two for the changed files); if that compile fails, it
|
||||
says so and starts the engine you had.
|
||||
|
||||
## Where things are stored
|
||||
|
||||
- **Your chats: only in your browser.** The Chat tab keeps the conversation, its settings and the API key you typed
|
||||
in the browser's local storage (`strata.*` keys) - not on the server and not in the Strata folder. Pictures are not
|
||||
kept, only their names. Another browser or a private window starts empty; clearing the site's data deletes them.
|
||||
- **How the model starts:** `strata-<model>.json` in the Strata folder (context, GPUs, host, API key, ...), written
|
||||
by setup; next to it `run-<model>.bat` / `.sh`, the log `strata-<model>.log` and, when you use "Use for other
|
||||
apps too", `strata-<model>.shared-settings.json`.
|
||||
- **The model files** (`models/`, `packs/`, `mtp/`, 70-120 GB): in **`Strata-data` next to the Strata folder**, or
|
||||
wherever `--data-dir` put them.
|
||||
- **Where that data folder is:** `%APPDATA%\Strata\settings.json` on Windows, `~/.config/strata/settings.json` on
|
||||
Linux ([details](DETAILS.md#before-you-start)).
|
||||
- **The program itself:** `.venv/`, `engine/` and `third_party/` in the Strata folder.
|
||||
|
||||
## Setup's questions
|
||||
|
||||
- **Which model and size?** The original, Swift 1.5, the Coder or Unsloth's 4-bit, and Q2_0, IQ2_XS, IQ3_XXS or
|
||||
IQ3_S - see [which model](MODELS.md).
|
||||
- **How much context?** How much text it can keep in mind at once (it suggests one for your card). 384K and
|
||||
512K (experimental) extend the model past its trained 262K by rope scaling - the setup turns it on itself (yarn and a
|
||||
covering factor; `--rope-scaling`/`--rope-scale` override) ([details](DETAILS.md): "Context extension past
|
||||
262K").
|
||||
- **Images?** Whether it should also read pictures. The image encoder (0.9 GB) runs on the GPU (0.1-0.5 s per
|
||||
picture, ~1.4 GB of VRAM kept free for it) or on the CPU (10-30 s per picture, nothing on the GPU); with AMD cards
|
||||
on the CPU for now. [Details](DETAILS.md#images-vision).
|
||||
- **Experimental speed projection?** Off unless you say yes - [read what it does](DETAILS.md#experimental-speed-projection-experimental-off-by-default)
|
||||
first. It changes how the model answers, and only the original model offers it.
|
||||
|
||||
With `--yes` setup takes the recommended answer to every question.
|
||||
|
||||
## Tuning for your PC
|
||||
|
||||
Every PC is different: `START-HERE.bat --calibrate` (Linux: `./setup.sh --calibrate`) measures a few engine settings
|
||||
on yours and keeps the fastest (about 5-10 minutes; on an RTX 5070 with a Ryzen 5 7600 it made the Coder 7% faster).
|
||||
It keeps a setting only when it is more than 3% faster, and the result is remembered per PC and model, so updates
|
||||
keep it. NVIDIA cards for now. [What it measures](DETAILS.md#double-click-start-herebat).
|
||||
|
||||
## Options without questions
|
||||
|
||||
```
|
||||
START-HERE.bat --setup install another model, or change context / images
|
||||
SETUP.bat the same (double-click it)
|
||||
START-HERE.bat --model IQ2_XS --context 32768 --vision yes --yes no questions
|
||||
START-HERE.bat --gguf-dir D:\models\IQ2_XS use GGUF files you already have
|
||||
START-HERE.bat --data-dir E:\Strata-data keep the model files somewhere else
|
||||
START-HERE.bat --port 8081 another port
|
||||
START-HERE.bat --gpu 1 another GPU (setup picks the one with the most VRAM)
|
||||
START-HERE.bat --gpus 0,2 several GPUs sharing the model
|
||||
START-HERE.bat --setup --backend hip the AMD engine on a PC that also has an NVIDIA card
|
||||
START-HERE.bat --setup --host 0.0.0.0 --api-key <secret> reachable from other devices, with a key
|
||||
START-HERE.bat --calibrate tune the engine for this PC (about 5-10 minutes), then start
|
||||
START-HERE.bat --check only check this PC
|
||||
```
|
||||
|
||||
On Linux the same options go to `./setup.sh`. `START-HERE.bat --help` lists them all. The server's own settings
|
||||
(sharing the GPU with games, MCP tools, CORS, API keys, the API itself) are in the [details](DETAILS.md#using-it).
|
||||
@@ -0,0 +1,205 @@
|
||||
# Strata's MCP server: let your AI assistant install and run Strata
|
||||
|
||||
`tools/strata_mcp.py` is an [MCP](https://modelcontextprotocol.io) server for the AI assistant you already use
|
||||
(Claude Code, Claude Desktop, Cursor, VS Code, Codex ...). Add it once, then ask in plain words:
|
||||
|
||||
- "Install Strata for this PC."
|
||||
- "Start Strata." / "Stop Strata."
|
||||
- "Is Strata running? How much VRAM does it use?"
|
||||
- "Which Strata model fits my PC?"
|
||||
- "Strata didn't start - what does the log say?"
|
||||
- "How fast is Strata on this PC?" (a short speed test)
|
||||
- "How do I connect Open WebUI / the OpenAI SDK / Claude Code to Strata?"
|
||||
|
||||
The assistant calls the server's tools. The tools run Strata's own setup and start the model the way the start
|
||||
scripts do. It is one Python file that uses only the standard library, so it works before setup has created
|
||||
`.venv`. It needs Python 3.10 or newer, 64-bit on Windows.
|
||||
|
||||
It is not the same thing as [Tools from MCP servers](DETAILS.md#tools-from-mcp-servers). That feature
|
||||
(`serve/mcp.py`) lets the *Strata model* call tools from your MCP servers in its chat page. This page is the other
|
||||
direction: *your assistant* manages Strata.
|
||||
|
||||
## Add it
|
||||
|
||||
You need a copy of Strata: the zip from GitHub, or `git clone https://github.com/Niko1221/Strata`. Below,
|
||||
`C:\Users\you\Strata` (Windows) and `/home/you/Strata` (Linux) stand for that folder. Use `python3` on Linux. On
|
||||
Windows, use `py` if `python` is not on your PATH.
|
||||
|
||||
### Claude Code
|
||||
|
||||
```bash
|
||||
claude mcp add strata -- python C:\Users\you\Strata\tools\strata_mcp.py # Windows
|
||||
claude mcp add strata -- python3 /home/you/Strata/tools/strata_mcp.py # Linux
|
||||
```
|
||||
|
||||
Add `--scope user` to have it in every project. `/mcp` in Claude Code shows that it is connected.
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
Open Settings > Developer > Edit Config (`claude_desktop_config.json`) and add:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"strata": {
|
||||
"command": "python",
|
||||
"args": ["C:\\Users\\you\\Strata\\tools\\strata_mcp.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
On Linux, use `"command": "python3"` and `"args": ["/home/you/Strata/tools/strata_mcp.py"]`. Restart Claude
|
||||
Desktop after the change.
|
||||
|
||||
### Cursor
|
||||
|
||||
Use `~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` in one project. The format is the same as
|
||||
Claude Desktop's:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"strata": {
|
||||
"command": "python",
|
||||
"args": ["C:\\Users\\you\\Strata\\tools\\strata_mcp.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### VS Code (GitHub Copilot agent mode)
|
||||
|
||||
Use `.vscode/mcp.json` in a workspace, or run "MCP: Open User Configuration" from the command palette:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"strata": {
|
||||
"type": "stdio",
|
||||
"command": "python",
|
||||
"args": ["C:\\Users\\you\\Strata\\tools\\strata_mcp.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
On Linux, use `"command": "python3"` and `"args": ["/home/you/Strata/tools/strata_mcp.py"]`.
|
||||
|
||||
### Codex CLI
|
||||
|
||||
In `~/.codex/config.toml`:
|
||||
|
||||
```toml
|
||||
[mcp_servers.strata]
|
||||
command = "python"
|
||||
args = ['C:\Users\you\Strata\tools\strata_mcp.py'] # Linux: command = "python3", args = ["/home/you/Strata/tools/strata_mcp.py"]
|
||||
tool_timeout_sec = 180 # strata_stop / strata_benchmark can take longer than 60 s
|
||||
```
|
||||
|
||||
### Any other MCP client
|
||||
|
||||
The command is `python <Strata folder>/tools/strata_mcp.py`. The transport is stdio. It speaks MCP revision
|
||||
2025-06-18, and also 2025-03-26 and 2024-11-05.
|
||||
|
||||
`--root <folder>` manages a Strata folder other than the one the file is in. For example:
|
||||
`python D:\tools\strata_mcp.py --root D:\Strata`.
|
||||
|
||||
## The tools
|
||||
|
||||
| Tool | What it does | Arguments (all optional) |
|
||||
| --- | --- | --- |
|
||||
| `strata_status` | Whether a model is running: the model, engine version, context, busy or idle, VRAM and RAM in use. What is installed: models, start scripts, engine version, data folder, downloads. An install in progress. This PC's hardware: OS, CPU, RAM, GPUs, page file, free disk. The model and context setup recommends for this PC. | `port`, `hardware` (true/false) |
|
||||
| `strata_models` | Every family and size setup offers, with download size, RAM needed, whether it fits this PC, and whether it is installed. Also the context lengths and the recommendation. | - |
|
||||
| `strata_install` | Runs setup without questions, in the background (see below). Without `confirm: true` it only returns the plan. A second call shows the progress. `cancel: true` stops the install. | `family` (qwen, swift, coder, unsloth), `model` (Q2_0, IQ2_XS, IQ3_XXS, IQ3_S, IQ1_M, UD-Q4_K_XL), `context` (8192 ... 524288), `vision` (no, yes, gpu, cpu), `kv` (int8, q4_0, k8v4), `gpu` (a number) or `gpus` ("0,2", "all"), `backend` (auto, cuda, hip), `low_ram`, `port`, `data_dir`, `confirm`, `cancel` |
|
||||
| `strata_start` | Starts an installed model in the background and waits until it answers. If it is not ready after `wait_seconds` (default 50), it returns "loading". | `model` (an id from `strata_status`, e.g. `q2_0`, `coder-iq1_m`; default: the most recently used), `port`, `gpu`, `wait_seconds` (0-600) |
|
||||
| `strata_stop` | Unloads the model through the server's own API, then ends the server it started. | `port`, `force` |
|
||||
| `strata_logs` | The last lines of the server log, the engine log or the setup log. | `source` (server, engine, setup), `model`, `lines` (1-500) |
|
||||
| `strata_benchmark` | Sends a short fixed request (greedy, thinking off) and reports output and prompt tokens/s. | `port`, `max_tokens` (16-512) |
|
||||
| `strata_connect_info` | The OpenAI and Anthropic base URLs, the model id, whether a key is needed, and settings for Claude Code, Codex, the OpenAI SDK, curl, Cursor, Continue and Open WebUI. | `port` |
|
||||
|
||||
Every result is JSON with a one-line `summary`. It is sent both as text and as MCP structured content.
|
||||
|
||||
## How it works
|
||||
|
||||
**Install.** `strata_install` uses setup's own logic and tables (it imports `setup.py`'s `MODELS`, `FAMILIES`,
|
||||
`CONTEXTS` and RAM checks) to fill in what you left out:
|
||||
|
||||
- the size setup itself would pick: IQ3_XXS from 60 GB of RAM, Q2_0 from about 44 GB, the Coder from about 28 GB,
|
||||
and the Coder in the low-RAM mode below that when the GPU makes up for the RAM;
|
||||
- the context setup recommends for the GPU's VRAM;
|
||||
- images off.
|
||||
|
||||
The first call returns the plan: the download size (58-111 GB), the free disk space, the RAM check, and the exact
|
||||
setup command. The assistant should show you the plan and call again with `confirm: true` once you agree.
|
||||
|
||||
It then runs the same steps `START-HERE.bat` / `setup.sh` run:
|
||||
|
||||
1. It creates `.venv` with the Python it runs on, if `.venv` is missing.
|
||||
2. It runs `setup.py --yes --no-start --family ... --model ... --context ... --vision ...` as a background process.
|
||||
The output goes to `.strata-mcp/setup.log` in the Strata folder.
|
||||
|
||||
The tool returns at once. Call it again to see the step (1-7), the download percentage, or setup's error message.
|
||||
Everything setup has finished is skipped on the next run, and downloads resume. If the AI app closes, the install
|
||||
keeps running. The install is refused while a model this server started is running, because setup may replace the
|
||||
engine files that model uses.
|
||||
|
||||
**Start.** `strata_start` runs the same command as the `run-<model>` script, `serve/server.py --engine strata
|
||||
--config strata-<model>.json --port N`, with the venv's Python, without `--open` (no browser) and without a console
|
||||
window. The server's output goes to `.strata-mcp/server.log`. The tool then polls `GET /health` until the model
|
||||
answers. Loading takes 1-3 minutes. If the server ends while loading, the tool returns the end of its log. The model
|
||||
keeps running after the AI app closes, until `strata_stop`. If the AI app runs its tools inside a Windows job that
|
||||
does not allow processes to leave it, the model ends together with the app.
|
||||
|
||||
Two cases are refused:
|
||||
|
||||
- a model is already running: the tool returns it and starts nothing;
|
||||
- another program uses the port: the tool says so.
|
||||
|
||||
**Stop.** `strata_stop` first sends `POST /unload`. The server sends the engine `QUIT`, and the engine frees its
|
||||
VRAM and pinned RAM itself, as when you close the window. Then the server ends:
|
||||
|
||||
- Linux: SIGTERM, which runs the server's own Ctrl+C path.
|
||||
- Windows: the process ends. Its job object ends the vision encoder and the MCP servers it started.
|
||||
|
||||
While a request is running, the tool refuses unless you pass `force: true`.
|
||||
|
||||
**Status.** The tool reads `GET /health`, `/v1/status`, `/status` and, on older servers, `/metrics` on 127.0.0.1.
|
||||
If a config of the folder sets an `api_key`, the tool sends that key with its own requests. It never shows the key.
|
||||
|
||||
## Safety
|
||||
|
||||
- **No shell, no free paths.** Every argument is checked against a fixed schema and against setup's own choices. An
|
||||
unknown argument, a model or family setup does not offer, or a `gpus` value other than digits and commas is
|
||||
refused before anything runs. Commands are passed as argument lists; nothing goes through a shell.
|
||||
- **Paths.** The only path argument is `data_dir`. It must be one of:
|
||||
- the current Strata data folder;
|
||||
- a folder inside the Strata folder;
|
||||
- a new absolute folder whose name starts with `Strata` (e.g. `D:\Strata-data`), whose parent exists, and which is
|
||||
not in a system folder.
|
||||
|
||||
`..`, relative paths and network (UNC) paths are refused. Logs are read only from `.strata-mcp/` and from the
|
||||
engine log a config names, if that log is inside the Strata folder or the data folder.
|
||||
- **Only its own processes.** The server records the process id *and start time* of every process it starts, in
|
||||
`.strata-mcp/*.json`. It only ends a process when both match, so a process id the OS has given to another program
|
||||
is never touched. A Strata started some other way (its window, a run script, a service) is only asked over its
|
||||
API to unload the model. To end that server, close its window.
|
||||
- **Big downloads need a yes.** `strata_install` does nothing until it is called with `confirm: true`. The plan
|
||||
shows the download size and the free space first, and the install is refused if the disk is too small.
|
||||
- **Local only.** The server checks `127.0.0.1` and never changes the network settings: it does not install with
|
||||
`--host 0.0.0.0` or an API key. To share Strata on your network, run setup yourself (see
|
||||
[Using it](DETAILS.md#using-it)). Experimental options are left off too: the speed projection, rope scaling past
|
||||
262K and `--build`. Run setup by hand for those.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **The assistant does not see the tools.** Run the command by hand:
|
||||
`python C:\Users\you\Strata\tools\strata_mcp.py`. It should print `[strata-mcp] ready (Strata folder ...)` to
|
||||
stderr and wait for input (Ctrl+C ends it). If it reports `not a Strata folder`, the path is wrong or `--root` is
|
||||
needed. Python older than 3.10 is refused.
|
||||
- **The install failed.** Ask the assistant for the setup log (`strata_logs` with `source: setup`), or open
|
||||
`.strata-mcp/setup.log`. Setup's `[X]` line says what to fix. Then install again: every finished step is skipped.
|
||||
- **The model does not start.** Get the server log with `strata_logs`, and the engine log with `source: engine`.
|
||||
[Troubleshooting](DETAILS.md#troubleshooting) has the usual causes.
|
||||
- **Tests.** `python -m unittest tools/test_strata_mcp.py` runs the tests with a fake setup and server. They do not
|
||||
download anything or use the GPU.
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
# Which model? Sizes, versions and what fits
|
||||
|
||||
Strata runs one model, [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), in several **sizes**
|
||||
(the same model, compressed more or less) and several **versions** (the original, a coding version, a fine-tune).
|
||||
The installer recommends one for your PC; this page explains the choice. Back to the [README](../README.md).
|
||||
|
||||
> **On this page:** [Pick by RAM](#pick-by-ram) · [Speed](#how-fast-is-each-size) · [The sizes](#the-sizes) ·
|
||||
> [Will it fit?](#will-it-fit) · [The versions](#the-versions) · [Adding another model](#adding-or-switching-models)
|
||||
|
||||
## Pick by RAM
|
||||
|
||||
| Your RAM | Take | Why |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Coder** | it fits 32 GB, and it is made for code (with a 24 GB card, Q2_0 and IQ2_XS run too: [low-RAM mode](#a-big-graphics-card-and-little-ram)) |
|
||||
| **48 GB** | **IQ2_XS** (or Q2_0, the fastest) | the larger sizes do not fit |
|
||||
| **64 GB** | **IQ2_XS** (recommended), or IQ3_XXS / IQ3_S | every size fits (IQ3_S with little else open) |
|
||||
| **96 GB or more** | **IQ3_S**, or [Unsloth's 4-bit](#unsloth-ud-q4_k_xl-experimental) (experimental) | room for the largest sizes |
|
||||
|
||||
Not sure? Take **IQ2_XS** - or the **Coder** if you mainly write code, or have 32-48 GB of RAM.
|
||||
|
||||
## How fast is each size
|
||||
|
||||
Measured on an RTX 5070 (12 GB), a Ryzen 5 7600 and 64 GB of RAM:
|
||||
|
||||
| Size | Writes answers (short chat) | Writes answers (128K context) | Reads your prompt |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| **Q2_0** | 94 tokens/s | 76 tokens/s | 2,650 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 63 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 49 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 46 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** (IQ1_M) | 55 tokens/s | 43 tokens/s | 2,180 tokens/s |
|
||||
|
||||
Measured on an AMD RX 9070 XT (16 GB), a Ryzen 9 3900X and 47 GB of RAM (Linux, setup's own install; Q2_0 is
|
||||
above setup's RAM estimate for 47 GB and was installed with `--model Q2_0 --yes`):
|
||||
|
||||
| Size | Writes answers (short chat) | Writes answers (128K context) | Reads your prompt |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| **Q2_0** | 60 tokens/s | 48 tokens/s | 1,160 tokens/s |
|
||||
| **IQ2_XS** | 52 tokens/s | 36 tokens/s | 1,110 tokens/s |
|
||||
| **Coder** (IQ1_M) | 44 tokens/s | 33 tokens/s | 1,420 tokens/s |
|
||||
|
||||
- **Writes answers** = how fast the reply appears (tokens per second; a token is about ¾ of a word).
|
||||
- **Reads your prompt** = how fast it takes in what you send (long documents, code, chat history), measured on a
|
||||
32K-token prompt; a 4K prompt reads at 910-1,580 tokens/s. A 32K prompt takes about 15 seconds with Q2_0.
|
||||
|
||||
A card with more VRAM is faster, because more of the model fits on the GPU: an RTX 3090 (24 GB) should do roughly
|
||||
100-140 tokens per second. All measurements, long-context numbers and estimates for other cards are in the
|
||||
[details](DETAILS.md#speed-measured). The AMD measurements per card (RX 9070 XT, Radeon AI PRO R9700, RX 7800 XT,
|
||||
RX 9060 XT, RX 6900 XT) are in [AMD_HIP.md](AMD_HIP.md#rdna4-gfx1201).
|
||||
|
||||
Every PC is different: `START-HERE.bat --calibrate` measures a few engine settings on yours and keeps the fastest
|
||||
(about 5-10 minutes; on the PC above it made the Coder 7% faster; NVIDIA cards for now). Measured Strata on your own
|
||||
PC? See [Community benchmark results](COMMUNITY_BENCHMARKS.md) for a report template and how to share your results
|
||||
in a pull request.
|
||||
|
||||
## The sizes
|
||||
|
||||
| Model | RAM+VRAM Requirements | Speed | Quality |
|
||||
| --- | ---: | --- | --- |
|
||||
| **Q2_0** | 37.6 GB | fastest | good |
|
||||
| **IQ2_XS** | 39.2 GB | fast | better (**recommended**) |
|
||||
| **IQ3_XXS** | 47.0 GB | slower | great |
|
||||
| **IQ3_S** | 54.8 GB | slowest | best: matches the full model on the published tests (original model only) |
|
||||
|
||||
The download is 66-76 GB for the three smaller sizes ([details](DETAILS.md#which-model)); the first start also
|
||||
fetches the MTP draft layer (~6 GB, +1 GB with images).
|
||||
|
||||
## Will it fit?
|
||||
|
||||
Shard 1 is the part of the model that gets loaded when it starts: its experts go into your **RAM**, the rest onto
|
||||
your graphics card (the second shard, a 29 GB lookup table, stays on the SSD). So it fits when your **RAM is at least
|
||||
shard 1 + about 10 GB** for Windows and your other programs. With 64 GB of RAM every size fits (IQ3_S with little
|
||||
else open); with 48 GB, Q2_0 and IQ2_XS. A bigger graphics card makes it faster, but it doesn't lower the RAM needed
|
||||
- except in the low-RAM mode below.
|
||||
|
||||
### A big graphics card and little RAM
|
||||
|
||||
On a PC whose RAM cannot hold the experts beside the system, setup maps them from the model's files instead and keeps
|
||||
in RAM only what the graphics card does not hold (the [low-RAM mode](DETAILS.md#speed-measured), chosen by setup). For
|
||||
example, a 32 GB PC with a 24 GB GPU runs Q2_0, IQ2_XS and the Coder this way, and a 32 GB PC with a 12-16 GB GPU the
|
||||
Coder. With a small card most experts then come from the SSD and it is much slower (setup says so).
|
||||
`START-HERE.bat --setup --low-ram on|off` overrides the choice.
|
||||
|
||||
## The versions
|
||||
|
||||
### Qwen3.8-Flash-Next
|
||||
|
||||
The original, in all four sizes. With images, and with the
|
||||
[experimental speed projection](DETAILS.md#experimental-speed-projection-experimental-off-by-default) as an option.
|
||||
|
||||
### Coder
|
||||
|
||||
**[Coder](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF)** - ISTA-DASLab's coding version:
|
||||
half of the experts removed, keeping the ones that code, tool use and images need (91% of the full model's SWE-bench
|
||||
Verified score, 99% of LiveCodeBench, by its authors). One size (IQ1_M: its experts stored like IQ3_S): shard 1 is
|
||||
**29.6 GB**, so it fits a PC with **32 GB of RAM**, runs 262K context on 64 GB, and reads long prompts the fastest of
|
||||
all. It is weaker outside code, and that includes Chinese and other CJK text (#438: Chinese answers came out wrong
|
||||
or looping where English was fine). For general chat or CJK text, take a size that keeps every expert: Q2_0,
|
||||
IQ2_XS or IQ3_S.
|
||||
More: [details](DETAILS.md#or-the-coder-half-the-experts-for-code).
|
||||
|
||||
```
|
||||
START-HERE.bat --setup --family coder
|
||||
```
|
||||
|
||||
### Swift 1.5
|
||||
|
||||
**[Swift 1.5](https://huggingface.co/ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF)** - a fine-tune by UkisAI that
|
||||
thinks much shorter before answering, so you get the answer sooner, with about the same quality. Same speed per
|
||||
token, and about the same RAM as the same size of the original (no IQ3_S). Its own license applies (see its page).
|
||||
More: [details](DETAILS.md#or-swift-15-a-fine-tune-that-thinks-shorter).
|
||||
|
||||
```
|
||||
START-HERE.bat --setup --family swift --model IQ2_XS
|
||||
```
|
||||
|
||||
### Unsloth UD-Q4_K_XL (experimental)
|
||||
|
||||
**Unsloth's 4-bit UD-Q4_K_XL** (experimental) is the fourth version in setup's menu (`--family unsloth`): the closest
|
||||
to the full model, but a 111 GB download whose 77 GB of experts do not fit in RAM. Strata keeps your RAM minus 24 GB
|
||||
of them in RAM and reads the rest from the SSD while it answers: 7-8.5 tokens/s on a 64 GB PC with a 12 GB GPU, several
|
||||
times slower than the sizes above, and long prompts are slow. It needs 48 GB of RAM or more, an NVMe SSD and one
|
||||
NVIDIA GPU (no images yet). Details and measurements: [UD-Q4_K_XL](UNSLOTH_Q4.md).
|
||||
|
||||
```
|
||||
START-HERE.bat --setup --family unsloth --model UD-Q4_K_XL
|
||||
```
|
||||
|
||||
### OrcaRouter Uncensored IQ3_XXS
|
||||
|
||||
For **OrcaRouter's Flash-Next Uncensored IQ3_XXS**, see the [manual compatibility setup](ORCA.md). It needs an
|
||||
explicit packing conversion and is not an installer menu option.
|
||||
|
||||
## Adding or switching models
|
||||
|
||||
You can add another model any time with `SETUP.bat` (the same as `START-HERE.bat --setup`; on Linux
|
||||
`./setup.sh --setup`). Files another model shares are not downloaded again (the Coder uses the original's shard 2 and
|
||||
vision encoder). With more than one model installed, `START-HERE.bat` asks which one to start; `run-<model>.bat`
|
||||
(Linux: `run-<model>.sh`) starts one directly.
|
||||
+11
-1
@@ -43,7 +43,8 @@ now on; the answer is kept.
|
||||
**Not supported** (setup says so and names the cards that can be used instead):
|
||||
- a card older than the RTX 20 series (compute capability below 7.5: GTX 10 and older);
|
||||
- a card with less than 8 GB of VRAM, together with others (each card holds a copy of the dense weights and its
|
||||
own prompt buffers);
|
||||
own prompt buffers) - unless you name it with `--gpus`: then setup says the risk and asks (`--yes` with the named
|
||||
cards goes ahead);
|
||||
- Intel GPUs, and a mix of NVIDIA and AMD cards. (AMD cards share a model among themselves: `./setup.sh --backend
|
||||
hip --gpus 1,0`, see [AMD_HIP.md](AMD_HIP.md).)
|
||||
|
||||
@@ -60,6 +61,15 @@ profiled expert plus the context's KV, the draft layer and the reserve, and says
|
||||
stays. On an R9700 32 GB + RX 9070 XT the R9700 holds all of the Coder's experts: with the flag 4K prompts read at
|
||||
1,776 tok/s instead of 1,244 (split) and decode runs at ~60 tok/s instead of ~51 (16K prompts ~5% slower than split).
|
||||
|
||||
**Short prompts on a split (0.1.32, #340).** 0.1.30 gave each card's prompt path a loan from its own expert cache,
|
||||
refilled after every request; on cards that hold nearly all their experts that cost short prompts up to a third of
|
||||
their speed. 0.1.32 refills all cards at once, uses a smaller streaming ring on a split, and lets a card with free
|
||||
VRAM keep its own prompt buffers - the same output as 0.1.31, measured on an R9700 + RX 9070 XT: 2K prompts 993 ->
|
||||
1,265 tok/s, 16K 1,852 -> 1,950, decode unchanged. `STRATA_SPLIT_OWN=1` (opt-in) gives every card its own buffers:
|
||||
2K 1,450 and 16K 2,227 tok/s there, but a full card then keeps a different set of experts resident, so the output
|
||||
differs from the default's (stable and coherent); `STRATA_SPLIT_OWN=auto` does that only where the buffers are at
|
||||
most 12% of each card's VRAM.
|
||||
|
||||
The engine flags behind it: `--layer-split K1[,K2..]|auto` and `--split-device D1[,D2..]` (the later stages'
|
||||
devices; default the next visible ones). `--layer-split K --split-device 0` runs both stages on one card sharing
|
||||
everything - the bit-exact check of the hand-off, not a speed mode.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# OrcaRouter Uncensored Q4_K_S (manual setup)
|
||||
|
||||
This path uses the three original `orcarouter/Qwen3.8-Flash-Next-Uncensored-GGUF` Q4_K_S shards without rewriting their expert, embedding or PLE table bytes. It is separate from the installer model menu. Keep all three shard filenames together.
|
||||
|
||||
The ordinary GGUF quantizes some small projections which Strata reads as BF16. `--compat-bf16` dequantizes and rounds those projections to BF16 in the separate pack; it does not restore their original precision. The Q5_0 PLE table remains in the source GGUF and currently uses the mapped reader (`--ple-io mmap`). The Q4_K gate/up experts and Q5_0/Q5_1 down experts remain in GGUF form, including layers whose three tensors cross shard boundaries (each role is read from its own shard). The GPU expert kernels take Q4_K with Q5_1 (or Q8_0) downs; Q5_0 downs are not among them yet, and the engine refuses to start on a pack with such a layer (it names the layer).
|
||||
|
||||
## Build and prepare
|
||||
|
||||
Build Strata with CUDA, native experts, `STRATA_ORCA_Q4KS_MMQ=ON` (and `STRATA_BUILD_TESTS=ON` for the two parity programs below). This opt-in compiles the Q5_0 GGML MMQ instance for CUDA or HIP (the Q4_K and Q5_1 instances come with the engine's K-quant MMQ build on CUDA); the usual Q8_0 draft-layer instance remains in both builds. HIP also needs `STRATA_PREFILL_MMQ=ON`. Select your card's compute capability, for example `89` on an L40S. On Windows with Ninja and CUDA 12.6, use the Visual Studio developer environment and `-DCMAKE_CUDA_RUNTIME_LIBRARY=Shared` to keep the CUDA runtime linkage consistent. The default pinned ggml checkout is supported by the project build.
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -G Ninja -DSTRATA_ENABLE_CUDA=ON -DSTRATA_NATIVE_EXPERTS=ON -DSTRATA_ORCA_Q4KS_MMQ=ON -DCMAKE_CUDA_ARCHITECTURES=89 -DCMAKE_CUDA_RUNTIME_LIBRARY=Shared -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build --target strata native_expert_parity ple_q5_parity
|
||||
python tools/iq_pack.py --gguf /models/Qwen3.8-Flash-Next-Uncensored-Q4_K_S-00001-of-00003.gguf --out packs/orca-q4_k_s --compat-bf16
|
||||
```
|
||||
|
||||
`iq_pack.py` needs NumPy and the `gguf-py` module from the pinned llama.cpp dependency (`STRATA_GGUF_PY` can point to that directory). It writes a roughly 1.43 GiB dense pack and a native-expert manifest. The original 112 GB of model shards are read in place; `experts.bin` is optional and is not written by this command.
|
||||
|
||||
Check the changed kernels against the real files before starting the service:
|
||||
|
||||
```sh
|
||||
build/native_expert_parity /models/Qwen3.8-Flash-Next-Uncensored-Q4_K_S-00001-of-00003.gguf /models/Qwen3.8-Flash-Next-Uncensored-Q4_K_S-00002-of-00003.gguf /models/Qwen3.8-Flash-Next-Uncensored-Q4_K_S-00003-of-00003.gguf 0 5 6 20 47
|
||||
build/ple_q5_parity /models/Qwen3.8-Flash-Next-Uncensored-Q4_K_S-00001-of-00003.gguf
|
||||
```
|
||||
|
||||
## Engine flags
|
||||
|
||||
Pass the same first shard to `--native` and `--ple-gguf`; Strata discovers the other two shards by name. For a first text-only run, use 32K context and a modest cache. Once that passes, increase the context and cache with memory measurements on the target machine.
|
||||
|
||||
```sh
|
||||
--pack packs/orca-q4_k_s --native /models/Qwen3.8-Flash-Next-Uncensored-Q4_K_S-00001-of-00003.gguf --ple-gguf /models/Qwen3.8-Flash-Next-Uncensored-Q4_K_S-00001-of-00003.gguf --ple-io mmap --expert-profile data/expert-profile.bin --expert-cache auto --prefill 512 --spec 4 --mtp /path/to/mtp/rt --max-context 32768 --kv int8
|
||||
```
|
||||
|
||||
For two GPUs in persistent server mode, add `--layer-split auto --split-device 1` (with CUDA0 and CUDA1 visible). The usual Strata MTP runtime is built from the original model's draft layer; draft acceptance on the Uncensored fine-tune must be measured. Keep this model's own tokenizer from the pack. Do not combine `--expert-cache-per-layer` with a variable-size native cache: that policy's slot ordering is independent of the pack's per-layer blob sizes.
|
||||
|
||||
This port does not claim that Q4_K_S beats llama.cpp on a given computer. Compare the same model bytes, prompt, context, output length and GPU load; report prefill and decode separately.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Something went wrong?
|
||||
|
||||
The common problems and what to do. Back to the [README](../README.md#something-went-wrong). The full table of
|
||||
error messages, with the older engine fixes, is in the [details](DETAILS.md#troubleshooting).
|
||||
|
||||
## While installing or starting
|
||||
|
||||
**My PC froze, or got very slow, the first time Strata started.**
|
||||
That's normal while it starts, most of all the first time. Strata loads 35-55 GB into your RAM, locks part of it for
|
||||
the graphics card, and works out how much of the model fits on your GPU. The mouse can freeze for a few minutes.
|
||||
**Wait, and don't close the window.** The next starts are much faster. Still frozen after 10 minutes? Restart the
|
||||
PC, close other programs (browsers use a lot of RAM) and try again. If it keeps happening, pick a smaller size (Q2_0
|
||||
or IQ2_XS).
|
||||
|
||||
**It stopped while downloading or installing.**
|
||||
Run `START-HERE.bat` (Linux: `./setup.sh`) again. It continues where it stopped.
|
||||
|
||||
**It says the NVIDIA driver is too old.**
|
||||
Update it (NVIDIA App or [nvidia.com/drivers](https://www.nvidia.com/drivers)), restart the PC, and run
|
||||
`START-HERE.bat` again.
|
||||
|
||||
**It says port 8080 is already in use.**
|
||||
Strata is already running. Look for its window. Or another program uses the port: `START-HERE.bat --port 8081`.
|
||||
|
||||
**Python or the build tools could not be installed.**
|
||||
Install what it names (links are printed), then run it again. Everything already done is kept.
|
||||
|
||||
**The first start takes minutes.**
|
||||
It is reading 34-55 GB into RAM; the second start is faster while the files are in the OS cache. Started from Task
|
||||
Scheduler, it can be 24x slower: see [Running it at startup](DETAILS.md#running-it-at-startup-task-scheduler).
|
||||
|
||||
## While it answers
|
||||
|
||||
**It's very slow and the disk light keeps blinking.**
|
||||
Your PC is out of free RAM. Close other programs, or pick a smaller size (Q2_0 or IQ2_XS).
|
||||
|
||||
**An answer stopped with "the engine stopped unexpectedly".**
|
||||
Usually not enough RAM (on Linux the system then stops the engine). Just send your message again: Strata starts the
|
||||
engine by itself. If it keeps happening, close other programs or pick a smaller size.
|
||||
|
||||
**It says the prompt exceeds the context.**
|
||||
The conversation is longer than the context you chose. Start a new chat, or run `SETUP.bat` and pick more
|
||||
context.
|
||||
|
||||
**It is slower than the tables.**
|
||||
The monitor plugged into the graphics card and other GPU programs take VRAM from the expert cache; RAM running below
|
||||
its rated speed (enable EXPO/XMP in the BIOS) slows the CPU half.
|
||||
|
||||
**Pictures are refused, or slow.**
|
||||
"this server was started without the vision encoder": the model was set up for text only - run setup again with
|
||||
`--vision gpu` (or `--vision cpu`). Pictures that take 10-30 s are read by the encoder on the CPU; `--vision gpu`
|
||||
(NVIDIA, ~1.4 GB of VRAM) makes it 0.1-0.5 s.
|
||||
|
||||
## AMD cards
|
||||
|
||||
**"No AMD GPU found (the amdgpu driver's KFD topology is empty)" on Linux.**
|
||||
The kernel's amdgpu driver is not loaded for the card. Integrated Radeon GPUs are listed as not supported; setup
|
||||
lists every card it found and whether Strata can use it.
|
||||
|
||||
**The engine stops at start with the card's name, its architecture and the build's list.**
|
||||
The engine was compiled for another card (for example after moving the Strata folder to another PC). Run
|
||||
`./setup.sh --setup --backend hip`: it compiles the engine for this card's architecture.
|
||||
|
||||
**Large pinned host allocations fail on ROCm although RAM is free.**
|
||||
See [AMD_HIP.md](AMD_HIP.md#model-and-serving-configuration): the mapped expert mode avoids the full pinned arena.
|
||||
|
||||
## Still stuck?
|
||||
|
||||
Look in the [full troubleshooting table](DETAILS.md#troubleshooting), or open an
|
||||
[issue](https://github.com/Niko1221/Strata/issues) and attach `strata-<model>.log` from the Strata folder.
|
||||
+91
-15
@@ -1,8 +1,8 @@
|
||||
# Unsloth UD-Q4_K_XL (experimental, manual import)
|
||||
# Unsloth UD-Q4_K_XL (experimental)
|
||||
|
||||
**Experimental, not in setup yet.** Engine 0.1.31 or newer. Validated on one PC: Windows 10, an RTX 5070 (12 GB),
|
||||
64 GB of RAM and an AVX-512 CPU, on 2026-10-01. Everything below is a manual workflow; `START-HERE.bat` / `setup.sh`
|
||||
do not offer this model.
|
||||
**Experimental.** Setup offers it from engine 0.1.32 ([below](#setup)); the manual workflow after that section works
|
||||
with engine 0.1.31 or newer. Validated on one PC: Windows 10, an RTX 5070 (12 GB), 64 GB of RAM and an AVX-512 CPU,
|
||||
on 2026-10-01.
|
||||
|
||||
The target is Unsloth's 4-bit quantization of the same model the other packs use:
|
||||
[unsloth/Qwen3.8-Flash-Next-GGUF, `UD-Q4_K_XL`, revision `38bb39e`](https://huggingface.co/unsloth/Qwen3.8-Flash-Next-GGUF/tree/38bb39ee97821de2c9009abb7e93950eec396e66/UD-Q4_K_XL).
|
||||
@@ -11,6 +11,31 @@ Q5_1 (Q8_0 in layers 2, 4, 30, 46, 47) for down; the token embedding, the output
|
||||
projections and the PLE key are Q8_0; the 28.8 GB PLE table is IQ4_NL. The routed experts are 71.7 GiB, about twice
|
||||
the 3-bit models'.
|
||||
|
||||
## Setup
|
||||
|
||||
`START-HERE.bat --setup` (Linux: `./setup.sh --setup`) and choose **Qwen3.8-Flash-Next (Unsloth)**, marked
|
||||
`[experimental]`; or directly:
|
||||
|
||||
```sh
|
||||
START-HERE.bat --setup --family unsloth --model UD-Q4_K_XL
|
||||
```
|
||||
|
||||
What setup does differently for this model:
|
||||
|
||||
- It needs 48 GB of RAM or more (with less it asks, default no; `--model UD-Q4_K_XL --yes` installs it anyway) and
|
||||
engine 0.1.32 or newer (checked before anything is downloaded), and an NVIDIA
|
||||
GPU: it has not been run on AMD cards (its prompt kernels for the Q4_K / Q5_K experts are NVIDIA-only), so with
|
||||
`--backend hip` setup says so and asks before the download (#429; `--model UD-Q4_K_XL --yes` tries it). One GPU only: with `--gpus` it uses the first one and says so. No images (the vision encoder is not wired to
|
||||
this file yet) and no experimental speed projection (not tested with it).
|
||||
- It downloads the four shards below from the pinned revision `38bb39e` (resumable, like the other models), then
|
||||
checks each one's size and SHA-256 against the table below; the check takes a few minutes once and is remembered
|
||||
in the file's finish mark. A file with the wrong hash is deleted, so the next run downloads it again.
|
||||
- It packs with `--compat-bf16` and never writes `experts.bin` (`--low-ram` does not apply).
|
||||
- The RAM budget is the PC's RAM less 24 GB: `--resident-budget-gib 40` on 64 GB, at most all 71 GiB of experts on
|
||||
96 GB or more. From a 64K context up, where the KV cache moves to RAM, its size comes out of the budget. Setup's
|
||||
`--resident-budget-gib N` sets another one (a bigger one is kept, with a note on what it risks).
|
||||
- It recommends an 8K context on a GPU under 14 GB (every GB of KV cache is a GB less of cached experts).
|
||||
|
||||
## The files
|
||||
|
||||
Four shards, 111,334,654,784 bytes (111.3 GB) together. Shard 1 holds only the metadata; layer 11's down projection is
|
||||
@@ -24,7 +49,8 @@ in shard 2 and its gate/up in shard 3, which this engine handles (`native_expert
|
||||
| `Qwen3.8-Flash-Next-UD-Q4_K_XL-00004-of-00004.gguf` | 12,087,983,520 | `753bda48b98ba4f1636134a90a967de1b2d3908a236c026e464777342e53510a` |
|
||||
|
||||
Keep the four files together in one folder under these names (the engine finds shards 2-4 from shard 1's name; a
|
||||
missing shard is an error that names it). Check them before packing (`sha256sum -c`, or `Get-FileHash` on Windows).
|
||||
missing shard is an error that names it). Setup checks them itself; by hand, check them before packing
|
||||
(`sha256sum -c`, or `Get-FileHash` on Windows).
|
||||
Hugging Face snapshot symlinks work if you pass the snapshot's file name, not the hash-named blob it points to.
|
||||
|
||||
## What it needs
|
||||
@@ -35,7 +61,7 @@ Hugging Face snapshot symlinks work if you pass the snapshot's file name, not th
|
||||
| RAM | 64 GB measured. The RAM budget (below) holds the most-used experts; the rest come from the SSD through the OS file cache. |
|
||||
| GPU | 12 GB measured (RTX 5070): after the weights, the draft layer and the buffers, the expert cache held 1,280 of the 24,576 experts (3.7 GiB) at 4K context. |
|
||||
|
||||
## The pack
|
||||
## The pack (by hand)
|
||||
|
||||
Build Strata (or install 0.1.31+), then, from the repository root, with gguf-py from the pinned llama.cpp
|
||||
(setup installs it; `STRATA_GGUF_PY` can point to its `gguf-py` folder):
|
||||
@@ -61,8 +87,8 @@ source/destination type, method, whether it is exact, the largest absolute error
|
||||
264 F32 tensors (routers, injections, SSM gates) whose values are already BF16's are stored as BF16 exactly; the 195
|
||||
Q8_0 projections above are rounded to BF16 (largest absolute error 0.0144, in `hc_ffn_up`); the PLE convolution (F32)
|
||||
is narrowed to F16 when the engine loads it (largest error 3e-8). That is the compatibility Unsloth's file needs; it is
|
||||
not the original BF16 checkpoint. The output has not yet been compared with llama.cpp on the same GGUF (argmax
|
||||
agreement); treat quality as unmeasured.
|
||||
not the original BF16 checkpoint. The output matches llama.cpp's on the same file:
|
||||
[Quality](#quality-against-llamacpp-on-the-same-file).
|
||||
|
||||
## The MTP draft layer
|
||||
|
||||
@@ -107,7 +133,7 @@ budget take first.
|
||||
|
||||
**Choosing the RAM budget N:** your RAM minus 20-24 GB (the OS, the engine itself, the 126 MB router copy, and room for
|
||||
the OS file cache that serves the rest). On 64 GB, 40. The engine clamps a budget larger than the RAM it finds free
|
||||
(minus 4 GB) and says so. Everything above N comes from the SSD for every token, so N is the setting that matters
|
||||
(minus 4 GB and a 256 MiB margin) and says so; before #403's fix such a clamped budget could then fail the start. Everything above N comes from the SSD for every token, so N is the setting that matters
|
||||
most; a bigger budget was faster in every measurement (24 / 32 / 40 GiB). When the driver page-locks the whole budget
|
||||
(24 GiB did on this PC, 32 and 40 did not), the GPU also computes a share of the misses over PCIe, as in the resident
|
||||
low-RAM mode; `STRATA_RESIDENT_PIN=0` keeps the budget locked only (the CPU then computes every miss).
|
||||
@@ -123,13 +149,61 @@ low-RAM mode; `STRATA_RESIDENT_PIN=0` keeps the budget locked only (the CPU then
|
||||
| 40 GiB | **7-8.5 tok/s** (6 runs: 6.7-8.6, mean 7.9) | 1.1 GB read from the SSD per verify round, ~0.33 GB per token |
|
||||
|
||||
- The first answer comes about 60 s after the engine starts (mostly loading: the RAM budget is copied from the GGUF
|
||||
files at start). A 30-token prompt then took 5-6 s. Longer prompts have not been measured and will be slow: the
|
||||
prompt path dequantizes these formats to FP16 (fast prompt kernels for Q4_K/Q5_K/Q5_1 are future work).
|
||||
files at start). A 30-token prompt then took 5-6 s.
|
||||
- Long prompts (engine 0.1.32): a 16K prompt is read at **160 tokens/s** (15,873 tokens in 99 s; 0.1.31: 57 tokens/s,
|
||||
273-282 s). Reading a prompt streams every expert once per 4K chunk, and the third of them outside the RAM budget
|
||||
come from the SSD (~35 GB per chunk), so the prompt path now reads them with 32 threads and up to 128 blobs in
|
||||
flight when the experts are read from the GGUF in place (`STRATA_STAGER_THREADS` / `STRATA_STAGER_RING` override
|
||||
it; 4 and 16 before, still the default for every other model). Between the chunks, the 28.8 GB PLE table's rows
|
||||
take ~5 s per chunk.
|
||||
- An engine compiled with `-DSTRATA_MMQ_KQUANTS=ON` multiplies the Q4_K / Q5_K / Q5_1 experts of a prompt with
|
||||
llama.cpp's MMQ kernels (as the other models' formats always are) instead of dequantizing them to FP16: 4x less GPU
|
||||
time for those products, but on this PC the prompt waits for the SSD either way (29 s per 4K chunk with or
|
||||
without). Off in the released engine: it loads every kernel at start, and these took 1-3 expert slots of VRAM from
|
||||
every model.
|
||||
- The speed varies from run to run (6.7-8.6 tok/s at 40 GiB for the same prompt), with what the OS file cache holds.
|
||||
- For comparison, IQ3_S (all its experts in RAM) writes ~53 tok/s on the same PC ([the speed tables](DETAILS.md#speed-measured)).
|
||||
- Where the time goes (`--stats` on the command line): at 40 GiB about 550-750 ms of each verify round (3.5 tokens) is
|
||||
reading experts from the SSD, ~75 ms the CPU's expert kernels, ~13 ms the GPU.
|
||||
|
||||
## Quality: against llama.cpp on the same file
|
||||
|
||||
Strata and llama.cpp (the pinned commit `3cf0325`, a CPU build reading the GGUF memory-mapped) were given the same
|
||||
token sequences, and at every position each wrote its 20 most likely next tokens with their log-probabilities: Strata
|
||||
through its verify windows (`STRATA_LOGPOS` with `STRATA_LOGPOS_TOPK=20`), llama.cpp from `llama_get_logits_ith` over
|
||||
one batch. Strata ran with the recommended settings (RAM budget 40 GiB, the MTP draft layer on, greedy,
|
||||
`--adapt-every 100000` so the expert cache does not move during the run). Per position: whether the most likely token is the same (argmax
|
||||
agreement), how many of the 5 / 10 most likely tokens both have, the KL divergence (llama.cpp's distribution against
|
||||
Strata's, over llama.cpp's top 20 plus one bucket for the rest), and the perplexity of the sequence under each.
|
||||
|
||||
| Token set | Positions | Argmax agreement | Top-5 / top-10 overlap | KL | Perplexity Strata / llama.cpp |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| Code: Strata's greedy answer to a coding prompt (an LRU cache in Python, 56-token prompt) | 400 | **99.0%** | 95.6% / 95.3% | 0.0008 | 1.067 / 1.070 |
|
||||
| Thinking: Strata's greedy answer to a reasoning puzzle (with thinking, 55-token prompt) | 324 | **97.5%** | 96.2% / 96.4% | 0.0016 | 1.107 / 1.106 |
|
||||
| After a 16K prompt: Strata's greedy continuation (256 tokens) of the 16,384 real-text tokens below | 256 | 90.2% | 88.1% / 89.3% | 0.039 | 1.619 / 1.623 |
|
||||
| Real text after a 16K prompt: the last 510 tokens of 16,384 tokens of this repository's docs and code | 510 | 91.2% | 88.3% / 89.9% | 0.035 | 4.27 / 4.29 |
|
||||
| The same 511 positions with only 512 tokens before them | 511 | 89.2% | 86.0% / 86.3% | 0.061 | 8.99 / 8.87 |
|
||||
|
||||
For comparison, [eddoursul/Strata](https://github.com/eddoursul/Strata) reported 98.2-99.3% (code), 95.6-95.8%
|
||||
(thinking) and 97.7% (after a 16K prompt) for the same file against llama.cpp on an RTX 3090.
|
||||
|
||||
**Where they differ:** at near-ties. Grouped by llama.cpp's own gap between its two most likely tokens, Strata picks
|
||||
the same token at every position of the code and thinking answers where that gap is 0.2 or more (695 of 695) and at
|
||||
every position after the 16K prompt where it is 0.5 or more (144 of 144 in the continuation, 254 of 254 in the real
|
||||
text; 50 of 54 and 117 of 121 between 0.2 and 0.5). Text after a long document has many more near-ties than a short
|
||||
answer (58 of the continuation's 256 positions and 135 of the real text's 510 have a gap under 0.2, against 13 of the
|
||||
code answer's 400), which is why its agreement is lower. It is not the long context itself: the same real-text
|
||||
positions after only 512 tokens agree a little less (89.2%), and the perplexities match within 3% in every set. For
|
||||
scale, Strata's own greedy run and its teacher-forced rerun of the continuation pick different tokens at 6% of the
|
||||
positions (93.8% the same: other window sizes, other experts in VRAM), the same kind of near-tie flips. The 16K
|
||||
prompts were read by Strata's batched prompt path (15,872 tokens), the rest through the verify windows. These numbers
|
||||
are the FP16 prompt path's (the released engine's); with the MMQ prompt path (`-DSTRATA_MMQ_KQUANTS=ON`, above) the
|
||||
continuation after the 16K prompt agrees at 92.2% (KL 0.029, three runs, identical).
|
||||
|
||||
To repeat it: Strata's side is `STRATA_LOGPOS=<file>` with `STRATA_LOGPOS_TOPK=20` (engine 0.1.32) on a serve
|
||||
engine with `--short-read` covering the positions to compare; llama.cpp's side was a short program over
|
||||
`llama_decode` / `llama_get_logits_ith` writing the same top 20 per position.
|
||||
|
||||
## Opt-ins and switches (environment variables)
|
||||
|
||||
| | |
|
||||
@@ -137,7 +211,7 @@ low-RAM mode; `STRATA_RESIDENT_PIN=0` keeps the budget locked only (the CPU then
|
||||
| `STRATA_LOOKAHEAD=0` | Turns off the routing-aware prefetch (on by default in this mode): while the CPU works on a layer, a thread applies the next layer's router to this layer's input and asks the OS to read the predicted experts' pages. Pages only, the answers are the same. About half of the SSD reads were predicted; mean +14% (6.9 -> 7.9 tok/s). `STRATA_LOOKAHEAD_K` sets the experts per token (default 10). |
|
||||
| `STRATA_KQ256=1` | Multi-token AVX2 kernels for the Q4_K / Q5_1 / Q8_0 experts. Bit-exact with ggml's, but measured no faster, so off. |
|
||||
| `STRATA_PARTIAL_PIN=1` | Registers the hottest part of the RAM budget (up to `STRATA_PARTIAL_PIN_GIB`, default 24) with the GPU driver, so the GPU computes a share of the misses over PCIe (`--pcie-frac`). Measured no faster on this PC, and it changes the numerics of those experts (GPU kernels instead of the CPU's), so off. |
|
||||
| `STRATA_FETCH_THREADS=N` | Threads that read the experts from the GGUF (default 8; 16 was no faster). |
|
||||
| `STRATA_FETCH_THREADS=N` | Threads that read the experts from the GGUF while it answers (default 8; 16 was no faster). The prompt path has its own: `STRATA_STAGER_THREADS` (default 32 here) and `STRATA_STAGER_RING` (128). |
|
||||
|
||||
## Scope and validation
|
||||
|
||||
@@ -145,10 +219,12 @@ low-RAM mode; `STRATA_RESIDENT_PIN=0` keeps the budget locked only (the CPU then
|
||||
have kernels for; the engine checks every layer's formats at start and refuses an unsupported one by name.
|
||||
- Tests: the packer's synthetic 4-shard and conversion tests (`.venv/bin/python -m unittest discover -s tools -p
|
||||
test_iq_pack.py`); CTests `gguf_split_test`, `expert_layout_test`, `native_expert_parity_*` (the three real expert
|
||||
format pairs against ggml-cpu, the Q5_1 min term, Q8_0 rows); the in-place mode against `experts.bin` on the Coder
|
||||
format pairs against ggml-cpu, the Q5_1 min term, Q8_0 rows), `prefill_mmq_kquant_test` (with `-DSTRATA_MMQ_KQUANTS=ON`: the
|
||||
prompt path's MMQ products for Q4_K / Q5_K / Q5_1 / Q8_0 against ggml's dequantized weights); the in-place mode against `experts.bin` on the Coder
|
||||
(identical tokens and logits).
|
||||
- Real runs: greedy answers to a coding prompt (correct) at every budget and setting above, identical across them. Not
|
||||
yet: llama.cpp argmax agreement, long prompts, sampled decoding, long conversations. Please report what you see.
|
||||
- Real runs: greedy answers to a coding prompt (correct) at every budget and setting above, identical across them;
|
||||
the same tokens as llama.cpp at 97.5-99% of the positions of short greedy answers and 90-91% after a 16K prompt,
|
||||
differing mostly at near-ties (above). Not yet: sampled decoding, long conversations. Please report what you see.
|
||||
|
||||
## Credits
|
||||
|
||||
|
||||
@@ -96,6 +96,8 @@ inline const char* ggml_type_name(uint32_t t) {
|
||||
return "IQ2_S";
|
||||
case 23:
|
||||
return "IQ4_XS";
|
||||
case 24:
|
||||
return "I8";
|
||||
case 30:
|
||||
return "BF16";
|
||||
case 34:
|
||||
@@ -204,6 +206,10 @@ inline bool block_geometry(uint32_t t, int& elems, int& bytes) {
|
||||
elems = 256;
|
||||
bytes = 56;
|
||||
return true;
|
||||
case 24: // I8: raw bytes (the FP8 PLE table of tools/ple_fp8_pack.py)
|
||||
elems = 1;
|
||||
bytes = 1;
|
||||
return true;
|
||||
case 42:
|
||||
elems = 64;
|
||||
bytes = 18;
|
||||
|
||||
@@ -164,8 +164,44 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
// #342: drop the parked entries an outgoing conversation (its live tokens and checkpoint chain) supersedes:
|
||||
// the same conversation a turn back, whose DEEPEST checkpoint the outgoing chain still holds, so all it adds
|
||||
// is the tail the client rewrote (the reply as it was generated, before the next request re-rendered it) and
|
||||
// checkpoints older than that one. A subagent's successive turns parked one such copy each, and make_room's
|
||||
// oldest-first eviction then pushed the parent conversation out after `slots` turns. An entry without
|
||||
// checkpoints, or whose deepest checkpoint the outgoing chain does not hold (another conversation that only
|
||||
// shares the system prompt's root with it), is kept. Returns how many were dropped.
|
||||
size_t drop_superseded(const std::vector<int32_t>& ids, const std::vector<ConversationImageKey>& images,
|
||||
const std::vector<ConversationCheckpoint>& checkpoints, bool cvec) {
|
||||
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;
|
||||
};
|
||||
size_t dropped = 0;
|
||||
for (size_t i = 0; i < entries_.size();) {
|
||||
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)) {
|
||||
bytes_ -= e.bytes();
|
||||
entries_.erase(entries_.begin() + (std::ptrdiff_t) i);
|
||||
++dropped;
|
||||
continue;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
superseded_ += dropped;
|
||||
return dropped;
|
||||
}
|
||||
size_t superseded() const { return superseded_; }
|
||||
|
||||
bool put(SavedConversation&& image, size_t held = 0) {
|
||||
const size_t n = image.bytes();
|
||||
if (!enabled() || held > budget_ || n > budget_ - held) return false; // make_room's refusal, first
|
||||
drop_superseded(image.live.ids, image.live.imgs, image.checkpoints, image.cvec);
|
||||
if (!make_room(n, held)) return false;
|
||||
entries_.push_back(std::move(image));
|
||||
bytes_ += n;
|
||||
@@ -173,7 +209,7 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
size_t budget_ = 0, slots_ = 0, bytes_ = 0, evictions_ = 0;
|
||||
size_t budget_ = 0, slots_ = 0, bytes_ = 0, evictions_ = 0, superseded_ = 0;
|
||||
std::deque<SavedConversation> entries_; // least recently active first
|
||||
ConversationKvReuse reuse_;
|
||||
};
|
||||
|
||||
@@ -49,15 +49,23 @@ namespace strata::core {
|
||||
/// The longest penalty window the coupled drafter mirrors (serve's kPenaltyWindowCap).
|
||||
inline constexpr int kCoupledHistCap = 4096;
|
||||
|
||||
/// STRATA_SPEC_COUPLED=1 (any value but empty or "0"): read once.
|
||||
inline bool coupled_draft_env() {
|
||||
static const bool on = [] {
|
||||
inline bool& coupled_draft_state() {
|
||||
static bool on = [] {
|
||||
const char* e = std::getenv("STRATA_SPEC_COUPLED");
|
||||
return e != nullptr && *e != '\0' && std::strcmp(e, "0") != 0;
|
||||
}();
|
||||
return on;
|
||||
}
|
||||
|
||||
/// Whether coupled draft sampling is enabled (via STRATA_SPEC_COUPLED=1 or --coupled-draft CLI flag).
|
||||
inline bool coupled_draft_env() {
|
||||
return coupled_draft_state();
|
||||
}
|
||||
|
||||
inline void set_coupled_draft(bool on) {
|
||||
coupled_draft_state() = on;
|
||||
}
|
||||
|
||||
/// The MTP cell of chain step j after a window at `p` with `a` drafts accepted (draft() runs step j there).
|
||||
STRATA_COUPLED_HD inline int64_t coupled_draft_cell(int64_t p, int a, int j) { return p + a + j; }
|
||||
|
||||
|
||||
@@ -38,11 +38,25 @@ std::string gpu_arch_problem(int ordinal);
|
||||
// The GPU architectures this binary was compiled for ("gfx1100,gfx1201"); "" on CUDA builds.
|
||||
const char* compiled_gpu_archs();
|
||||
|
||||
// How many devices the runtime enumerates, numbered as HIP_VISIBLE_DEVICES / CUDA_VISIBLE_DEVICES number them; 0 when
|
||||
// there is none (or no usable runtime). `device_info` throws on a card this binary cannot run, so a caller that LISTS
|
||||
// every card - the ones it has no code for included (`strata-device --list-devices`) - counts them with this first.
|
||||
int device_count();
|
||||
|
||||
// One line per device for `strata-device --list-devices`, without the arch check that `device_info` applies:
|
||||
// "arch gfx1201, 15.9 GiB" (HIP) or "compute capability 12.0, 11.9 GiB" (CUDA), plus the device's name. false when
|
||||
// the runtime cannot describe it.
|
||||
bool device_summary(int ordinal, std::string& name, std::string& detail);
|
||||
|
||||
// Throws when there is no CUDA device. The engine targets sm_120 specifically and must say so rather than
|
||||
// run slowly on something else: `CMakeLists.txt` already refuses to COMPILE for another architecture, and
|
||||
// this is the matching check at run time (a binary can be carried to a different machine).
|
||||
DeviceInfo device_info(int ordinal = 0);
|
||||
|
||||
/// "" when this build has device code for the current device, else CUDA's error: a build for other GPUs would
|
||||
/// otherwise fail at its first kernel launch, with nothing that names the cause.
|
||||
std::string device_code_error();
|
||||
|
||||
class CudaError : public std::runtime_error {
|
||||
public:
|
||||
CudaError(const std::string& what, int code) : std::runtime_error(what), code_(code) {}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// strata/core/emulate.hpp - STRATA_EMULATE_CC: another GPU generation's code paths, run on this card (tests only).
|
||||
//
|
||||
// A build whose device code is PTX for an older architecture (CMAKE_CUDA_ARCHITECTURES=86-virtual) runs on a newer
|
||||
// card through the driver's JIT, and every kernel then sees __CUDA_ARCH__ = 860. What the host decides from the
|
||||
// device must agree with that, or it picks paths and shared-memory sizes the device code was not built for:
|
||||
// STRATA_EMULATE_CC=75|80|86|89 makes each such query (compute capability, shared memory per block) answer as that
|
||||
// card would. Speed under it says nothing about the real card; results do - the kernels are the real card's.
|
||||
#pragma once
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace strata {
|
||||
|
||||
/// the emulated compute capability as 10 * major + minor, or 0 (off)
|
||||
inline int emulated_cc() {
|
||||
static const int cc = [] {
|
||||
const char* e = std::getenv("STRATA_EMULATE_CC");
|
||||
const int v = e ? std::atoi(e) : 0;
|
||||
return v >= 75 && v < 120 ? v : 0;
|
||||
}();
|
||||
return cc;
|
||||
}
|
||||
inline int cc_major_of(int real_major) { return emulated_cc() ? emulated_cc() / 10 : real_major; }
|
||||
inline int cc_minor_of(int real_minor) { return emulated_cc() ? emulated_cc() % 10 : real_minor; }
|
||||
/// cudaDevAttrMaxSharedMemoryPerBlockOptin as the emulated card reports it: 64 KB on Turing, 163 KB on the A100,
|
||||
/// 99 KB on the other Ampere and Ada cards
|
||||
inline int smem_optin_of(int real) {
|
||||
switch (emulated_cc()) {
|
||||
case 0: return real;
|
||||
case 75: return 65536;
|
||||
case 80: return 166912;
|
||||
default: return real < 101376 ? real : 101376;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace strata
|
||||
@@ -55,6 +55,24 @@ inline constexpr int32_t kNotResident = -1;
|
||||
bool read_expert_profile(const std::string& path, int64_t n_layers, int64_t n_expert,
|
||||
std::vector<std::pair<int32_t, int32_t>>& ranked, int64_t& slots, std::string& err);
|
||||
|
||||
/// #477 (--expert-profile-save): what the adaptive tier learned, as a profile ranking EVERY (layer, expert) pair -
|
||||
/// the experts resident in VRAM now first (where the swaps left the cache), then the rest; within each, by `heat`
|
||||
/// (the routing the adaptive tier counted since the start, descending), then by `prior` (the profile the engine
|
||||
/// started from: an expert this run never routed keeps its old place), then by index. A start from it begins
|
||||
/// where this one ended; one with more slots adds the hottest of the rest, one with fewer keeps the hottest.
|
||||
/// `resident` and `heat`: n_layers x n_expert entries (`heat` may be empty: no counts, the prior decides).
|
||||
std::vector<std::pair<int32_t, int32_t>> rank_learned_profile(int64_t n_layers, int64_t n_expert,
|
||||
const std::vector<uint8_t>& resident,
|
||||
const std::vector<double>& heat,
|
||||
const std::vector<std::pair<int32_t, int32_t>>& prior);
|
||||
|
||||
/// #477: writes `ranked` in tools/make_profile.py's format, byte for byte: `STRP`, `1, n_layers, n_expert,
|
||||
/// n_ranked, n_ranked` (uint32), the pairs (uint16 layer, uint16 expert), then the n_layers x n_expert int32 table
|
||||
/// of each pair's rank (-1: not ranked). Atomically: a `<path>.tmp` beside it, renamed over `path` once complete,
|
||||
/// so a reader (the next start) never sees half a file. False with `err` when it could not be written.
|
||||
bool write_expert_profile(const std::string& path, int64_t n_layers, int64_t n_expert,
|
||||
const std::vector<std::pair<int32_t, int32_t>>& ranked, std::string& err);
|
||||
|
||||
class ExpertCache {
|
||||
public:
|
||||
ExpertCache() = default;
|
||||
|
||||
@@ -421,6 +421,10 @@ public:
|
||||
/// CS-T, `budget_bytes` > 0 (`--resident-budget-gib`): only as many of those experts as fit `budget_bytes`, taken
|
||||
/// in `rank` order (the expert profile: the hottest after the GPU cache's), are copied; the rest stay on the
|
||||
/// mapped files (the SSD tier). No lend region then (a lent slot's expert is read from the files).
|
||||
/// #467: `budget_bytes` = `kResidentWhatFits` is that path sized by the RAM alone (available minus the headroom
|
||||
/// and the #403 margin) - the soft --resident-experts mode's second try when the whole complement does not fit;
|
||||
/// false when not even one expert fits. On Windows the mapped experts leave the working set before any reading.
|
||||
static constexpr uint64_t kResidentWhatFits = ~0ull;
|
||||
bool pin_cache_complement(
|
||||
const ExpertCache& cache, std::string& err, bool pin = true,
|
||||
const std::vector<std::pair<int32_t, int32_t>>& additional_gpu_pairs = {}, int64_t lend_from_slot = -1,
|
||||
|
||||
@@ -217,6 +217,9 @@ struct QsaState {
|
||||
/// Uses k_q/k_scale + v_q4. Mode 0 only (no KV streaming, no ring); under this setting the MTP drafter's
|
||||
/// ring state stays plain INT8, so the block movers never see the hybrid layout.
|
||||
bool kv_hybrid = false;
|
||||
/// K and V go through kv_q4.hpp's Walsh-Hadamard rotation before they are stored, the queries too, the output
|
||||
/// back: always for Q4_0, for INT8 by qsa_set_kv_int8_rotate (spreads outlier channels over the scale groups)
|
||||
bool kv_rot = false;
|
||||
int32_t* page_table = nullptr; ///< (n_pages,) logical page -> physical page (-1: not resident, streamed)
|
||||
int64_t n_pages = 0;
|
||||
int64_t max_cells = 0;
|
||||
@@ -237,6 +240,7 @@ struct QsaState {
|
||||
int32_t* idx_block_pos = nullptr;
|
||||
|
||||
float* cos_tab = nullptr; ///< (max_cells, n_rot/2), built on the HOST in float64
|
||||
bool owns_rope = false; ///< built the table above (not borrowed with share_rope): it releases it
|
||||
float* sin_tab = nullptr;
|
||||
|
||||
/// THE PER-TOKEN COUNTS, IN DEVICE MEMORY - the whole reason this layer can be a graph. `qsa_step_fill`
|
||||
@@ -278,6 +282,8 @@ uint64_t qsa_kv_host_bytes();
|
||||
/// Plan v0.3 P7: store K/V as INT8 with FP16 scales per 64 values (half the VRAM of FP16). Set before sizing and
|
||||
/// initializing the session; default off until gate G-C accepts it.
|
||||
void qsa_set_kv_int8(bool enabled);
|
||||
/// INT8 K/V through the Hadamard rotation (off by default: STRATA_KV_ROT=1)
|
||||
void qsa_set_kv_int8_rotate(bool enabled);
|
||||
bool qsa_kv_int8();
|
||||
/// PR #21: store K/V as Q4_0 after a Hadamard rotation (`--kv q4_0`): 576 B per cell, vs 1,056 in INT8.
|
||||
void qsa_set_kv_q4(bool enabled);
|
||||
|
||||
@@ -25,6 +25,10 @@ public:
|
||||
/// supported type, 2-D), read from the GGUF headers only - so the canonical arena can skip them.
|
||||
static bool served_names(const std::vector<std::string>& shards, bool include_ple_key,
|
||||
std::set<std::string>& out, std::string& err);
|
||||
/// #326: a native pack whose `blk.1.ple_key.weight` row is unquantized (iq_pack --compat-bf16 of a GGUF key
|
||||
/// the native kernel also reads, e.g. OrcaRouter's IQ3_XXS) serves the PLE from that row, so it is taken out
|
||||
/// of `skip` and `load` does not upload the GGUF key over it. A quantized row leaves `skip` unchanged.
|
||||
static bool keep_unquantized_ple_key(const std::string& pack_dir, std::set<std::string>& skip, std::string& err);
|
||||
uint64_t weight_bytes() const { return bytes_; }
|
||||
size_t tensor_count() const { return weights_.size(); }
|
||||
|
||||
|
||||
@@ -99,6 +99,9 @@ uint64_t session_bytes(const ModelGeometry& g, int64_t max_cells, int64_t k, int
|
||||
/// Carves `base` (DEVICE memory) into `s`. Returns the bytes used. Same range convention as `session_bytes`.
|
||||
uint64_t session_init(const ModelGeometry& g, int64_t max_cells, int64_t k, void* base, SessionState& s,
|
||||
int64_t layer_lo = 0, int64_t layer_hi = -1);
|
||||
/// Before the memory `session_init` carved is freed: forgets what points into it from outside the session (the
|
||||
/// rope kernels' registered angle table, #280), so a later session never rotates by freed memory.
|
||||
void session_release(SessionState& s);
|
||||
/// Zeroes every layer's state - the residual to `R_init`, everything else to zero, so a fresh sequence starts
|
||||
/// from the reference's own `zeros()`.
|
||||
void session_zero(SessionState& s, const ModelGeometry& g, const float* R_init, void* stream);
|
||||
|
||||
@@ -74,6 +74,9 @@ public:
|
||||
/// One window: `tokens[0..T)` at positions pos0.., the pool served per layer; `out[t]` = argmax after token t.
|
||||
/// The PLE rows are gathered here from `ss.ple_prev` and the tokens. Captures the T-token graph on first use.
|
||||
bool run(int T, const int32_t* tokens, int64_t pos0, PoolMultiFn pool, void* user, int32_t* out, std::string& err);
|
||||
/// Diagnostics: row `t` of the last window's head logits (n_vocab floats) to the host. Valid after run().
|
||||
bool copy_logits(int t, float* host) const;
|
||||
int64_t vocab() const { return next_ ? next_->vocab() : n_vocab_; }
|
||||
/// The sampling the verify window's head applies (temperature / top_p / top_k / seed). Set per
|
||||
/// request; greedy by default. The sampling itself runs OUTSIDE the captured graph - its
|
||||
/// parameters would otherwise be baked forever - so this can change between requests freely.
|
||||
@@ -116,6 +119,14 @@ public:
|
||||
|
||||
/// Keep the first `n_keep` (1..T) tokens of the last window; advances `ss.ple_prev` by them.
|
||||
bool commit(int n_keep, std::string& err);
|
||||
/// commit() returns without waiting for its graph (a single-GPU session sets it): the next window follows it on
|
||||
/// the same stream and the drafter reads nothing it writes, so it overlaps the draft. Whoever reads or writes
|
||||
/// the session from another stream or the host afterwards (a new request, a checkpoint, a snapshot, the prompt
|
||||
/// path, the end of a run) calls wait_commit() first. STRATA_COMMIT_SYNC=1 keeps the wait.
|
||||
static void set_commit_async(bool on);
|
||||
/// Waits for the last commit graph when commit() did not (an event recorded after it, not the whole device);
|
||||
/// false with `err` when it failed. Free when nothing is pending.
|
||||
bool wait_commit(std::string& err);
|
||||
|
||||
/// Measurement hook (STRATA_LOGPOS): after run(), write one line per row t of the last window's head -
|
||||
/// "pos target logprob top top_logprob hit extra_logprob target_logprob_without_extra" - where row t is the
|
||||
@@ -173,7 +184,8 @@ private:
|
||||
bool ple_stage() const { return lb_ <= 1 && 1 < le_; } ///< holds layer 1, where the PLE block runs
|
||||
bool capture_commit(std::string& err);
|
||||
bool record_window(int T, cudaStream_t cs, std::string& err);
|
||||
static constexpr int kProfPer = 32; // stamps per layer
|
||||
static constexpr int kProfPer = 33; // stamps per layer (32 left the hc-read second
|
||||
// half's up-stamp at slot 32 = the next layer's slot 0: D8)
|
||||
bool prof_on_ = false;
|
||||
unsigned long long* prof_ = nullptr; // device: n_layers * kProfPer + 4 stamps
|
||||
std::vector<unsigned long long> prof_h_;
|
||||
@@ -208,6 +220,8 @@ private:
|
||||
uint32_t* h_flag_ = nullptr; uint32_t* m_flag_ = nullptr;
|
||||
uint32_t* h_flagA_ = nullptr; uint32_t* m_flagA_ = nullptr; // the GPU plan is in place
|
||||
uint32_t* h_flagB_ = nullptr; uint32_t* m_flagB_ = nullptr; // the PCIe share's DMA copies have landed
|
||||
cudaEvent_t commit_done_ = nullptr; // recorded after an async commit (set_commit_async); see wait_commit
|
||||
bool commit_pending_ = false;
|
||||
cudaStream_t copy_ = nullptr; // the copy engine's stream (DMA of missed experts)
|
||||
struct FlagSet { uint32_t* flag; uint32_t value; };
|
||||
FlagSet flag_sets_[2 * 64 * 2] = {}; // host-function arguments, one per (layer, group)
|
||||
|
||||
@@ -113,6 +113,12 @@ public:
|
||||
static bool pool_bytes(const std::string& pack_dir, uint64_t& out, std::string& err,
|
||||
const std::set<std::string>* skip = nullptr);
|
||||
|
||||
/// The `code_bits` field of one row of `<pack_dir>/index.txt`, readable WITHOUT loading anything (0 = the
|
||||
/// pack stores the tensor unquantized, e.g. a --compat-bf16 key; -1 = no such row). #326: the loader has
|
||||
/// to know this before it builds the skip set.
|
||||
static bool index_code_bits(const std::string& pack_dir, const std::string& name, int& code_bits,
|
||||
std::string& err);
|
||||
|
||||
/// Load every tensor in `<pack_dir>/index.txt` into `arena_base`.
|
||||
///
|
||||
/// `arena_bytes` MUST be at least `pool_bytes`; the loader checks rather than trusting the caller,
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
#pragma once
|
||||
// Included only by STRATA_ENABLE_HIP builds. CUDA builds use NVIDIA headers.
|
||||
#if defined(_WIN32)
|
||||
// hip_runtime.h pulls in <windows.h> on Windows: keep its min/max macros and the rarely used APIs out of the engine.
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#endif
|
||||
#include <hip/hip_runtime.h>
|
||||
// Do not let HIP's legacy macro corrupt libstdc++ attribute names.
|
||||
#ifdef __noinline__
|
||||
@@ -9,6 +18,7 @@
|
||||
#define cudaDevAttrMultiProcessorCount hipDeviceAttributeMultiprocessorCount
|
||||
#define cudaDevAttrClockRate hipDeviceAttributeClockRate
|
||||
#define cudaDevAttrComputeCapabilityMajor hipDeviceAttributeComputeCapabilityMajor
|
||||
#define cudaDevAttrComputeCapabilityMinor hipDeviceAttributeComputeCapabilityMinor
|
||||
#define cudaDeviceGetAttribute hipDeviceGetAttribute
|
||||
#define cudaDeviceProp hipDeviceProp_t
|
||||
#define cudaDeviceSynchronize hipDeviceSynchronize
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
// CUDA device intrinsics used by Strata kernels that HIP does not provide on RDNA3 / RDNA4 (wave32).
|
||||
// CUDA device intrinsics used by Strata kernels that HIP does not provide on RDNA2 / RDNA3 / RDNA4 (wave32).
|
||||
// This header is included only from the HIP cuda_runtime compatibility shim.
|
||||
#if defined(__HIPCC__)
|
||||
|
||||
@@ -21,6 +21,10 @@ __device__ __forceinline__ int dp4a(int a, int b, int c) {
|
||||
// both packed operands signed to preserve CUDA __dp4a semantics; keep the
|
||||
// portable path for other HIP compilers/targets.
|
||||
return __builtin_amdgcn_sudot4(true, a, true, b, c, false);
|
||||
#elif (defined(__gfx1030__) || defined(__gfx1031__) || defined(__gfx1032__)) && __has_builtin(__builtin_amdgcn_sdot4)
|
||||
// RDNA2 has no sudot4 (that is gfx11+), but it has the plain signed v_dot4_i32_i8 (dot1-insts): the same
|
||||
// signed x signed byte products accumulated modulo 2^32, no clamp.
|
||||
return __builtin_amdgcn_sdot4(a, b, c, false);
|
||||
#else
|
||||
const uint32_t ua = static_cast<uint32_t>(a);
|
||||
const uint32_t ub = static_cast<uint32_t>(b);
|
||||
|
||||
@@ -63,13 +63,32 @@ struct ExpertJobMulti {
|
||||
const void* nact[MAXT] = {};
|
||||
};
|
||||
|
||||
/// How worker threads are allocated across physical/logical CPU cores (#272). `All` is the layout the pool has
|
||||
/// always used and the default; the hybrid-aware ones are opt-in (--pool-affinity auto|p-cores).
|
||||
enum class PoolAffinity {
|
||||
Auto, ///< Hybrid: prioritize physical P-cores, then SMT, then E-cores (defaults to P-core count)
|
||||
PCores, ///< Restrict workers strictly to Performance cores and their SMT siblings
|
||||
All, ///< The default: one worker per physical core in the OS's order, without hybrid distinction
|
||||
};
|
||||
|
||||
struct CpuTopology {
|
||||
bool is_hybrid = false;
|
||||
int p_cores = 0; ///< Physical performance cores
|
||||
int p_threads = 0; ///< Total logical threads on performance cores
|
||||
int e_cores = 0; ///< Efficient cores
|
||||
std::vector<int> worker_cores; ///< Ordered logical core IDs for workers (excluding host if skip_first)
|
||||
int host_core = -1; ///< Logical core reserved for host thread
|
||||
};
|
||||
|
||||
CpuTopology detect_cpu_topology(bool skip_first, PoolAffinity affinity = PoolAffinity::All);
|
||||
|
||||
/// One logical processor per PHYSICAL core, so a worker is never scheduled onto an SMT sibling of another
|
||||
/// worker. On the 6-core/12-thread machine this project measures on, `hardware_concurrency()/2` workers on
|
||||
/// logical processors 0..5 would put every worker on a sibling pair and halve the useful bandwidth - which is
|
||||
/// exactly the kind of error that shows up as "the CPU path is slower than the model says" with no clue why.
|
||||
///
|
||||
/// `skip_first` drops the first core, which P2.S3 reserves for the host loop.
|
||||
std::vector<int> physical_cores(bool skip_first);
|
||||
std::vector<int> physical_cores(bool skip_first, PoolAffinity affinity = PoolAffinity::All);
|
||||
|
||||
/// **THE RESERVATION IS A FICTION UNLESS THE HOST IS ACTUALLY PUT THERE.**
|
||||
///
|
||||
@@ -101,7 +120,8 @@ public:
|
||||
/// With `host_works`, `run()` claims jobs itself instead of spinning on `done_`, and the pool is six
|
||||
/// threads on six cores. `false` is the A/B arm and exists so the change is measurable rather than
|
||||
/// asserted - the counter it moves is `pool phases ... drain`, which is host-side and needs no profiler.
|
||||
explicit ExpertPool(int n_workers = 0, bool pin = true, bool host_works = true);
|
||||
explicit ExpertPool(int n_workers = 0, bool pin = true, bool host_works = true,
|
||||
PoolAffinity affinity = PoolAffinity::All);
|
||||
/// The watchdog's view of the pool (issue #31): the batch, the counters, every thread's state.
|
||||
void diag(std::FILE* f) const;
|
||||
~ExpertPool();
|
||||
@@ -113,6 +133,12 @@ public:
|
||||
/// is on" is only true if the engine says which adaptation it took.
|
||||
bool host_works() const { return host_works_; }
|
||||
|
||||
bool is_hybrid() const { return topo_.is_hybrid; }
|
||||
int p_cores() const { return topo_.p_cores; }
|
||||
int p_threads() const { return topo_.p_threads; }
|
||||
int e_cores() const { return topo_.e_cores; }
|
||||
PoolAffinity affinity() const { return affinity_; }
|
||||
|
||||
/// Publish `n` jobs, then block until every one has been claimed AND every worker has parked.
|
||||
/// `jobs` must outlive the call (it does, and the workers never touch it afterwards).
|
||||
void run(ExpertJob* jobs, int n);
|
||||
@@ -239,6 +265,8 @@ private:
|
||||
};
|
||||
const NativeFmt* nfmt_ = nullptr;
|
||||
std::vector<SplitBufMulti> split_multi_;
|
||||
PoolAffinity affinity_ = PoolAffinity::All;
|
||||
CpuTopology topo_;
|
||||
};
|
||||
|
||||
} // namespace strata::kernels::cpu
|
||||
@@ -49,4 +49,17 @@ constexpr int kFusedGrMaxT = 8;
|
||||
void 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);
|
||||
|
||||
/// The multi read's variants (#315; not main's opt-in STRATA_GR_V3 read, which sums in another order), all computing
|
||||
/// every output with the plain read's operations in its order, so bitwise the plain read's and the single-token
|
||||
/// read's: plain (0.1.31's default: the norm one block per token, the down projection on 41 blocks), split (the norm
|
||||
/// one block per token and stream, then the plain down projection) and staged (split's norm, and the down
|
||||
/// projection's activations staged ahead by cp.async, two half-stream tiles in flight, bank-conflict-free).
|
||||
/// `fused_gr_check` runs all of them and the single-token read on the current card with random weights and inputs
|
||||
/// (1..8 tokens, with and without the pending write) and from then on uses there the newest one that agrees with the
|
||||
/// plain read bit for bit; STRATA_HC_SPLIT=0 keeps the plain read, =1 stops at split. It runs once per card
|
||||
/// (Verifier::init calls it) and prints which one runs. On a card it has not checked, `fused_gr_variant` is the
|
||||
/// plain read unless STRATA_HC_SPLIT=1 or 2 names a variant.
|
||||
void fused_gr_check();
|
||||
int fused_gr_variant();
|
||||
|
||||
} // namespace strata::kernels
|
||||
@@ -14,6 +14,8 @@ namespace strata::kernels {
|
||||
|
||||
/// ggml type ids handled here.
|
||||
bool iq_supported(int ggml_type) noexcept;
|
||||
/// The token-embedding types iq_embed_rows and iq_dequant_f32 read: the i-quants above and BF16 (30).
|
||||
bool embed_type_supported(int ggml_type) noexcept;
|
||||
/// Bytes of one row of `n` values of `ggml_type` (n a multiple of the type's block).
|
||||
size_t iq_row_bytes(int ggml_type, int64_t n) noexcept;
|
||||
|
||||
|
||||
@@ -16,18 +16,46 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "strata/kernels/rope_scaling.hpp"
|
||||
|
||||
namespace strata::kernels {
|
||||
|
||||
/// The table of the CURRENT device (a layer split sets one per device; null = the identity).
|
||||
void mrope_table_set(const int32_t* device_table);
|
||||
const int32_t* mrope_table();
|
||||
|
||||
/// #280, opt-in (STRATA_ROPE_TABLE=1): the rotation angles, 32 pairs of a 64-wide rotary slice, from the session's
|
||||
/// float64 table (build_rope_table, [max_pos][32] cos and sin in VRAM, the rope scaling inside) of the CURRENT
|
||||
/// device. With it the rope kernels rotate by the table's exact angles; without it they compute pos * powf(...) with cosf/sinf, which under
|
||||
/// --use_fast_math is 0.0014 rad off at 32K and ~0.02 at 262K, and the prompt path (precise libm) and decode
|
||||
/// (fast-math) disagree with each other.
|
||||
struct RopeTab {
|
||||
const float* cos = nullptr; ///< null: no table, the kernels compute the angle as they always did
|
||||
const float* sin = nullptr;
|
||||
int max_pos = 0;
|
||||
};
|
||||
/// Registers the table the session just built with `scaling` (the latest one of the device wins).
|
||||
void rope_table_set(const float* cos_tab, const float* sin_tab, int max_pos, const RopeScaling& scaling);
|
||||
/// Forgets the table `cos_tab` (on every device) - before the memory that holds it is freed (session_release).
|
||||
void rope_table_release(const float* cos_tab);
|
||||
/// The registered table when STRATA_ROPE_TABLE=1 and it was built with `scaling`; otherwise none (the default: the
|
||||
/// kernels compute the angle exactly as before).
|
||||
RopeTab rope_table_for(const RopeScaling& scaling);
|
||||
|
||||
#if defined(__CUDACC__) || defined(__HIPCC__)
|
||||
/// ggml rope_multi, is_imrope, sections {11, 11, 10, 0}: sector = pair % 32; sector % 3 == 1 -> h (sector < 33),
|
||||
/// == 2 -> w (sector < 30), == 0 -> t (sector < 33). For pairs 0..31 all three bounds hold, so it is pair % 3.
|
||||
__device__ __forceinline__ int mrope_pos(const int32_t* tab, int pos, int pair) {
|
||||
return tab ? __ldg(tab + (size_t) pos * 3 + pair % 3) : pos;
|
||||
}
|
||||
/// cos and sin of rotary position p, pair `pair` (0..31) from the table (scaling and magnitude included); false
|
||||
/// when there is no table or p lies past it - then the caller computes the angle as it always did
|
||||
__device__ __forceinline__ bool rope_tab_cs(const RopeTab& t, int p, int pair, float& c, float& s) {
|
||||
if (t.cos == nullptr || p < 0 || p >= t.max_pos) return false;
|
||||
c = __ldg(t.cos + (size_t) p * 32 + pair);
|
||||
s = __ldg(t.sin + (size_t) p * 32 + pair);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace strata::kernels
|
||||
@@ -44,7 +44,9 @@ inline constexpr float NG_RMS_EPS = 1e-6f;
|
||||
// The table: [160, 320001536] IQ4_NL. ne0 = 160 is the FAST axis, so one row is 160 contiguous elements =
|
||||
// 5 blocks of 32 at 18 bytes = 90 bytes. The head-slowest flatten then makes 16 rows exactly n_embd = 2560.
|
||||
inline constexpr uint64_t PLE_TABLE_ROWS = 320001536ull;
|
||||
inline constexpr int PLE_ROW_BYTES = (PLE_HEAD_DIM / 32) * 18; // 90
|
||||
inline constexpr int PLE_ROW_BYTES = (PLE_HEAD_DIM / 32) * 18; // 90: an IQ4_NL row
|
||||
inline constexpr int PLE_ROW_BYTES_FP8 = PLE_HEAD_DIM; // 160: an F8_E4M3 row, one byte a value
|
||||
inline constexpr int PLE_ROW_BYTES_MAX = PLE_ROW_BYTES_FP8;
|
||||
|
||||
/// The artifact's own hash constants, transcribed from `docs/gguf-dump-shard1.txt`:
|
||||
///
|
||||
@@ -96,6 +98,11 @@ int iq4nl_code(int code);
|
||||
/// a perfectly plausible embedding of the wrong 160 values.
|
||||
void iq4nl_dequant_row(const uint8_t* row, float* out160);
|
||||
|
||||
/// One FP8 row -> 160 floats: each byte an E4M3 value (the "fn" variant: no infinities, 0x7F/0xFF are NaN), times the
|
||||
/// table's one scale. This is the table as Qwen3.8-Flash-Next ships it (`...ngram_embedding.shard_k`, F8_E4M3, and
|
||||
/// `weight_scale`), kept byte for byte by tools/ple_fp8_pack.py; IQ4_NL is 8% off it per row.
|
||||
void fp8_e4m3_dequant_row(const uint8_t* row, float scale, float* out160);
|
||||
|
||||
/// How the table's rows are read (plan v0.3 P2). `Direct` is the default: unbuffered 4 KiB reads from the SSD,
|
||||
/// so the table never occupies RAM or the OS file cache. `Mmap` is the earlier memory-mapped path, kept as the
|
||||
/// A/B arm; it returns the same bytes.
|
||||
@@ -113,6 +120,11 @@ struct PleIoOptions {
|
||||
/// Mmap mode only (`--ple-io ram`): lock the whole mapped table in RAM at open, so no SSD read ever sits on
|
||||
/// the prompt or token path. Needs RAM for the full table. POSIX only (mlock); `locked()` reports the outcome.
|
||||
bool lock = false;
|
||||
/// Direct mode with the I/O worker only: keep the SSD awake while rows are asked for - one page of the table
|
||||
/// after this long without a read (0 = off), until `keepalive_window_s` after the last request for rows
|
||||
/// (see PleReader::set_keepalive).
|
||||
double keepalive_ms = 0;
|
||||
double keepalive_window_s = 60;
|
||||
};
|
||||
|
||||
/// The PLE table. Held by pointer-to-impl so this header does not drag `<windows.h>` into every
|
||||
@@ -150,6 +162,8 @@ public:
|
||||
/// True when `PleIoOptions::lock` was asked for and mlock succeeded (false: pages only pre-touched).
|
||||
bool locked() const;
|
||||
uint64_t rows() const;
|
||||
/// "IQ4_NL" or "F8_E4M3" (a GGUF from tools/ple_fp8_pack.py: type I8, strata.ple.format = f8_e4m3).
|
||||
const char* format() const;
|
||||
|
||||
/// 16 row indices -> 2560 floats. The gathered rows are flattened HEAD-SLOWEST: row h's 160 values
|
||||
/// occupy `out[h*160, (h+1)*160)`, which is what `ggml_get_rows` does and what makes the result a plain
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
// a chunk it may already hold a block completed later, so it is not read).
|
||||
// qsa_block_topk : one block per query; a 4-pass radix select over the query's n_bid + 1 blocks, each
|
||||
// weighted by its cell count, then the cells emitted in ascending order with ties to the
|
||||
// lowest index - the same selection as topk_kernel, over a quarter of the elements.
|
||||
// lowest index - the same selection as topk_kernel, over a quarter of the elements. On sm_90+
|
||||
// a call of up to 16 queries runs it on a cluster of 8 CTAs per query: the same ids.
|
||||
//
|
||||
// Queries carry their own step record (pos, n_kv, n_bid, width) as everywhere else in QSA.
|
||||
#pragma once
|
||||
@@ -30,12 +31,22 @@ void qsa_block_scores(const float* pooled, const float* dead, const float* q_idx
|
||||
|
||||
/// The same scores on tensor cores (3xTF32, FP32-level accuracy but another summation order: not bitwise; the tail
|
||||
/// block n_bid is the warp kernel's arithmetic). For the prompt path; false (nothing launched) on another geometry.
|
||||
/// On AMD it is the gfx12 (RDNA4) WMMA kernel (a three-way bf16 split, six products); false on any other AMD target.
|
||||
bool qsa_block_scores_tc(const float* pooled, const float* dead, const float* q_idx, const int32_t* steps, int64_t nq,
|
||||
int64_t max_blocks, const QsaShapes& s, float* scores, void* stream, int64_t active_blocks);
|
||||
|
||||
/// ids [nq, cap] (cells, ascending); `cap` >= the largest selection width.
|
||||
void qsa_block_topk(const float* scores, const int32_t* steps, int64_t nq, int64_t max_blocks, int64_t cap,
|
||||
const QsaShapes& s, int32_t* ids, void* stream);
|
||||
const QsaShapes& s, int32_t* ids, void* stream,
|
||||
int64_t active_blocks = -1); ///< > 0: no query of the call has more than this many blocks (n_bid + 1;
|
||||
///< the same contract as qsa_block_scores's). The register kernel is
|
||||
///< chosen by this, not by the capacity max_blocks (a long
|
||||
///< --max-context otherwise sends every short prompt to the slow one)
|
||||
/// The same ids on a thread-block cluster of 8 CTAs per query (sm_90+, CUDA; S19). qsa_block_topk takes it for calls
|
||||
/// of up to 16 queries unless STRATA_QSA_CLUSTER=0. False (nothing launched) where it cannot run: HIP, a card or a
|
||||
/// build below sm_90, or a capacity whose keys do not fit one cluster's shared memory. Capturable.
|
||||
bool qsa_block_topk_cluster(const float* scores, const int32_t* steps, int64_t nq, int64_t max_blocks, int64_t cap,
|
||||
const QsaShapes& s, int32_t* ids, void* stream);
|
||||
/// The original kernel (keys read from memory on every radix pass), for tests: the same ids.
|
||||
void qsa_block_topk_ref(const float* scores, const int32_t* steps, int64_t nq, int64_t max_blocks, int64_t cap,
|
||||
const QsaShapes& s, int32_t* ids, void* stream);
|
||||
|
||||
@@ -17,4 +17,10 @@ namespace strata::kernels {
|
||||
void router_top10(const float* logits, int n_tokens, int n_expert, int k, int* ids, float* weights,
|
||||
void* stream);
|
||||
|
||||
/// Tests (S6, HIP only): one router kernel by name - 0 the portable kernel, 1 the AMD fast kernel (the default there
|
||||
/// for 64 < n_expert <= 512, k <= 32; bitwise the same results), 2 the fast kernel forced onto its serial-sum path.
|
||||
/// False (nothing launched) on CUDA builds or outside that geometry.
|
||||
bool router_top10_variant(const float* logits, int n_tokens, int n_expert, int k, int* ids, float* weights,
|
||||
void* stream, int variant);
|
||||
|
||||
} // namespace strata::kernels
|
||||
@@ -42,6 +42,11 @@ struct SamplerParams {
|
||||
void sample_tokens(const float* logits, int n_tokens, int n_vocab, const int* history, int history_len,
|
||||
const SamplerParams& p, int* out, void* stream);
|
||||
|
||||
// The greedy pick (no penalties) on a thread-block cluster of 8 CTAs per row (sm_90+, CUDA; S19): the same token as
|
||||
// sample_tokens' one-block argmax, which takes it unless STRATA_ARGMAX_MULTI=0. False (nothing launched) where it
|
||||
// cannot run: HIP, a card or a build below sm_90. Device pointers; capturable.
|
||||
bool sample_greedy_cluster(const float* logits, int n_tokens, int n_vocab, int* out, void* stream);
|
||||
|
||||
// ---- COUPLED DRAFT SAMPLING (include/strata/core/coupled_draft.hpp, STRATA_SPEC_COUPLED=1). Device pointers
|
||||
// throughout; every per-request / per-round value comes from device memory, so the calls can be captured.
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// include/strata/ngram/ple_reader.hpp - plan v0.3 P2: the n-gram table read straight from the SSD.
|
||||
//
|
||||
// The table is 320,001,536 rows of 90 bytes (26.8 GiB) and is never held in RAM: every row comes from an
|
||||
// The table is 320,001,536 rows of 90 bytes (IQ4_NL, 26.8 GiB) or 160 (FP8 E4M3, 51.2 GB) and is never held in RAM: every row comes from an
|
||||
// unbuffered 4 KiB read (platform::DirectFile). A token needs 16 rows on 16 different pages, and all 16 depend
|
||||
// on the token itself, so the only time to hide them is the embedding plus layer 0. Hence the split API:
|
||||
//
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
namespace strata::ngram {
|
||||
|
||||
inline constexpr uint32_t ROW_BYTES = 90;
|
||||
inline constexpr uint32_t ROW_BYTES = 90; ///< an IQ4_NL row; `open` takes the table's own size
|
||||
inline constexpr uint32_t PAGE = 4096;
|
||||
|
||||
struct ReaderStats {
|
||||
@@ -37,6 +37,8 @@ struct ReaderStats {
|
||||
double submit_us = 0; ///< time spent inside the read submission call (non-zero = it blocks)
|
||||
double read_us_sum = 0; ///< sum of per-read latencies (issue to completion)
|
||||
uint64_t late_injected = 0; ///< reads delayed by fault injection
|
||||
uint64_t keepalive_reads = 0; ///< pages read only to keep the SSD awake (not in `reads`, `bytes` or latencies)
|
||||
double keepalive_us_max = 0; ///< the slowest of them
|
||||
std::vector<float> read_us; ///< last <= 65,536 read latencies, for percentiles
|
||||
double percentile(double q) const;
|
||||
};
|
||||
@@ -56,24 +58,36 @@ public:
|
||||
/// validated GGUF parse (PleTable::open checks the table exactly fills the file from there).
|
||||
/// `io_thread` (default): a worker thread submits and reaps reads, so `issue` costs the caller no ReadFile
|
||||
/// calls. false: the caller's thread does it (A/B arm).
|
||||
/// `row_bytes`: one row's size in the file (90 IQ4_NL, 160 FP8), at most one page.
|
||||
bool open(const std::string& path, uint64_t table_offset, uint64_t n_rows, uint32_t max_inflight,
|
||||
uint64_t cache_rows, std::string& err, bool io_thread = true);
|
||||
uint64_t cache_rows, std::string& err, bool io_thread = true, uint32_t row_bytes = ROW_BYTES);
|
||||
uint32_t row_bytes() const;
|
||||
void close();
|
||||
bool is_open() const;
|
||||
|
||||
/// Start fetching `n` rows; row i's 90 raw bytes land at `out_raw + 90 * i`. `out_raw` must stay valid
|
||||
/// until `collect` returns. Out-of-range rows produce 90 zero bytes (the mmap path's behaviour).
|
||||
/// Start fetching `n` rows; row i's raw bytes land at `out_raw + row_bytes * i`. `out_raw` must stay valid
|
||||
/// until `collect` returns. Out-of-range rows produce zero bytes (the mmap path's behaviour).
|
||||
Ticket issue(const uint32_t* rows, size_t n, uint8_t* out_raw);
|
||||
|
||||
/// Block until every row of the ticket is in `out_raw`. Returns false on an I/O error (message in `err`).
|
||||
bool collect(Ticket t, std::string& err);
|
||||
|
||||
/// Keep the SSD awake while the table is in use (io_thread mode only; call after `open`): when no read has
|
||||
/// gone out for `period_ms`, the worker reads one page of the table, until `window_s` after the last `issue`.
|
||||
/// Some SSDs drop into a power state after ~250 ms without a command and stall the next reads 50-150 ms
|
||||
/// (a WD_BLACK SN7100 on Windows 11; the NVMe idle timeouts of the power plan did not change it), which in
|
||||
/// decode happens after a few rounds whose rows all came from the row cache. 0 turns it off (the reader's
|
||||
/// default; generate turns it on, see STRATA_SSD_KEEPALIVE). A keep-alive read that fails turns it off.
|
||||
void set_keepalive(double period_ms, double window_s);
|
||||
|
||||
/// Fault injection for tests and for the plan's P2 exit check: every read completes no earlier than
|
||||
/// `delay_us` after it was issued. 0 disables.
|
||||
void set_injected_delay_us(double delay_us);
|
||||
|
||||
/// Read with no ticket in flight (the worker updates these while reads are outstanding).
|
||||
/// Read with no ticket in flight (the worker updates these while reads are outstanding; with the keep-alive
|
||||
/// on it may also be counting a keep-alive read - `snapshot` takes a consistent copy).
|
||||
const ReaderStats& stats() const;
|
||||
ReaderStats snapshot() const;
|
||||
void reset_stats();
|
||||
uint64_t cache_capacity() const;
|
||||
uint64_t cache_size() const;
|
||||
|
||||
@@ -14,21 +14,24 @@ namespace strata::prefill {
|
||||
|
||||
// ---- hyper-connection (n_embd 2560, hc 4, hc_lr 320)
|
||||
/// xn[t, c*2560 + d] = R[t,c,d] * rsqrt(mean_d R[t,c,:]^2 + eps) * w_norm[c*2560 + d]; also its BF16 image.
|
||||
void gr_norm(const float* R, const float* w_norm, float eps, float* xn, uint16_t* xn16, int64_t T, void* stream);
|
||||
/// `xn16_lo` (null: none) takes bf16(x - xn16): W.xn16 + W.xn16_lo is the product with ~16 mantissa bits of x.
|
||||
void gr_norm(const float* R, const float* w_norm, float eps, float* xn, uint16_t* xn16, int64_t T, void* stream,
|
||||
uint16_t* xn16_lo = nullptr);
|
||||
/// F-1: gr_norm without its FP32 output: the row scales rs[t*4 + c] and the BF16 image; gr_mix_r then reads R.
|
||||
void gr_norm_rs(const float* R, const float* w_norm, float eps, float* rs, uint16_t* xn16, int64_t T, void* stream);
|
||||
void gr_norm_rs(const float* R, const float* w_norm, float eps, float* rs, uint16_t* xn16, int64_t T, void* stream,
|
||||
uint16_t* xn16_lo = nullptr);
|
||||
/// gr_mix with xn recomputed from R, rs and w_norm exactly as gr_norm computes it (the same bits).
|
||||
void gr_mix_r(const float* R, const float* rs, const float* w_norm, const float* gated, float* mixed, uint16_t* mixed16,
|
||||
int64_t T, void* stream, uint16_t* mixed_h = nullptr);
|
||||
int64_t T, void* stream, uint16_t* mixed_h = nullptr, uint16_t* mixed16_lo = nullptr);
|
||||
/// F-2: gr_write, then gr_norm_rs of the next half (its norm weights) over the rows just written - the same bits as
|
||||
/// the two calls, without reading R back.
|
||||
void gr_write_norm_rs(float* R, const float* bo, const float* inj, int64_t inj_ld, const float* w_norm_next, float eps,
|
||||
float* rs, uint16_t* xn16, int64_t T, void* stream);
|
||||
float* rs, uint16_t* xn16, int64_t T, void* stream, uint16_t* xn16_lo = nullptr);
|
||||
/// lo16[t, k] = bf16(silu(lo[t, k] / hc))
|
||||
void gr_silu(const float* lo, uint16_t* lo16, int64_t T, void* stream);
|
||||
void gr_silu(const float* lo, uint16_t* lo16, int64_t T, void* stream, uint16_t* lo16_lo = nullptr);
|
||||
/// mixed[t, d] = mean_c xn[t, c, d] * sigmoid(gated[t, c, d]); FP32, BF16 and FP16 (either image may be null).
|
||||
void gr_mix(const float* xn, const float* gated, float* mixed, uint16_t* mixed16, int64_t T, void* stream,
|
||||
uint16_t* mixed_h = nullptr);
|
||||
uint16_t* mixed_h = nullptr, uint16_t* mixed16_lo = nullptr);
|
||||
/// R[t, c, d] += bo[t, d] * 2 sigmoid(inj[t, c] / hc) (inj has row stride inj_ld)
|
||||
void gr_write(float* R, const float* bo, const float* inj, int64_t inj_ld, int64_t T, void* stream);
|
||||
/// R[t, c, :] = e[t, :] for all four streams (the embedding broadcast).
|
||||
@@ -85,7 +88,7 @@ void kv_append(const float* K, const float* V, int64_t T, int64_t pos0, const in
|
||||
|
||||
/// fp32 -> fp16 bits and fp32 -> bf16, n elements (the two activation images of the prompt GEMMs).
|
||||
void to_f16(const float* x, uint16_t* y, int64_t n, void* stream);
|
||||
void to_bf16(const float* x, uint16_t* y, int64_t n, void* stream);
|
||||
void to_bf16(const float* x, uint16_t* y, int64_t n, void* stream, uint16_t* ylo = nullptr);
|
||||
/// y = fp32(fp16(x)): what an FP16 store of x would read back (the FP16 indexer-key experiment)
|
||||
void round_f16(const float* x, float* y, int64_t n, void* stream);
|
||||
/// Expert blob -> FP16 (Q2_0 values are exact in FP16).
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// include/strata/prefill/moe_fused.hpp - #136 (prompt speed): the routed experts of a Q2_0 pack in the prompt path on
|
||||
// Strata's own int8 tensor-core kernels, opt-in with STRATA_PF_FUSED=1 (NVIDIA sm_80 and newer; sm_75 and HIP: MMQ).
|
||||
//
|
||||
// The MMQ path (moe_mmq.hpp) per MoE layer: a host sync to group the (token, k) pairs by expert, a gather of each
|
||||
// expert into a 16-expert group buffer, then per group the q8_1 activations of every routed slot, gate/up, SwiGLU,
|
||||
// the q8_1 of H and down - five passes over per-slot buffers. Here:
|
||||
// - the activations are rounded to int8 once per TOKEN (not per slot), with a scale per 32 values;
|
||||
// - the grouping (counts, offsets, the rows of each expert, a list of 64-row tiles) runs on the GPU from the
|
||||
// router's ids, so the host never waits for the routing;
|
||||
// - one launch per batch of up to kMaxBatch experts reads each expert where it already is (a resident cache slot
|
||||
// or a slot of the streamed ring: a pointer per expert, no gather) - gate/up with SwiGLU and the int8 rounding
|
||||
// of H in its epilogue, then down into the per-slot rows the existing combine reads.
|
||||
// The 2-bit codes are unpacked in registers and multiplied with mma.sync m16n8k32 (s8 x s8 -> s32); Q2_0's offset
|
||||
// (w = d * (q - 1)) is folded into the activation blocks' sums. The numbers differ from MMQ's (another int8
|
||||
// rounding of the activations and of H) by about as much as MMQ differs from FP32 (tests/cuda/prefill_fused_moe_test).
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::prefill::fused {
|
||||
|
||||
/// Routed rows (token, k pairs of one expert) per tile.
|
||||
constexpr int kTileRows = 64;
|
||||
/// Experts per launch: their blob pointers travel in the launch's parameters (1 KB).
|
||||
constexpr int kMaxBatch = 128;
|
||||
|
||||
/// This build has the kernels (a CUDA build; HIP and builds without MMQ do not).
|
||||
bool built();
|
||||
/// built(), and the current device is sm_80 or newer (mma.sync with s8 operands at m16n8k32).
|
||||
bool available();
|
||||
/// available(), unless STRATA_PF_FUSED=0 (read once): the Q2_0 pack's fused experts, on by default since 0.1.36.
|
||||
bool enabled();
|
||||
/// STRATA_PF_FUSED=1 given explicitly, and available(): the native IQ packs' fused kernels (opt-in).
|
||||
bool requested();
|
||||
|
||||
/// Bytes of `rows` activation rows of `cols` values (a multiple of 64) in the kernels' int8 form: per 64 values 64
|
||||
/// codes and {d, c} of each half (c = -(1.5 * 2^23 + the half's code sum), what the epilogue adds back).
|
||||
size_t act_bytes(int64_t rows, int64_t cols);
|
||||
/// Bytes of the grouping tables for `n` routed rows over `n_expert` experts.
|
||||
size_t group_bytes(int64_t n, int n_expert);
|
||||
|
||||
/// x [rows][cols] FP32 -> `xa` (act_bytes(rows, cols)).
|
||||
void quantize_act(const float* x, int64_t rows, int64_t cols, void* xa, void* stream);
|
||||
|
||||
/// The routing `ids` [n = tokens * k] -> per expert its rows (counts, offsets, the 64-row tile list, in `scratch`,
|
||||
/// group_bytes(n, n_expert)), `slot[i]` = the row of pair i (the row of the per-slot outputs the combine reads) and
|
||||
/// `src[row]` = its token (i / k). The order of the rows within an expert is not fixed; no output depends on it.
|
||||
void group(const int32_t* ids, int64_t n, int k, int n_expert, void* scratch, int32_t* slot, int32_t* src,
|
||||
void* stream);
|
||||
|
||||
/// Experts [e0, e1) of one layer (e1 - e0 <= kMaxBatch); blob[e - e0]: the Strata Q2_0 blob of expert e on the
|
||||
/// device (16-byte aligned), read only for an expert with rows.
|
||||
struct Batch {
|
||||
int e0 = 0, e1 = 0;
|
||||
const uint8_t* blob[kMaxBatch] = {};
|
||||
};
|
||||
/// The batch's products: gate/up from `xa` (quantize_act of the layer's input, [tokens][2560]) through the rows
|
||||
/// `src` of `group`, SwiGLU, H as int8 into `ha` (act_bytes(n, 640)), down into `dm` [n][2560] FP32 at the rows of
|
||||
/// `group`. `n`: the layer's routed rows (tokens * k), which bounds the launch grid.
|
||||
void experts(const Batch& b, int n_expert, int64_t n, const void* scratch, const void* xa, const int32_t* src, void* ha,
|
||||
float* dm, void* stream);
|
||||
|
||||
} // namespace strata::prefill::fused
|
||||
@@ -0,0 +1,41 @@
|
||||
// include/strata/prefill/moe_fused_iq.hpp - #136: the fused int8 prompt experts (moe_fused.hpp) for the native GGUF
|
||||
// packs (tools/iq_pack.py: an expert is its raw GGUF slices [gate rows | up rows | down rows], formats per layer in
|
||||
// expert_layout().fmt), opt-in with the same STRATA_PF_FUSED=1 (NVIDIA sm_80 and newer; sm_75 and HIP: MMQ).
|
||||
//
|
||||
// The grouping on the GPU (fused::group), the per-expert pointers (fused::Batch: a cache slot or a ring slot, no
|
||||
// gather) and the launch shape are the Q2_0 path's. What differs is the load stage: the i-quant blocks are decoded to
|
||||
// int8 in shared memory before the mma (codebook and sign lookups, as llama.cpp's MMQ load_tiles does), with the
|
||||
// block's scale per 32 (or per 16: IQ2_XS, IQ2_S, multiplied at m16n8k16) beside them. The activations and H are
|
||||
// int8 per 32 values in their natural order here (quantize_act_native), not the Q2_0 path's permuted one.
|
||||
//
|
||||
// Covered: gate/up IQ2_XXS, IQ2_XS, IQ2_S, IQ3_XXS, IQ3_S, IQ4_XS; down Q2_0, IQ4_NL - every layer of the IQ2_XS,
|
||||
// IQ3_XXS and IQ3_S packs but the IQ1_M ones (which MMQ does not cover either). Other layers keep MMQ.
|
||||
#pragma once
|
||||
|
||||
#include "strata/prefill/moe_fused.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::prefill::fused {
|
||||
|
||||
/// One layer's native expert geometry (cpu::NativeFmt's fields the kernels read; n_embd 2560, n_ff 640).
|
||||
struct NativeGeom {
|
||||
int gu_type = -1, d_type = -1; ///< ggml types
|
||||
size_t gu_row = 0, d_row = 0; ///< bytes per gate/up row and per down row
|
||||
size_t up_off = 0, down_off = 0; ///< inside the blob
|
||||
};
|
||||
|
||||
/// enabled() (STRATA_PF_FUSED=1 on sm_80+), and the kernels cover this gate/up and down pair on this device.
|
||||
bool native_supported(int gu_type, int d_type);
|
||||
|
||||
/// x [rows][cols] FP32 -> `xa` (act_bytes(rows, cols)): int8 per 32 values in natural order, the native kernels' form.
|
||||
void quantize_act_native(const float* x, int64_t rows, int64_t cols, void* xa, void* stream);
|
||||
|
||||
/// experts() for native blobs: b.blob[e - e0] is the native blob of expert e (gate at 0, up at g.up_off, down at
|
||||
/// g.down_off; 2-byte aligned). `xa` from quantize_act_native; H in `ha` (act_bytes(n, 640)); down into `dm` at the
|
||||
/// rows of `group`.
|
||||
void experts_native(const Batch& b, const NativeGeom& g, int n_expert, int64_t n, const void* scratch, const void* xa,
|
||||
const int32_t* src, void* ha, float* dm, void* stream);
|
||||
|
||||
} // namespace strata::prefill::fused
|
||||
@@ -12,8 +12,13 @@ namespace strata::prefill::mmq {
|
||||
|
||||
/// This build has the MMQ path (the ggml sources were available to the build).
|
||||
bool built();
|
||||
/// MMQ covers this ggml type (the i-quants and Q2_0 the packs use; IQ1_M is not covered).
|
||||
/// MMQ covers this ggml type (the i-quants and Q2_0 the packs use, Q8_0, and in a CUDA build with STRATA_MMQ_KQUANTS
|
||||
/// the Q4_K / Q5_K / Q5_1 of Unsloth's UD-Q4_K_XL; IQ1_M is not covered).
|
||||
bool supported(int ggml_type);
|
||||
/// #420: `supported`, and on every visible GPU llama.cpp's MMQ has a tile for this type and a weight matrix of
|
||||
/// `w_rows` rows that fits the card's shared memory - the same test its tile choice makes, which aborts the process
|
||||
/// ("J_best=0") when nothing fits. false (said once per type) keeps that product on the non-MMQ path.
|
||||
bool fits(int ggml_type, int64_t w_rows);
|
||||
/// Bytes of one expert's gate+up ([2*n_ff, n_embd]) or down ([n_embd, n_ff]) weights in `ggml_type`.
|
||||
size_t matrix_bytes(int ggml_type, int64_t rows, int64_t cols);
|
||||
/// Bytes of `rows` activation rows of `cols` values quantized for MMQ (the row padded to 512 values).
|
||||
|
||||
@@ -73,6 +73,9 @@ public:
|
||||
/// ring (a big one only pays when the copy engine, not the host copies, is the limit); set before bytes_needed.
|
||||
static void set_pinned_share(double share);
|
||||
static double pinned_share();
|
||||
/// #340: the streamed ring's slot count for chunks that stream every expert, instead of the pinned-share rule
|
||||
/// (0 = that rule). Set before any `bytes_needed`/`init` (both count the ring); STRATA_PREFILL_RING still wins.
|
||||
static void set_ring_override(int slots);
|
||||
|
||||
/// Device bytes `init` needs for a chunk of `chunk` tokens (what a borrowed region must hold).
|
||||
static uint64_t bytes_needed(const core::ModelGeometry& g, const core::SessionState& ss, int64_t chunk);
|
||||
|
||||
+40
-6
@@ -25,12 +25,17 @@ from jinja2.sandbox import ImmutableSandboxedEnvironment
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ template
|
||||
class TemplateRequestError(jinja2.exceptions.TemplateError, ValueError):
|
||||
"""The template refused the request's messages (e.g. "No user query found in messages."): a ValueError, so the
|
||||
client gets a 400 with the template's message instead of a dropped connection (#365)."""
|
||||
|
||||
|
||||
class ChatTemplate:
|
||||
"""The model's chat template, rendered with the same Jinja settings as transformers' apply_chat_template."""
|
||||
|
||||
def __init__(self, path: str | Path):
|
||||
def raise_exception(message):
|
||||
raise jinja2.exceptions.TemplateError(message)
|
||||
raise TemplateRequestError(message)
|
||||
|
||||
def tojson(x, ensure_ascii=False, indent=None, separators=None, sort_keys=False):
|
||||
return json.dumps(x, ensure_ascii=ensure_ascii, indent=indent, separators=separators, sort_keys=sort_keys)
|
||||
@@ -138,10 +143,27 @@ def _late_system_to_user(messages: list[dict]) -> list[dict]:
|
||||
return [dict(m, role="user") if m.get("role") == "system" and i > 0 else m for i, m in enumerate(messages)]
|
||||
|
||||
|
||||
def _object_list(value, name: str) -> list[dict]:
|
||||
"""#460: a request's "messages" (or a message's "tool_calls") as a list of objects. Some clients send the array
|
||||
double-encoded, as a JSON string, which used to be iterated character by character and crashed on m.get: such a
|
||||
string is decoded. Anything that is still not a list of objects is a ValueError, which the server answers with
|
||||
a 400 naming the field. None (or no field) is an empty list, as a missing field always was."""
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
value = json.loads(value)
|
||||
except ValueError:
|
||||
raise ValueError(f"{name} must be a list of objects (a string was sent that is not JSON)") from None
|
||||
if not isinstance(value, list) or not all(isinstance(m, dict) for m in value):
|
||||
raise ValueError(f"{name} must be a list of objects")
|
||||
return value
|
||||
|
||||
|
||||
def openai_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dict]:
|
||||
"""OpenAI Chat Completions -> (template messages, template tools, template kwargs)."""
|
||||
messages = []
|
||||
for m in req.get("messages", []):
|
||||
for m in _object_list(req.get("messages"), "messages"):
|
||||
role = m.get("role")
|
||||
if role == "developer":
|
||||
role = "system"
|
||||
@@ -150,8 +172,10 @@ def openai_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dict]:
|
||||
out["reasoning_content"] = m["reasoning_content"]
|
||||
if m.get("tool_calls"):
|
||||
calls = []
|
||||
for c in m["tool_calls"]:
|
||||
for c in _object_list(m["tool_calls"], "tool_calls"):
|
||||
fn = c.get("function", c)
|
||||
if not isinstance(fn, dict):
|
||||
raise ValueError("tool_calls must be a list of objects (each with a \"function\" object)")
|
||||
args = fn.get("arguments")
|
||||
if isinstance(args, str): # the template requires a mapping, not a JSON string
|
||||
args = json.loads(args) if args.strip() else {}
|
||||
@@ -173,13 +197,15 @@ def openai_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dict]:
|
||||
return _late_system_to_user(messages), tools, kwargs
|
||||
|
||||
|
||||
def anthropic_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dict]:
|
||||
"""Anthropic Messages -> (template messages, template tools, template kwargs)."""
|
||||
def anthropic_to_messages(req: dict, think_unasked: bool = True) -> tuple[list[dict], list[dict] | None, dict]:
|
||||
"""Anthropic Messages -> (template messages, template tools, template kwargs). `think_unasked`: a request
|
||||
without "thinking", an effort or a budget gets the template's default (it thinks), as through 0.1.31; False
|
||||
renders it without thinking (#278, the config's "anthropic_thinking": "on_request")."""
|
||||
messages = []
|
||||
system = req.get("system")
|
||||
if system:
|
||||
messages.append({"role": "system", "content": _text_of(system)})
|
||||
for m in req.get("messages", []):
|
||||
for m in _object_list(req.get("messages"), "messages"):
|
||||
content = m.get("content")
|
||||
if isinstance(content, str):
|
||||
messages.append({"role": m["role"], "content": content})
|
||||
@@ -219,6 +245,14 @@ def anthropic_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dic
|
||||
kwargs.update(effort_kwargs(effort))
|
||||
elif isinstance(thinking, dict) and thinking.get("budget_tokens"):
|
||||
kwargs.update(budget_effort(thinking["budget_tokens"]))
|
||||
elif thinking is None and not req.get("reasoning_budget_tokens") and not think_unasked:
|
||||
# Opt-in (the config's "anthropic_thinking": "on_request"; the default thinks as 0.1.31 did, since a
|
||||
# client that never asks would otherwise lose the thinking on every turn). Anthropic's thinking is
|
||||
# opt-in there. Claude Code's helper calls (a session title, a topic check) ask for none
|
||||
# and allow a few dozen tokens, which the model otherwise spent thinking and answered with no text at all.
|
||||
# A config's reasoning_effort still applies: Service.with_shared sets output_config before this runs. A
|
||||
# request that gives its own reasoning_budget_tokens (#123) asks for thinking, so it thinks as before.
|
||||
kwargs["enable_thinking"] = False
|
||||
return _late_system_to_user(messages), tools, kwargs
|
||||
|
||||
|
||||
|
||||
+673
-79
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,147 @@
|
||||
"""JSON response formats at the HTTP boundary: prompt once, validate before delivery.
|
||||
|
||||
The native engine has no grammar decoder. Failed generations are errors, never
|
||||
silently retried or returned as successful structured output.
|
||||
|
||||
`jsonschema` is optional (no new hard dependency of the server): json_object needs only the standard library, and
|
||||
json_schema is checked against its schema when the package is installed - without it, only that the answer is one
|
||||
JSON object, and the server says so once.
|
||||
"""
|
||||
import json
|
||||
import threading
|
||||
|
||||
|
||||
class StructuredOutputError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class _ObjectOnly:
|
||||
"""The validator for json_object, and for json_schema without jsonschema: one JSON object."""
|
||||
|
||||
def iter_errors(self, value):
|
||||
if not isinstance(value, dict):
|
||||
yield _ObjectError()
|
||||
|
||||
|
||||
class _ObjectError:
|
||||
absolute_path = ()
|
||||
message = "the answer is not a JSON object"
|
||||
|
||||
|
||||
_jsonschema = None # (validators, SchemaError, Registry, NoSuchResource), False when not installed
|
||||
_jsonschema_lock = threading.Lock()
|
||||
|
||||
|
||||
def jsonschema_modules():
|
||||
"""jsonschema, imported on first use; None when it is not installed (said once, in the server window)."""
|
||||
global _jsonschema
|
||||
with _jsonschema_lock:
|
||||
if _jsonschema is None:
|
||||
try:
|
||||
from jsonschema import validators
|
||||
from jsonschema.exceptions import SchemaError
|
||||
from referencing import Registry
|
||||
from referencing.exceptions import NoSuchResource
|
||||
_jsonschema = (validators, SchemaError, Registry, NoSuchResource)
|
||||
except ImportError:
|
||||
_jsonschema = False
|
||||
print('[strata] response_format json_schema: the Python package jsonschema is not installed, so '
|
||||
'answers are only checked to be one JSON object (python -m pip install "jsonschema>=4.23,<5")',
|
||||
flush=True)
|
||||
return _jsonschema or None
|
||||
|
||||
|
||||
def prepare_format(response_format, messages):
|
||||
if response_format is None:
|
||||
return messages, None
|
||||
if not isinstance(response_format, dict):
|
||||
raise ValueError("response_format must be an object")
|
||||
kind = response_format.get("type")
|
||||
if kind == "text":
|
||||
return messages, None
|
||||
if kind == "json_object":
|
||||
schema = {"type": "object"}
|
||||
modules = None # the standard library is enough for "one JSON object"
|
||||
elif kind == "json_schema":
|
||||
spec = response_format.get("json_schema")
|
||||
if not isinstance(spec, dict) or not isinstance(spec.get("schema"), dict):
|
||||
raise ValueError("response_format.json_schema needs a schema object")
|
||||
if not isinstance(spec.get("name"), str) or not spec["name"]:
|
||||
raise ValueError("response_format.json_schema needs a name")
|
||||
if "strict" in spec and not isinstance(spec["strict"], bool):
|
||||
raise ValueError("response_format.json_schema.strict must be boolean")
|
||||
schema = spec["schema"]
|
||||
if schema.get("type") != "object":
|
||||
raise ValueError("response_format schema must have type object at its root")
|
||||
modules = jsonschema_modules()
|
||||
else:
|
||||
raise ValueError("response_format.type must be text, json_object or json_schema")
|
||||
|
||||
def check_refs(node):
|
||||
if isinstance(node, dict):
|
||||
for key, value in node.items():
|
||||
if key in ("$ref", "$dynamicRef") and isinstance(value, str) and not value.startswith("#"):
|
||||
raise ValueError("response_format supports only local schema references (#...)")
|
||||
check_refs(value)
|
||||
elif isinstance(node, list):
|
||||
for value in node:
|
||||
check_refs(value)
|
||||
check_refs(schema)
|
||||
if modules is None:
|
||||
validator = _ObjectOnly()
|
||||
else:
|
||||
validators, SchemaError, Registry, NoSuchResource = modules
|
||||
|
||||
def no_remote(uri):
|
||||
raise NoSuchResource(ref=uri)
|
||||
|
||||
try:
|
||||
cls = validators.validator_for(schema)
|
||||
cls.check_schema(schema)
|
||||
validator = cls(schema, registry=Registry(retrieve=no_remote))
|
||||
except SchemaError as exc:
|
||||
raise ValueError(f"invalid response_format schema: {exc.message}") from exc
|
||||
directive = ("OUTPUT FORMAT REQUIREMENT: Return exactly one JSON object matching the JSON Schema below. "
|
||||
"No Markdown, headings, code fences, commentary, or text outside the JSON. "
|
||||
"Use every required field, correct types, and only allowed fields. "
|
||||
"Put all requested writing inside the appropriate JSON string fields.\nJSON Schema:\n" +
|
||||
json.dumps(schema, ensure_ascii=False, allow_nan=False, separators=(",", ":")))
|
||||
messages = [dict(message) for message in messages]
|
||||
if messages and messages[0].get("role") == "system":
|
||||
content = messages[0].get("content") or ""
|
||||
if isinstance(content, list):
|
||||
messages[0]["content"] = content + [{"type": "text", "text": directive}]
|
||||
else:
|
||||
messages[0]["content"] = content + "\n\n" + directive
|
||||
else:
|
||||
messages.insert(0, {"role": "system", "content": directive})
|
||||
return messages, validator
|
||||
|
||||
|
||||
def validated_json(text, validator, finish):
|
||||
def pairs(items):
|
||||
obj = {}
|
||||
for key, value in items:
|
||||
if key in obj:
|
||||
raise ValueError(f"duplicate JSON key: {key}")
|
||||
obj[key] = value
|
||||
return obj
|
||||
|
||||
def constant(value):
|
||||
raise ValueError(f"invalid JSON constant: {value}")
|
||||
|
||||
if finish != "stop":
|
||||
raise StructuredOutputError(f"structured output was incomplete (finish_reason={finish}); increase the output budget")
|
||||
try:
|
||||
value = json.loads(text or "", object_pairs_hook=pairs, parse_constant=constant)
|
||||
canonical = json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(",", ":"))
|
||||
except (ValueError, TypeError) as exc:
|
||||
raise StructuredOutputError(f"model did not return valid JSON: {exc}") from exc
|
||||
try:
|
||||
error = next(validator.iter_errors(value), None)
|
||||
except Exception as exc:
|
||||
raise StructuredOutputError(f"could not validate structured output: {exc}") from exc
|
||||
if error is not None:
|
||||
path = "/" + "/".join(str(part) for part in error.absolute_path)
|
||||
raise StructuredOutputError(f"model output failed the JSON Schema at {path}: {error.message}")
|
||||
return canonical
|
||||
+100
-4
@@ -2,7 +2,8 @@
|
||||
|
||||
A background thread samples once a second and keeps the last 60 readings of each series for the sparklines:
|
||||
- GPU: NVIDIA's own NVML library (nvml.dll / libnvidia-ml.so.1, installed with every driver) through ctypes, so no
|
||||
pip package is needed: load, VRAM, temperature, power, PCIe link and throughput.
|
||||
pip package is needed: load, VRAM, temperature, power, PCIe link and throughput. With the AMD backend (#301): the
|
||||
amdgpu driver's Linux sysfs files - load, VRAM, temperature and power.
|
||||
- CPU, RAM, disk: `psutil` when it is installed (setup installs it); without it the CPU and RAM readings fall back to
|
||||
the OS (Windows GlobalMemoryStatusEx / GetSystemTimes, Linux /proc) and the disk rate is absent.
|
||||
Anything that cannot be read is None; nothing here can stop the server.
|
||||
@@ -103,6 +104,100 @@ class _Nvml:
|
||||
return out
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ AMD (Linux sysfs)
|
||||
SYSFS = "/sys"
|
||||
|
||||
|
||||
def amd_device_dir(index, sysfs=None):
|
||||
"""The amdgpu sysfs folder (/sys/class/drm/renderD<N>/device) of the AMD GPU that HIP numbers `index`: the KFD
|
||||
topology's GPU nodes in order, the CPU nodes skipped, linked to their render node by drm_render_minor - the
|
||||
numbering setup's amd_gpus() and HIP_VISIBLE_DEVICES use. None when there is no such card (or no amdgpu)."""
|
||||
base = os.path.join(sysfs or SYSFS, "class", "kfd", "kfd", "topology", "nodes")
|
||||
try:
|
||||
nodes = sorted((n for n in os.listdir(base) if n.isdigit()), key=int)
|
||||
except OSError:
|
||||
return None
|
||||
gpus = []
|
||||
for n in nodes:
|
||||
try:
|
||||
with open(os.path.join(base, n, "properties"), encoding="utf-8") as f:
|
||||
props = dict(line.strip().partition(" ")[::2] for line in f if line.strip())
|
||||
if int(props.get("gfx_target_version") or 0) == 0 or int(props.get("simd_count") or 0) == 0:
|
||||
continue
|
||||
gpus.append(props)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
if not 0 <= index < len(gpus) or not gpus[index].get("drm_render_minor"):
|
||||
return None
|
||||
dev = os.path.join(sysfs or SYSFS, "class", "drm", "renderD" + gpus[index]["drm_render_minor"].strip(), "device")
|
||||
return dev if os.path.isdir(dev) else None
|
||||
|
||||
|
||||
class _Amd:
|
||||
"""#301: an AMD card's readings from the amdgpu driver's sysfs files (Linux; no ROCm library needed), with _Nvml's
|
||||
interface: load (gpu_busy_percent), VRAM (mem_info_vram_used / _total), and from its hwmon folder the temperature
|
||||
(temp1_input, the edge sensor, m°C), power (power1_average or power1_input, µW) and its cap (power1_cap)."""
|
||||
|
||||
def __init__(self, index=0, sysfs=None):
|
||||
self.dev = amd_device_dir(index, sysfs)
|
||||
self.hwmon = None
|
||||
if self.dev:
|
||||
try:
|
||||
hw = sorted(os.listdir(os.path.join(self.dev, "hwmon")))
|
||||
self.hwmon = os.path.join(self.dev, "hwmon", hw[0]) if hw else None
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def ok(self):
|
||||
return self.dev is not None
|
||||
|
||||
@staticmethod
|
||||
def _int(path):
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return int(f.read().strip())
|
||||
except (OSError, ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def name(self):
|
||||
try:
|
||||
with open(os.path.join(self.dev, "product_name"), encoding="utf-8") as f:
|
||||
return f.read().strip() or "AMD Radeon"
|
||||
except (OSError, TypeError):
|
||||
return "AMD Radeon"
|
||||
|
||||
def read(self):
|
||||
out = {"util": self._int(os.path.join(self.dev, "gpu_busy_percent")),
|
||||
"mem_used": self._int(os.path.join(self.dev, "mem_info_vram_used")),
|
||||
"mem_total": self._int(os.path.join(self.dev, "mem_info_vram_total"))}
|
||||
if self.hwmon:
|
||||
t = self._int(os.path.join(self.hwmon, "temp1_input"))
|
||||
out["temp"] = t / 1000.0 if t is not None else None
|
||||
p = self._int(os.path.join(self.hwmon, "power1_average"))
|
||||
if p is None:
|
||||
p = self._int(os.path.join(self.hwmon, "power1_input"))
|
||||
out["power"] = p / 1e6 if p is not None else None
|
||||
cap = self._int(os.path.join(self.hwmon, "power1_cap"))
|
||||
out["power_limit"] = cap / 1e6 if cap is not None else None
|
||||
return out
|
||||
|
||||
|
||||
def gpu_reader(index=0, amd=False):
|
||||
"""The card's readings: NVML (NVIDIA), or the amdgpu sysfs files with the AMD backend (#301)."""
|
||||
return _Amd(index) if amd else _Nvml(index)
|
||||
|
||||
|
||||
def free_vram_mib(index=0, amd=False):
|
||||
"""Free VRAM of a card in MiB, or None when it cannot be read."""
|
||||
g = gpu_reader(index, amd)
|
||||
if not g.ok():
|
||||
return None
|
||||
r = g.read()
|
||||
if r.get("mem_total") is None or r.get("mem_used") is None:
|
||||
return None
|
||||
return int((r["mem_total"] - r["mem_used"]) >> 20)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ CPU / RAM
|
||||
def _cpu_name():
|
||||
if os.name == "nt":
|
||||
@@ -169,17 +264,18 @@ class _CpuRamFallback:
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ the sampler
|
||||
class Telemetry:
|
||||
def __init__(self, extra=None, gpu_index=0, gpu_indices=None):
|
||||
def __init__(self, extra=None, gpu_index=0, gpu_indices=None, amd=False):
|
||||
"""`extra()` -> dict of more series to record each second (the server's tok/s). `gpu_index`: the card the
|
||||
engine runs on, numbered as nvidia-smi and NVML number them (by PCI bus); `gpu_indices`: all of them when
|
||||
the model is split across several (issue #112) - the gpu_* readings are then their total (memory, power,
|
||||
PCIe traffic), mean (load) or hottest (temperature), and "gpus" has each card's own."""
|
||||
PCIe traffic), mean (load) or hottest (temperature), and "gpus" has each card's own. `amd`: the AMD backend's
|
||||
cards, numbered as HIP numbers them, read from sysfs (#301)."""
|
||||
self.extra = extra
|
||||
self.lock = threading.Lock()
|
||||
self.now: dict = {}
|
||||
self.hist = collections.defaultdict(lambda: collections.deque(maxlen=HISTORY))
|
||||
idx = list(gpu_indices) if gpu_indices and len(gpu_indices) > 1 else [gpu_index]
|
||||
self.gpus = [(i, _Nvml(i)) for i in idx]
|
||||
self.gpus = [(i, gpu_reader(i, amd)) for i in idx]
|
||||
self.gpus = [(i, g) for i, g in self.gpus if g.ok()] or self.gpus[:1]
|
||||
self.gpu = self.gpus[0][1]
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
"""Model discovery, on-demand loading and safe unloading over the actual HTTP API."""
|
||||
import json
|
||||
import io
|
||||
import subprocess
|
||||
import time
|
||||
import unittest
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from serve.frontend import ChatTemplate
|
||||
from serve.server import ByteTokenizer, EngineDied, EngineStuck, MockEngine, Service, StrataEngine, serve
|
||||
|
||||
|
||||
class ResidentEngine(StrataEngine):
|
||||
def __init__(self, tok):
|
||||
self.reply = MockEngine(tok, "Hello.", max_context=4096)
|
||||
self.max_context, self.info, self.last = 4096, {}, {}
|
||||
self.loaded, self.starts, self.closes = False, 0, 0
|
||||
self.unloaded = True
|
||||
self.progress = None
|
||||
|
||||
def alive(self):
|
||||
return self.loaded
|
||||
|
||||
def exit_code(self):
|
||||
return None
|
||||
|
||||
def restart(self):
|
||||
self.loaded = True
|
||||
self.starts += 1
|
||||
self.unloaded = False
|
||||
|
||||
def close(self):
|
||||
self.loaded = False
|
||||
self.closes += 1
|
||||
|
||||
def unload(self):
|
||||
self.close()
|
||||
self.unloaded = True
|
||||
|
||||
def generate(self, *args, **kwargs):
|
||||
yield from self.reply.generate(*args, **kwargs)
|
||||
|
||||
|
||||
class Lifecycle(unittest.TestCase):
|
||||
def test_close_sends_eof_before_waiting_for_windows_reader(self):
|
||||
engine = StrataEngine("missing-executable", [], lazy=True)
|
||||
proc = mock.Mock()
|
||||
proc.stdin, proc.stdout = io.StringIO(), io.StringIO()
|
||||
proc.poll.side_effect = [None, 0]
|
||||
proc.wait.side_effect = lambda **kwargs: self.assertTrue(proc.stdin.closed)
|
||||
engine.proc = proc
|
||||
engine.close()
|
||||
self.assertIsNone(engine.proc)
|
||||
proc.kill.assert_not_called()
|
||||
|
||||
def test_close_does_not_forget_a_process_still_exiting(self):
|
||||
engine = StrataEngine("missing-executable", [], lazy=True)
|
||||
proc = mock.Mock()
|
||||
proc.stdin, proc.stdout = io.StringIO(), io.StringIO()
|
||||
proc.poll.return_value = None
|
||||
proc.wait.side_effect = subprocess.TimeoutExpired("engine", 2)
|
||||
engine.proc = proc
|
||||
with self.assertRaisesRegex(EngineStuck, "still releasing"):
|
||||
engine.close()
|
||||
self.assertIs(engine.proc, proc)
|
||||
proc.terminate.assert_called_once()
|
||||
proc.kill.assert_called_once()
|
||||
|
||||
def test_close_terminates_before_it_kills(self):
|
||||
# an engine that does not end on QUIT is terminated first (as the unload always did); kill is the last resort
|
||||
engine = StrataEngine("missing-executable", [], lazy=True)
|
||||
proc = mock.Mock()
|
||||
proc.stdin, proc.stdout = io.StringIO(), io.StringIO()
|
||||
proc.poll.side_effect = [None, 0]
|
||||
proc.wait.side_effect = [subprocess.TimeoutExpired("engine", 20), 0]
|
||||
engine.proc = proc
|
||||
engine.close()
|
||||
proc.terminate.assert_called_once()
|
||||
proc.kill.assert_not_called()
|
||||
self.assertIsNone(engine.proc)
|
||||
|
||||
def test_native_lazy_constructor_does_not_start_a_process(self):
|
||||
engine = StrataEngine("missing-executable", ["--max-context", "16384"], lazy=True)
|
||||
self.assertFalse(engine.alive())
|
||||
self.assertEqual(engine.max_context, 16384)
|
||||
self.assertIsNone(engine.exit_code())
|
||||
engine.close()
|
||||
|
||||
def setUp(self):
|
||||
tok = ByteTokenizer()
|
||||
self.engine = ResidentEngine(tok)
|
||||
self.svc = Service(self.engine, tok, ChatTemplate(Path(__file__).parent / "chat_template.jinja"))
|
||||
self.httpd = serve(self.svc, port=0)
|
||||
self.base = f"http://127.0.0.1:{self.httpd.server_address[1]}"
|
||||
|
||||
def tearDown(self):
|
||||
self.httpd.shutdown()
|
||||
self.httpd.server_close()
|
||||
|
||||
def request(self, path, body=None, headers=None):
|
||||
req = urllib.request.Request(self.base + path, data=json.dumps(body).encode() if body is not None else None,
|
||||
headers={"Content-Type": "application/json", **(headers or {})})
|
||||
try:
|
||||
response = urllib.request.urlopen(req, timeout=10)
|
||||
except urllib.error.HTTPError as error:
|
||||
response = error
|
||||
with response:
|
||||
data = response.read().decode()
|
||||
return response.status, data if "event-stream" in response.headers.get("Content-Type", "") else json.loads(data)
|
||||
|
||||
def test_discover_chat_unload_and_stream_reload(self):
|
||||
self.assertEqual(self.request("/api/health")[1]["service"], "strata")
|
||||
self.assertFalse(self.request("/v1/status")[1]["loaded"])
|
||||
self.assertEqual(self.request("/v1/models")[1]["data"][0]["status"]["value"], "unloaded")
|
||||
self.assertTrue(self.request("/props")[1]["models_autoload"])
|
||||
self.assertEqual(self.engine.starts, 0)
|
||||
chat = {"messages": [{"role": "user", "content": "Hello"}], "max_tokens": 32, "reasoning_effort": "none"}
|
||||
for _ in range(2):
|
||||
code, answer = self.request("/v1/chat/completions", chat)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(answer["choices"][0]["message"]["content"], "Hello.")
|
||||
self.assertEqual(self.engine.starts, 1)
|
||||
self.assertFalse(self.request("/v1/unload", {})[1]["loaded"])
|
||||
code, stream = self.request("/v1/chat/completions", {**chat, "stream": True})
|
||||
self.assertEqual(code, 200)
|
||||
self.assertIn("data: [DONE]", stream)
|
||||
self.assertEqual(self.engine.starts, 2)
|
||||
|
||||
def test_busy_auth_and_foreign_origin_cannot_unload(self):
|
||||
self.engine.loaded = True
|
||||
with self.svc.fifo:
|
||||
self.assertEqual(self.request("/v1/unload", {})[0], 409)
|
||||
self.svc.api_key = "local-secret"
|
||||
self.assertEqual(self.request("/v1/unload", {})[0], 401)
|
||||
self.assertEqual(self.request("/v1/unload", {}, {"Authorization": "Bearer local-secret",
|
||||
"Origin": "https://other.example"})[0], 403)
|
||||
self.assertEqual(self.engine.closes, 0)
|
||||
self.assertTrue(self.engine.loaded)
|
||||
self.assertEqual(self.request("/v1/unload", {}, {"Authorization": "Bearer local-secret"})[0], 200)
|
||||
|
||||
def test_an_engine_that_died_on_load_is_reported_as_such(self):
|
||||
# EngineDied is a RuntimeError: it keeps its own answer ("the next request restarts it")
|
||||
def died():
|
||||
raise EngineDied("the engine ended while it started")
|
||||
self.engine.restart = died
|
||||
code, body = self.request("/v1/chat/completions", {"messages": [{"role": "user", "content": "Hello"}]})
|
||||
self.assertEqual(code, 503)
|
||||
self.assertIn("the next request restarts it", body["error"]["message"])
|
||||
|
||||
def test_explicit_load_and_model_matching(self):
|
||||
self.assertEqual(self.request("/v1/load", {"model": "other-model"})[0], 404)
|
||||
self.assertEqual(self.engine.starts, 0)
|
||||
before = time.time()
|
||||
code, status = self.request("/v1/load", {"model": self.svc.model})
|
||||
self.assertEqual(code, 200)
|
||||
self.assertTrue(status["loaded"])
|
||||
self.assertEqual(self.engine.starts, 1)
|
||||
self.assertGreaterEqual(self.svc.last_request_at, before)
|
||||
self.svc.status["queued"] = 1
|
||||
self.assertEqual(self.request("/v1/load", {})[0], 409)
|
||||
self.assertEqual(self.request("/v1/unload", {})[0], 409)
|
||||
self.assertEqual(self.engine.closes, 0)
|
||||
self.assertEqual(self.request("/v1/load", {}, {"Content-Type": "text/plain"})[0], 415)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,198 @@
|
||||
"""Request inspection through HTTP; no native model or GPU is required."""
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from serve.frontend import ChatTemplate
|
||||
from serve.server import ByteTokenizer, EngineDied, MockEngine, Service, serve
|
||||
|
||||
|
||||
class ReloadableEngine(MockEngine):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.running, self.unloaded = True, False
|
||||
|
||||
def alive(self):
|
||||
return self.running
|
||||
|
||||
def unload(self):
|
||||
self.running, self.unloaded = False, True
|
||||
|
||||
def restart(self):
|
||||
self.running, self.unloaded = True, False
|
||||
|
||||
|
||||
class MonitorOff(unittest.TestCase):
|
||||
"""#332 is opt-in: by default no prompt or answer is kept, and the monitor's endpoints do not exist."""
|
||||
|
||||
def test_off_by_default(self):
|
||||
tok = ByteTokenizer()
|
||||
svc = Service(MockEngine(tok, "Hello.", max_context=4096), tok,
|
||||
ChatTemplate(Path(__file__).parent / "chat_template.jinja"))
|
||||
httpd = serve(svc, port=0)
|
||||
base = f"http://127.0.0.1:{httpd.server_address[1]}"
|
||||
try:
|
||||
req = urllib.request.Request(base + "/v1/chat/completions", headers={"Content-Type": "application/json"},
|
||||
data=json.dumps({"messages": [{"role": "user", "content": "secret"}]}).encode())
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
self.assertEqual(r.status, 200)
|
||||
self.assertEqual(len(svc.api_requests), 0)
|
||||
for path in ("/api/requests", "/api-monitor"):
|
||||
with self.assertRaises(urllib.error.HTTPError) as e:
|
||||
urllib.request.urlopen(base + path, timeout=10)
|
||||
self.assertEqual(e.exception.code, 404, path)
|
||||
e.exception.close()
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
httpd.server_close()
|
||||
|
||||
|
||||
class Monitor(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tok = ByteTokenizer()
|
||||
self.engine = ReloadableEngine(self.tok, "Thought.</think>\nHello <script>alert(1)</script>.", max_context=16384)
|
||||
self.svc = Service(self.engine, self.tok, ChatTemplate(Path(__file__).parent / "chat_template.jinja"))
|
||||
self.svc.api_monitor = True # opt-in ("api_monitor": true / --api-monitor)
|
||||
self.httpd = serve(self.svc, port=0)
|
||||
self.base = f"http://127.0.0.1:{self.httpd.server_address[1]}"
|
||||
|
||||
def tearDown(self):
|
||||
self.httpd.shutdown()
|
||||
self.httpd.server_close()
|
||||
|
||||
def request(self, path, body=None, headers=None):
|
||||
req = urllib.request.Request(self.base + path, data=json.dumps(body).encode() if body is not None else None,
|
||||
headers={"Content-Type": "application/json", **(headers or {})})
|
||||
try:
|
||||
response = urllib.request.urlopen(req, timeout=10)
|
||||
except urllib.error.HTTPError as error:
|
||||
response = error
|
||||
with response:
|
||||
raw = response.read().decode()
|
||||
return response.status, json.loads(raw) if "application/json" in response.headers.get("Content-Type", "") else raw
|
||||
|
||||
def chat(self, api="openai", **kwargs):
|
||||
# Anthropic thinks only when asked (#278): "thinking" there, reasoning_effort for OpenAI
|
||||
think = {"thinking": {"type": "enabled", "budget_tokens": 1024}} if api == "anthropic" else {"reasoning_effort": "low"}
|
||||
return self.request("/v1/messages" if api == "anthropic" else "/v1/chat/completions",
|
||||
{"messages": [{"role": "user", "content": "Say hello."}],
|
||||
"max_tokens": 256, **think, **kwargs})
|
||||
|
||||
def latest(self):
|
||||
summary = self.request("/api/requests")[1]["requests"][0]
|
||||
return self.request("/api/requests?id=" + summary["id"])[1]
|
||||
|
||||
def test_output_and_reasoning_for_both_dialects_and_stream_modes(self):
|
||||
for api in ("openai", "anthropic"):
|
||||
for stream in (False, True):
|
||||
with self.subTest(api=api, stream=stream):
|
||||
code, reply = self.chat(api, stream=stream)
|
||||
self.assertEqual(code, 200)
|
||||
record = self.latest()
|
||||
self.assertEqual(record["state"], "completed")
|
||||
self.assertEqual(record["http_status"], 200)
|
||||
self.assertIn("Say hello.", record["input"])
|
||||
self.assertIn("<script>alert(1)</script>", record["output"])
|
||||
self.assertIn("Thought.", record["reasoning"])
|
||||
self.assertGreater(record["usage"].get("completion_tokens", record["usage"].get("output_tokens", 0)), 0)
|
||||
self.assertGreaterEqual(record["wallclock_s"], record["first_token_s"])
|
||||
self.assertNotIn("_clock", record)
|
||||
if not stream:
|
||||
self.assertEqual(json.loads(record["response"]), reply)
|
||||
else:
|
||||
self.assertNotIn("response", record)
|
||||
|
||||
def test_error_and_engine_death_are_not_successes(self):
|
||||
self.assertEqual(self.chat(max_tokens=20000)[0], 400)
|
||||
self.assertEqual(self.latest()["state"], "error")
|
||||
for api in ("openai", "anthropic"):
|
||||
for stream in (False, True):
|
||||
with self.subTest(api=api, stream=stream), mock.patch.object(self.engine, "generate", side_effect=EngineDied("stopped")):
|
||||
self.chat(api, stream=stream)
|
||||
record = self.latest()
|
||||
self.assertEqual(record["state"], "error")
|
||||
self.assertIn("stopped", record["error"]["message"])
|
||||
self.assertEqual(record["output"], "")
|
||||
self.assertNotIn("timings", record)
|
||||
|
||||
def test_history_is_authorized_and_does_not_capture_headers(self):
|
||||
self.svc.api_key = "secret"
|
||||
auth = {"Authorization": "Bearer secret"}
|
||||
self.assertEqual(self.request("/api/requests")[0], 401)
|
||||
self.assertEqual(self.request("/api/requests?id=missing")[0], 401)
|
||||
self.assertEqual(len(self.svc.api_requests), 0)
|
||||
self.assertEqual(self.request("/v1/chat/completions", {"messages": [{"role": "user", "content": "hi"}]}, auth)[0], 200)
|
||||
summary = self.request("/api/requests", headers=auth)[1]["requests"][0]
|
||||
self.assertNotIn("input", summary)
|
||||
self.assertNotIn("output", summary)
|
||||
self.assertEqual(self.request("/api/requests?id=" + summary["id"])[0], 401)
|
||||
detail = self.request("/api/requests?id=" + summary["id"], headers=auth)[1]
|
||||
self.assertNotIn("secret", json.dumps(detail))
|
||||
|
||||
def test_capture_is_bounded_and_eviction_returns_404(self):
|
||||
huge = "x" * 262200
|
||||
first = self.svc.begin_request("/v1/chat/completions", {"messages": [{"content": huge}]})
|
||||
self.assertEqual(len(first["input"]), 262144)
|
||||
self.assertTrue(first["input_truncated"])
|
||||
for _ in range(100):
|
||||
self.svc.begin_request("/v1/chat/completions", {"messages": []})
|
||||
self.assertEqual(len(self.request("/api/requests")[1]["requests"]), 100)
|
||||
self.assertEqual(self.request("/api/requests?id=" + first["id"])[0], 404)
|
||||
# Exercise output capture without making a mock engine emit 262K tokens.
|
||||
handler = object.__new__(self.httpd.RequestHandlerClass)
|
||||
handler.record = self.svc.api_requests[-1]
|
||||
chunk = {"choices": [{"delta": {"content": huge, "reasoning_content": huge}}]}
|
||||
list(handler._capture((item for item in [chunk]), "openai"))
|
||||
for field in ("output", "reasoning"):
|
||||
self.assertEqual(len(handler.record[field]), 262144)
|
||||
self.assertTrue(handler.record[field + "_truncated"])
|
||||
|
||||
def test_queue_wait_and_loading_are_attributed_separately(self):
|
||||
self.engine.unload()
|
||||
entered, release = threading.Event(), threading.Event()
|
||||
original = self.engine.restart
|
||||
def restart():
|
||||
entered.set()
|
||||
self.assertTrue(release.wait(5))
|
||||
original()
|
||||
with mock.patch.object(self.engine, "restart", side_effect=restart), ThreadPoolExecutor(max_workers=2) as pool:
|
||||
first = pool.submit(self.chat)
|
||||
self.assertTrue(entered.wait(5))
|
||||
second = pool.submit(self.chat)
|
||||
deadline = time.monotonic() + 5
|
||||
while len(self.svc.api_requests) < 2 and time.monotonic() < deadline:
|
||||
time.sleep(0.01)
|
||||
self.assertEqual(len(self.svc.api_requests), 2)
|
||||
history = self.request("/api/requests")[1]
|
||||
self.assertEqual({r["state"] for r in history["requests"]}, {"queued", "loading"})
|
||||
self.assertFalse(history["loaded"])
|
||||
self.assertTrue(history["auto_load"])
|
||||
time.sleep(0.03)
|
||||
release.set()
|
||||
self.assertEqual(first.result()[0], 200)
|
||||
self.assertEqual(second.result()[0], 200)
|
||||
records = list(self.svc.api_requests)
|
||||
self.assertGreater(records[0]["load_s"], 0.02)
|
||||
self.assertGreater(records[1]["queue_s"], 0.02)
|
||||
self.assertLess(records[1]["load_s"], records[0]["load_s"])
|
||||
self.assertTrue(all(r["state"] == "completed" for r in records))
|
||||
|
||||
def test_page_and_relative_assets_are_served(self):
|
||||
code, html = self.request("/api-monitor")
|
||||
self.assertEqual(code, 200)
|
||||
self.assertIn('src="/web/monitor.js"', html)
|
||||
code, script = self.request("/web/monitor.js")
|
||||
self.assertEqual(code, 200)
|
||||
self.assertIn("textContent", script)
|
||||
self.assertNotIn("innerHTML", script)
|
||||
self.assertEqual(self.request("/web/../server.py")[0], 404)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+664
-1
@@ -14,10 +14,13 @@ import unittest
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from serve.frontend import ChatTemplate # noqa: E402
|
||||
from serve.server import CTX_SLACK, ByteTokenizer, EngineDied, GpuBusy, MockEngine, Service, StrataEngine, request_timings, serve # noqa: E402
|
||||
from serve.server import (CTX_SLACK, ByteTokenizer, EngineDied, GpuBusy, MockEngine, Service, StrataEngine, # noqa: E402
|
||||
engine_args, prompt_tokens_seen, request_timings, serve, start_failure_hint)
|
||||
from types import SimpleNamespace # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CTX = 4096
|
||||
@@ -65,6 +68,43 @@ class MaxTokens(unittest.TestCase):
|
||||
u = b.get("usage", {})
|
||||
return s, b, u.get("input_tokens"), u.get("output_tokens")
|
||||
|
||||
def test_anthropic_thinks_only_when_asked(self):
|
||||
# #278: Anthropic's thinking is opt-in; "thinking", an effort or a reasoning_budget_tokens (#123) asks for it
|
||||
from serve.frontend import anthropic_to_messages
|
||||
msgs = [{"role": "user", "content": "u"}]
|
||||
kw = lambda **r: anthropic_to_messages({"messages": msgs, **r}, think_unasked=False)[2] # noqa: E731
|
||||
# the default ("anthropic_thinking": "model") renders an unasked request as 0.1.31 did
|
||||
self.assertNotIn("enable_thinking", anthropic_to_messages({"messages": msgs})[2])
|
||||
self.assertEqual(kw(), {"enable_thinking": False})
|
||||
self.assertEqual(kw(thinking={"type": "disabled"}), {"enable_thinking": False})
|
||||
self.assertNotIn("enable_thinking", kw(thinking={"type": "enabled", "budget_tokens": 2048}))
|
||||
self.assertNotIn("enable_thinking", kw(output_config={"effort": "high"}))
|
||||
self.assertNotIn("enable_thinking", kw(reasoning_budget_tokens=30))
|
||||
|
||||
def test_count_tokens_is_the_prompt_messages_reads(self):
|
||||
# /v1/messages/count_tokens renders and tokenizes the same prompt /v1/messages would read, without running it
|
||||
msgs = [{"role": "user", "content": "how many tokens is this?"}]
|
||||
s, b = self.post("/v1/messages/count_tokens", {"model": "m", "messages": msgs})
|
||||
self.assertEqual(s, 200)
|
||||
s2, _, n_in, _ = self.call("anthropic", "how many tokens is this?", max_tokens=8)
|
||||
self.assertEqual(s2, 200)
|
||||
self.assertEqual(b["input_tokens"], n_in)
|
||||
|
||||
def test_request_line_parses_the_engine_summary(self):
|
||||
line = ("strata serve: prompt 1200 tokens = 1000 reused + 200 read in 50 ms (4000.0 tok/s), 30 generated in "
|
||||
"300 ms (100.0 tok/s), drafts accepted 20 of 28, 2 checkpoints")
|
||||
from serve.server import ENGINE_REQUEST
|
||||
m = ENGINE_REQUEST.search(line)
|
||||
self.assertIsNotNone(m)
|
||||
self.assertEqual((m["prompt"], m["reused"], m["gen"], m["tg"]), ("1200", "1000", "30", "100.0"))
|
||||
# #471: a request cancelled while its prompt was read says how far it got
|
||||
m = ENGINE_REQUEST.search("strata serve: prompt 98179 tokens = 0 reused + 12288 of 98179 read in 17565 ms "
|
||||
"(699.6 tok/s), 0 generated in 0 ms (0.0 tok/s), drafts accepted 0 of 0, "
|
||||
"0 checkpoints (cancelled)")
|
||||
self.assertIsNotNone(m)
|
||||
self.assertEqual((m["prompt"], m["reused"], m["read"], m["pp"], m["gen"]),
|
||||
("98179", "0", "17565", "699.6", "0"))
|
||||
|
||||
def test_unset_budget_is_the_rest_of_the_context(self):
|
||||
cases = {"openai": [{"max_tokens": -1}, {"max_tokens": 0}, {}, {"max_tokens": None},
|
||||
{"max_completion_tokens": -1}, {"max_completion_tokens": None, "max_tokens": None}],
|
||||
@@ -424,6 +464,55 @@ class ClientShapes(unittest.TestCase):
|
||||
for part in ("Be brief.", "Now use digits.", "Also this.", "1+1?"):
|
||||
self.assertIn(part, text)
|
||||
|
||||
def test_no_user_turn_is_a_400(self):
|
||||
# #365: the template's own refusal (Qwen's "No user query found in messages." when no turn is a user's query)
|
||||
# answers 400, not a dropped connection
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
tpl = Path(d) / "chat_template.jinja"
|
||||
tpl.write_text("{% if messages[-1].role != 'user' %}{{ raise_exception('No user query found in messages.') }}"
|
||||
"{% endif %}{{ messages[-1].content }}", encoding="utf-8")
|
||||
template, self.svc.template = self.svc.template, ChatTemplate(tpl)
|
||||
try:
|
||||
status, b = self.post("/v1/chat/completions", {
|
||||
"model": "x", "max_tokens": 20, "messages": [{"role": "system", "content": "Only a system."}]})
|
||||
self.assertEqual(status, 400, b)
|
||||
self.assertIn("No user query found", b["error"]["message"])
|
||||
finally:
|
||||
self.svc.template = template
|
||||
status, b = self.post("/v1/chat/completions", {"model": "x", "max_tokens": 20,
|
||||
"messages": [{"role": "user", "content": "hi"}]})
|
||||
self.assertEqual(status, 200, b) # the server goes on
|
||||
|
||||
def test_messages_as_a_json_string_over_http(self):
|
||||
# #460: a double-encoded "messages" is answered; one that is not a list of objects is a 400, not a 500
|
||||
encoded = json.dumps([{"role": "user", "content": "1+1?"}])
|
||||
for path in ("/v1/chat/completions", "/v1/messages"):
|
||||
with self.subTest(path=path):
|
||||
status, b = self.post(path, {"model": "x", "max_tokens": 20, "messages": encoded})
|
||||
self.assertEqual(status, 200, b)
|
||||
self.assertIn("1+1?", self.prompt_text())
|
||||
status, b = self.post(path, {"model": "x", "max_tokens": 20, "messages": ["hi"]})
|
||||
self.assertEqual(status, 400, b)
|
||||
self.assertIn("messages must be a list of objects", b["error"]["message"])
|
||||
|
||||
def test_vision_temp_image_removed_when_the_pipe_fails(self):
|
||||
# #352: the temporary image goes even when the encoder's pipe raises
|
||||
from serve.server import Vision
|
||||
|
||||
class Gone:
|
||||
def write(self, _):
|
||||
raise BrokenPipeError("the encoder is gone")
|
||||
|
||||
v = Vision.__new__(Vision)
|
||||
v.dir, v.lock, v.cache = Path(tempfile.mkdtemp(prefix="strata-vision-test-")), threading.Lock(), {}
|
||||
v.proc = mock.Mock(stdin=Gone())
|
||||
with mock.patch.object(Vision, "load", return_value=b""), mock.patch.object(Vision, "normalize",
|
||||
return_value=b"png"):
|
||||
with self.assertRaises(BrokenPipeError):
|
||||
v.encode("x")
|
||||
self.assertEqual(list(v.dir.iterdir()), [])
|
||||
v.dir.rmdir()
|
||||
|
||||
def test_leading_system_unchanged(self):
|
||||
from serve.frontend import anthropic_to_messages, openai_to_messages
|
||||
msgs, _, _ = openai_to_messages({"messages": [{"role": "developer", "content": "D"}, {"role": "user", "content": "u"}]})
|
||||
@@ -431,6 +520,32 @@ class ClientShapes(unittest.TestCase):
|
||||
msgs, _, _ = anthropic_to_messages({"system": "S", "messages": [{"role": "user", "content": "u"}]})
|
||||
self.assertEqual([m["role"] for m in msgs], ["system", "user"])
|
||||
|
||||
def test_messages_sent_as_a_json_string(self):
|
||||
# #460: a client that double-encodes "messages" (and "tool_calls") as a JSON string gets them decoded; what is
|
||||
# still not a list of objects is a ValueError (the server's 400), not an AttributeError on m.get
|
||||
from serve.frontend import anthropic_to_messages, openai_to_messages
|
||||
call = [{"id": "c1", "type": "function", "function": {"name": "f", "arguments": "{\"x\": 1}"}}]
|
||||
listed = [{"role": "user", "content": "u"}, {"role": "assistant", "content": "", "tool_calls": call}]
|
||||
encoded = [listed[0], dict(listed[1], tool_calls=json.dumps(call))]
|
||||
want = openai_to_messages({"messages": listed})[0]
|
||||
self.assertEqual(want[1]["tool_calls"], [{"function": {"name": "f", "arguments": {"x": 1}}}])
|
||||
self.assertEqual(openai_to_messages({"messages": json.dumps(listed)})[0], want)
|
||||
self.assertEqual(openai_to_messages({"messages": json.dumps(encoded)})[0], want)
|
||||
anth = [{"role": "user", "content": "u"}]
|
||||
self.assertEqual(anthropic_to_messages({"messages": json.dumps(anth)})[0],
|
||||
anthropic_to_messages({"messages": anth})[0])
|
||||
self.assertEqual(openai_to_messages({})[0], []) # no field: nothing, as before
|
||||
self.assertEqual(openai_to_messages({"messages": None})[0], [])
|
||||
for bad in ("not json", "\"a string\"", json.dumps({"role": "user"}), ["hi"], [{"role": "user"}, 3], 5,
|
||||
{"role": "user", "content": "u"}):
|
||||
for fn in (openai_to_messages, anthropic_to_messages):
|
||||
with self.subTest(bad=bad, fn=fn.__name__):
|
||||
with self.assertRaisesRegex(ValueError, "messages must be a list of objects"):
|
||||
fn({"messages": bad})
|
||||
for calls in (["f"], "[1]", [{"function": "f"}], "{"):
|
||||
with self.subTest(calls=calls), self.assertRaisesRegex(ValueError, "tool_calls must be a list of objects"):
|
||||
openai_to_messages({"messages": [{"role": "assistant", "content": "", "tool_calls": calls}]})
|
||||
|
||||
|
||||
class SamplingKeys(unittest.TestCase):
|
||||
"""The GEN line's sampling keys: top_k 0 ("off") or wider than the engine's 64 get the widest list, 64 (they used
|
||||
@@ -472,6 +587,30 @@ class GpuChoice(unittest.TestCase):
|
||||
self.assertEqual(plain.get("CUDA_VISIBLE_DEVICES"), os.environ.get("CUDA_VISIBLE_DEVICES"))
|
||||
self.assertEqual(plain.get("CUDA_DEVICE_ORDER"), os.environ.get("CUDA_DEVICE_ORDER"))
|
||||
|
||||
def test_vision_device(self):
|
||||
# #408: the image encoder on its own card; the engine's environment stays as it was
|
||||
from serve.server import child_env, vision_env
|
||||
cfg = {"gpu": [0, 1], "vision": {"exe": "v", "cuda_device": 2}}
|
||||
env = child_env(cfg)
|
||||
venv = vision_env(cfg, env)
|
||||
self.assertEqual(venv["CUDA_VISIBLE_DEVICES"], "2")
|
||||
self.assertEqual(venv["CUDA_DEVICE_ORDER"], "PCI_BUS_ID")
|
||||
self.assertEqual(env["CUDA_VISIBLE_DEVICES"], "0,1")
|
||||
plain = {"gpu": [0, 1], "vision": {"exe": "v"}}
|
||||
self.assertIs(vision_env(plain, env), env) # no cuda_device: the engine's environment, unchanged
|
||||
|
||||
def test_hip_ordinal(self):
|
||||
"""#325: on Windows the HIP ordinal setup resolved wins over the config's "gpu" (an iGPU takes HIP's 0)."""
|
||||
from serve.server import child_env
|
||||
self.assertEqual(child_env({"backend": "hip", "gpu": 1})["HIP_VISIBLE_DEVICES"], "1") # Linux: KFD order
|
||||
self.assertEqual(child_env({"backend": "hip", "hip_ordinal": 1})["HIP_VISIBLE_DEVICES"], "1")
|
||||
self.assertEqual(child_env({"backend": "hip", "gpu": 0, "hip_ordinal": 1})["HIP_VISIBLE_DEVICES"], "1")
|
||||
self.assertEqual(child_env({"backend": "hip", "gpu": [1, 0], "hip_ordinal": 2})["HIP_VISIBLE_DEVICES"],
|
||||
"1,0") # a layer split keeps its list
|
||||
self.assertEqual(child_env({"backend": "hip", "gpu": 0, "hip_ordinal": "x"})["HIP_VISIBLE_DEVICES"], "0")
|
||||
plain = child_env({"backend": "hip"})
|
||||
self.assertEqual(plain.get("HIP_VISIBLE_DEVICES"), os.environ.get("HIP_VISIBLE_DEVICES"))
|
||||
|
||||
|
||||
class RecordingPrompt(MockEngine):
|
||||
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
|
||||
@@ -501,6 +640,59 @@ class DyingEngine(MockEngine):
|
||||
yield t
|
||||
|
||||
|
||||
class SlowPromptEngine(MockEngine):
|
||||
"""Reads a "long prompt" for up to 20 s without a token (the engine sends nothing then), stopping on cancel."""
|
||||
|
||||
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
|
||||
self.cancelled_after = None
|
||||
t0 = time.monotonic()
|
||||
while time.monotonic() - t0 < 20:
|
||||
if cancel.is_set():
|
||||
self.cancelled_after = time.monotonic() - t0
|
||||
return
|
||||
time.sleep(0.05)
|
||||
yield from super().generate(ids, max_new, sampling, cancel, embeddings)
|
||||
|
||||
|
||||
class ClientHangUp(unittest.TestCase):
|
||||
"""#430 #431: a client that hangs up during a long prompt read cancels the request within about a second -
|
||||
non-streamed (which writes nothing until the end) and streamed (one keep-alive per prompt chunk) alike."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
tok = ByteTokenizer()
|
||||
cls.engine = SlowPromptEngine(tok, "</think>\n\nOK", max_context=CTX)
|
||||
cls.svc = Service(cls.engine, tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
|
||||
cls.httpd = serve(cls.svc, port=0)
|
||||
cls.port = cls.httpd.server_address[1]
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.httpd.shutdown()
|
||||
cls.httpd.server_close()
|
||||
|
||||
def hang_up(self, stream):
|
||||
import socket as so
|
||||
body = json.dumps({"model": "x", "max_tokens": 20, "stream": stream,
|
||||
"messages": [{"role": "user", "content": "a long prompt"}]}).encode()
|
||||
c = so.create_connection(("127.0.0.1", self.port))
|
||||
c.sendall(b"POST /v1/chat/completions HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\n"
|
||||
b"Content-Length: " + str(len(body)).encode() + b"\r\n\r\n" + body)
|
||||
time.sleep(1.0)
|
||||
c.close()
|
||||
t0 = time.monotonic()
|
||||
while self.engine.cancelled_after is None and time.monotonic() - t0 < 10:
|
||||
time.sleep(0.05)
|
||||
self.assertIsNotNone(self.engine.cancelled_after, "the request was not cancelled")
|
||||
self.assertLess(self.engine.cancelled_after, 3.0)
|
||||
|
||||
def test_non_streamed(self):
|
||||
self.hang_up(False)
|
||||
|
||||
def test_streamed(self):
|
||||
self.hang_up(True)
|
||||
|
||||
|
||||
class EngineDeath(unittest.TestCase):
|
||||
"""Issue #27: a dead engine is an error (not "length"), and the next request starts it again."""
|
||||
|
||||
@@ -564,6 +756,174 @@ class EngineDeath(unittest.TestCase):
|
||||
httpd.server_close()
|
||||
|
||||
|
||||
class DoneLineEngine(MockEngine):
|
||||
"""The mock engine whose `last` comes from a DONE line, parsed as StrataEngine parses it."""
|
||||
|
||||
def __init__(self, *a, done_lines=(), **kw):
|
||||
super().__init__(*a, **kw)
|
||||
self.done_lines = list(done_lines)
|
||||
|
||||
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
|
||||
try:
|
||||
yield from super().generate(ids, max_new, sampling, cancel, embeddings)
|
||||
finally:
|
||||
StrataEngine._parse_done(self, self.done_lines.pop(0))
|
||||
|
||||
|
||||
class DraftCounts(unittest.TestCase):
|
||||
"""#457: GET /metrics gives each request's speculative draft counts (offered / accepted, from the engine's DONE
|
||||
line; None when the line has no such fields) and their running sums in the totals."""
|
||||
|
||||
def test_drafts_in_history_and_totals(self):
|
||||
tok = ByteTokenizer()
|
||||
engine = DoneLineEngine(tok, "</think>\n\nok", max_context=CTX, done_lines=[
|
||||
"DONE 4 20 40.0 30.0 stop 7 12 0", # 7 of 12 drafts accepted
|
||||
"DONE 4 20 40.0 30.0 stop", # an engine that reports no drafts
|
||||
"DONE 4 20 40.0 30.0 stop 3 5 0 9 10"])
|
||||
svc = Service(engine, tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
|
||||
self.assertEqual((svc.totals["drafts_offered"], svc.totals["drafts_accepted"]), (0, 0))
|
||||
httpd = serve(svc, port=0)
|
||||
base = f"http://127.0.0.1:{httpd.server_address[1]}"
|
||||
try:
|
||||
for _ in range(3):
|
||||
body = json.dumps({"model": "m", "max_tokens": 10, "messages": [{"role": "user", "content": "hi"}]})
|
||||
with urllib.request.urlopen(urllib.request.Request(base + "/v1/chat/completions", data=body.encode(),
|
||||
headers={"Content-Type": "application/json"}),
|
||||
timeout=30) as r:
|
||||
self.assertEqual(r.status, 200)
|
||||
with urllib.request.urlopen(base + "/metrics", timeout=10) as r:
|
||||
m = json.loads(r.read())
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
httpd.server_close()
|
||||
rows = m["requests"] # newest first
|
||||
self.assertEqual([(r["drafts_offered"], r["drafts_accepted"]) for r in rows], [(5, 3), (None, None), (12, 7)])
|
||||
self.assertEqual((m["totals"]["drafts_offered"], m["totals"]["drafts_accepted"]), (17, 10))
|
||||
|
||||
|
||||
class LearnedProfile(unittest.TestCase):
|
||||
"""#477: "expert_profile_save" in the config: the engine saves its learned profile there, and the next start
|
||||
begins from it when it is a profile of the same model; without the key the arguments are unchanged."""
|
||||
|
||||
def write(self, path, nl=48, ne=512, n=4):
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
import make_profile
|
||||
old = make_profile.N_LAYER
|
||||
make_profile.N_LAYER = nl
|
||||
try:
|
||||
make_profile.write_profile(path, [(i % nl, i // nl) for i in range(n)], n_expert=ne)
|
||||
finally:
|
||||
make_profile.N_LAYER = old
|
||||
|
||||
def test_without_the_key_nothing_changes(self):
|
||||
args = ["--native", "x", "--expert-profile", "data/expert-profile.bin", "--adapt-every", "4"]
|
||||
self.assertEqual(engine_args({"args": list(args)}), args)
|
||||
self.assertEqual(engine_args({"args": list(args), "expert_profile_save": ""}), args)
|
||||
|
||||
def test_save_and_start_from_it(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
self.write(d / "base.bin")
|
||||
cfg = {"args": ["--expert-profile", "base.bin"], "cwd": str(d), "expert_profile_save": "learned.bin",
|
||||
"expert_profile_save_every": 5}
|
||||
# nothing saved yet: the config's profile, and the engine is told where to save
|
||||
self.assertEqual(engine_args(cfg), ["--expert-profile", "base.bin", "--expert-profile-save", "learned.bin",
|
||||
"--expert-profile-save-every", "5"])
|
||||
self.write(d / "learned.bin")
|
||||
self.assertEqual(engine_args(cfg)[:2], ["--expert-profile", "learned.bin"])
|
||||
self.write(d / "learned.bin", ne=256) # another model's: not used
|
||||
self.assertEqual(engine_args(cfg)[:2], ["--expert-profile", "base.bin"])
|
||||
(d / "learned.bin").write_bytes(b"STRP" + bytes(20)) # not a whole profile
|
||||
self.assertEqual(engine_args(cfg)[:2], ["--expert-profile", "base.bin"])
|
||||
self.write(d / "learned.bin")
|
||||
(d / "learned.bin").write_bytes((d / "learned.bin").read_bytes()[:30]) # truncated
|
||||
self.assertEqual(engine_args(cfg)[:2], ["--expert-profile", "base.bin"])
|
||||
|
||||
def test_no_profile_in_the_args(self):
|
||||
cfg = {"args": ["--native", "x"], "expert_profile_save": "learned.bin"}
|
||||
self.assertEqual(engine_args(cfg), ["--native", "x", "--expert-profile-save", "learned.bin"])
|
||||
|
||||
|
||||
class DraftHeadHint(unittest.TestCase):
|
||||
"""#474: a start that stopped at "the draft head does not fit" says what to change, from this start's log lines."""
|
||||
|
||||
def log(self, text, before=""):
|
||||
d = tempfile.mkdtemp()
|
||||
p = Path(d) / "engine.log"
|
||||
p.write_text(before + text, encoding="utf-8")
|
||||
return str(p), len(before.encode())
|
||||
|
||||
def test_the_engines_hint_is_relayed(self):
|
||||
p, off = self.log("strata mtp: the draft head over 106299 tokens needs 348 MiB of VRAM and 120 MiB is free.\n"
|
||||
"strata mtp: hint: a smaller draft vocabulary needs less VRAM: --draft-vocab en (...)\n"
|
||||
"strata serve: mtp: the draft head does not fit\n")
|
||||
h = start_failure_hint(p, off)
|
||||
self.assertIn("the draft head does not fit", h)
|
||||
self.assertIn("348 MiB", h)
|
||||
self.assertIn("--draft-vocab en", h)
|
||||
|
||||
def test_an_older_engine_gets_the_advice_in_words(self):
|
||||
p, off = self.log("strata serve: mtp: the draft head does not fit\n")
|
||||
self.assertIn("--draft-vocab en", start_failure_hint(p, off))
|
||||
|
||||
def test_other_failures_and_earlier_starts_add_nothing(self):
|
||||
p, off = self.log("strata serve: cannot open the pack\n")
|
||||
self.assertEqual(start_failure_hint(p, off), "")
|
||||
# an earlier start's failure (before this start's offset) is not this one's
|
||||
p, off = self.log("strata serve: cannot open the pack\n",
|
||||
before="strata serve: mtp: the draft head does not fit\n")
|
||||
self.assertEqual(start_failure_hint(p, off), "")
|
||||
self.assertEqual(start_failure_hint(None, 0), "")
|
||||
self.assertEqual(start_failure_hint(str(Path(tempfile.mkdtemp()) / "missing.log"), 0), "")
|
||||
|
||||
|
||||
class CancelledRead(unittest.TestCase):
|
||||
"""#471: a request cancelled while its prompt was read is recorded with the tokens the engine read (the DONE
|
||||
line's 15th field), not the whole prompt; an older engine's line (no such field) keeps the whole prompt."""
|
||||
|
||||
def test_parse_done_read_field(self):
|
||||
e = SimpleNamespace()
|
||||
StrataEngine._parse_done(e, "DONE 0 98179 17565.0 0.0 cancel 0 0 0 0 0 0 0 0.0 12288")
|
||||
self.assertEqual((e.last["prompt_tokens"], e.last["prompt_read"], e.last["finish"]), (98179, 12288, "cancel"))
|
||||
StrataEngine._parse_done(e, "DONE 0 98179 17565.0 0.0 cancel 0 0 0 0 0 0 0 0.0")
|
||||
self.assertNotIn("prompt_read", e.last)
|
||||
|
||||
def test_prompt_tokens_seen(self):
|
||||
last = {"finish": "cancel", "reused": 1000, "prompt_read": 2000}
|
||||
self.assertEqual(prompt_tokens_seen(98179, last), 3000)
|
||||
self.assertEqual(prompt_tokens_seen(98179, {**last, "finish": "stop"}), 98179) # read in full: all of it
|
||||
self.assertEqual(prompt_tokens_seen(98179, {"finish": "cancel", "reused": 0}), 98179) # an older engine
|
||||
self.assertEqual(prompt_tokens_seen(98179, {}), 98179) # no DONE at all
|
||||
self.assertEqual(prompt_tokens_seen(10, {**last, "prompt_read": 50}), 10) # never past the prompt
|
||||
|
||||
def test_history_and_totals(self):
|
||||
tok = ByteTokenizer()
|
||||
engine = DoneLineEngine(tok, "</think>\n\nok", max_context=CTX, done_lines=[
|
||||
"DONE 0 20 400.0 0.0 cancel 0 0 0 0 0 0 0 0.0 8", # stopped after 8 prompt tokens
|
||||
"DONE 4 20 40.0 30.0 stop 0 0 0 0 0 0 0 0.0 20",
|
||||
"DONE 0 20 400.0 0.0 cancel 0 0 0"]) # an older engine: no read count
|
||||
svc = Service(engine, tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
|
||||
httpd = serve(svc, port=0)
|
||||
base = f"http://127.0.0.1:{httpd.server_address[1]}"
|
||||
try:
|
||||
for _ in range(3):
|
||||
body = json.dumps({"model": "m", "max_tokens": 10, "messages": [{"role": "user", "content": "hi"}]})
|
||||
with urllib.request.urlopen(urllib.request.Request(base + "/v1/chat/completions", data=body.encode(),
|
||||
headers={"Content-Type": "application/json"}),
|
||||
timeout=30) as r:
|
||||
self.assertEqual(r.status, 200)
|
||||
with urllib.request.urlopen(base + "/metrics", timeout=10) as r:
|
||||
m = json.loads(r.read())
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
httpd.server_close()
|
||||
rows = m["requests"][::-1] # oldest first
|
||||
total = rows[0]["prompt_total"]
|
||||
self.assertGreater(total, 8)
|
||||
self.assertEqual([r["prompt_total"] for r in rows], [total] * 3)
|
||||
self.assertEqual([(r["prompt_tokens"], r["prompt_read"]) for r in rows], [(8, 8), (total, 20), (total, None)])
|
||||
self.assertEqual(m["totals"]["prompt_tokens"], 8 + 2 * total)
|
||||
|
||||
|
||||
class LiveRate(unittest.TestCase):
|
||||
"""The Monitor's Speed readout: live.tok_s is a rate, and a request that never got a DONE keeps no counters.
|
||||
|
||||
@@ -783,6 +1143,29 @@ class SharedSettings(unittest.TestCase):
|
||||
finally:
|
||||
self.svc.api_key = ""
|
||||
|
||||
def test_proxy_headers_do_not_make_a_page_strata_s_own(self):
|
||||
# #321: X-Forwarded-*, CF-Ray or CF-Connecting-IP say nothing about the page that sent the request - any web
|
||||
# page behind any proxy would otherwise change the settings (or run MCP tools)
|
||||
for extra in ({"X-Forwarded-Host": "proxy.example.com"}, {"CF-Ray": "1234567890"},
|
||||
{"X-Forwarded-For": "203.0.113.9"}, {"CF-Connecting-IP": "203.0.113.9"}):
|
||||
code, _ = self.req("/settings", {"defaults": {"temperature": 1}},
|
||||
{"Origin": "https://proxy.example.com", **extra})
|
||||
self.assertEqual(code, 403, extra)
|
||||
# another port on the same host is another site (a local dev server's page)
|
||||
code, _ = self.req("/settings", {"defaults": {"temperature": 1}}, {"Origin": "http://127.0.0.1:1"})
|
||||
self.assertEqual(code, 403)
|
||||
|
||||
def test_a_trusted_origin_is_strata_s_own_page(self):
|
||||
# the web app behind a reverse proxy or tunnel: the config's trusted_origins
|
||||
self.svc.trusted_origins = ["https://strata.example.com"]
|
||||
try:
|
||||
code, _ = self.req("/settings", {"defaults": {}}, {"Origin": "https://strata.example.com"})
|
||||
self.assertEqual(code, 200)
|
||||
code, _ = self.req("/settings", {"defaults": {}}, {"Origin": "https://evil.example.com"})
|
||||
self.assertEqual(code, 403)
|
||||
finally:
|
||||
self.svc.trusted_origins = []
|
||||
|
||||
|
||||
class WebApp(unittest.TestCase):
|
||||
"""The web app (PR #22's dashboard idea, rebuilt): its page and files, and GET /metrics."""
|
||||
@@ -976,6 +1359,58 @@ class UsageAndStatus(unittest.TestCase):
|
||||
return [json.loads(line[6:]) for line in raw.decode().splitlines()
|
||||
if line.startswith("data: {")]
|
||||
|
||||
def preflight(self, path, origin="https://chat.example.com"):
|
||||
req = urllib.request.Request(self.base + path, method="OPTIONS",
|
||||
headers={"Origin": origin, "Access-Control-Request-Method": "POST",
|
||||
"Access-Control-Request-Headers": "authorization, content-type"})
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
return r.status, r.headers
|
||||
|
||||
def test_cors_is_off_by_default(self):
|
||||
# #321: OPTIONS is answered, but without cors_origins no page of another origin is let in
|
||||
status, h = self.preflight("/v1/chat/completions")
|
||||
self.assertEqual(status, 204)
|
||||
self.assertIsNone(h.get("Access-Control-Allow-Origin"))
|
||||
with urllib.request.urlopen(self.base + "/health", timeout=10) as r:
|
||||
self.assertIsNone(r.headers.get("Access-Control-Allow-Origin"))
|
||||
|
||||
def test_cors_for_the_configured_origins_on_the_api_only(self):
|
||||
self.svc.cors_origins = ["https://chat.example.com"]
|
||||
try:
|
||||
status, h = self.preflight("/v1/chat/completions")
|
||||
self.assertEqual((status, h.get("Access-Control-Allow-Origin")), (204, "https://chat.example.com"))
|
||||
self.assertIn("OPTIONS", h.get("Access-Control-Allow-Methods", ""))
|
||||
self.assertIn("authorization", h.get("Access-Control-Allow-Headers", ""))
|
||||
self.assertIsNone(self.preflight("/v1/chat/completions", "https://evil.example.com")[1]
|
||||
.get("Access-Control-Allow-Origin"))
|
||||
# never on the app's own endpoints (/settings, /unload, ...)
|
||||
self.assertIsNone(self.preflight("/settings")[1].get("Access-Control-Allow-Origin"))
|
||||
req = urllib.request.Request(self.base + "/v1/models", headers={"Origin": "https://chat.example.com"})
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
self.assertEqual(r.headers.get("Access-Control-Allow-Origin"), "https://chat.example.com")
|
||||
self.svc.cors_origins = ["*"]
|
||||
self.assertEqual(self.preflight("/v1/messages", "https://any.example.com")[1]
|
||||
.get("Access-Control-Allow-Origin"), "*")
|
||||
finally:
|
||||
self.svc.cors_origins = []
|
||||
|
||||
def test_sse_is_not_buffered_by_proxies(self):
|
||||
req = urllib.request.Request(self.base + "/v1/chat/completions", headers={"Content-Type": "application/json"},
|
||||
data=json.dumps({"model": "m", "stream": True,
|
||||
"messages": [{"role": "user", "content": "hi"}]}).encode())
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
self.assertEqual(r.headers.get("X-Accel-Buffering"), "no")
|
||||
r.read()
|
||||
|
||||
def test_origin_lists_are_checked(self):
|
||||
from serve.server import origins_of
|
||||
self.assertEqual(origins_of(None, "k", True), [])
|
||||
self.assertEqual(origins_of("https://a.example.com/", "k", False), ["https://a.example.com"])
|
||||
self.assertEqual(origins_of(["*", "http://localhost:3000"], "k", True), ["*", "http://localhost:3000"])
|
||||
for bad in ("*", "a.example.com", "https://a.example.com/path", "https://*.example.com", 5):
|
||||
with self.assertRaises(SystemExit):
|
||||
origins_of(bad, "k", False)
|
||||
|
||||
def test_openai(self):
|
||||
b = self.chat("/v1/chat/completions")
|
||||
u, t = b["usage"], b["timings"]
|
||||
@@ -1380,5 +1815,233 @@ class StatusHandover(unittest.TestCase):
|
||||
self.assertFalse(svc.status["busy"])
|
||||
self.assertNotIn("tail", svc.status)
|
||||
|
||||
|
||||
FAKE_STRATA = '''import pathlib, sys, time
|
||||
gate = pathlib.Path(sys.argv[sys.argv.index("--gate") + 1])
|
||||
print("INFO engine=0.0.0", flush=True)
|
||||
while not gate.exists(): # the test says when the engine is "ready"
|
||||
time.sleep(0.01)
|
||||
print("READY 4096 stop", flush=True)
|
||||
for line in sys.stdin:
|
||||
if line.startswith("QUIT"):
|
||||
break
|
||||
'''
|
||||
|
||||
|
||||
class RestartWindow(unittest.TestCase):
|
||||
"""#344: while the engine restarts it is not alive (a request waits for the restart instead of reading
|
||||
max_context 0), and a request that still meets max_context 0 gets a 503 "starting", not a 400 about its prompt."""
|
||||
|
||||
def test_not_alive_until_ready(self):
|
||||
from unittest import mock
|
||||
import serve.server as server
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
script, gate = Path(d) / "fake_strata.py", Path(d) / "ready"
|
||||
script.write_text(FAKE_STRATA, encoding="utf-8")
|
||||
real = server.subprocess.Popen
|
||||
with mock.patch.object(server.subprocess, "Popen",
|
||||
lambda cmd, **kw: real([sys.executable, str(script), *cmd[1:]], **kw)):
|
||||
gate.touch()
|
||||
eng = StrataEngine("strata", ["--gate", str(gate)])
|
||||
try:
|
||||
self.assertEqual(eng.max_context, 4096)
|
||||
self.assertTrue(eng.alive())
|
||||
gate.unlink()
|
||||
old = eng.proc
|
||||
old.kill()
|
||||
old.wait(10)
|
||||
deadline = time.time() + 10
|
||||
while not getattr(eng, "ended", False) and time.time() < deadline:
|
||||
time.sleep(0.01) # the server has noticed: its output closed
|
||||
self.assertFalse(eng.alive())
|
||||
t = threading.Thread(target=eng.restart)
|
||||
t.start()
|
||||
deadline = time.time() + 10
|
||||
while eng.proc is old and time.time() < deadline:
|
||||
time.sleep(0.005)
|
||||
self.assertIsNot(eng.proc, old, "restart never started the engine")
|
||||
time.sleep(0.2) # the new engine is up, but has not said READY
|
||||
self.assertEqual(eng.max_context, 0)
|
||||
self.assertFalse(eng.alive(), "alive before READY: a request would plan with context 0")
|
||||
gate.touch()
|
||||
t.join(10)
|
||||
self.assertFalse(t.is_alive())
|
||||
self.assertTrue(eng.alive())
|
||||
self.assertEqual(eng.max_context, 4096)
|
||||
# restart() of a running engine kills it first: that engine's output thread ends after the new
|
||||
# one is up, and must not mark it dead (it did: every request after restarted it again)
|
||||
eng.restart()
|
||||
time.sleep(0.5)
|
||||
self.assertTrue(eng.alive())
|
||||
self.assertEqual(eng.max_context, 4096)
|
||||
finally:
|
||||
gate.touch()
|
||||
eng.unload()
|
||||
|
||||
def test_context_zero_is_503(self):
|
||||
tok = ByteTokenizer()
|
||||
svc = Service(MockEngine(tok, "</think>\n\nok", max_context=0), tok,
|
||||
ChatTemplate(ROOT / "serve/chat_template.jinja"))
|
||||
httpd = serve(svc, port=0)
|
||||
base = f"http://127.0.0.1:{httpd.server_address[1]}"
|
||||
try:
|
||||
for path, body in (("/v1/chat/completions", {"model": "m", "messages": [{"role": "user", "content": "hi"}],
|
||||
"max_tokens": 16}),
|
||||
("/v1/messages", {"model": "m", "max_tokens": 16,
|
||||
"messages": [{"role": "user", "content": "hi"}]})):
|
||||
req = urllib.request.Request(base + path, data=json.dumps(body).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
with self.assertRaises(urllib.error.HTTPError) as cm:
|
||||
urllib.request.urlopen(req, timeout=30)
|
||||
with cm.exception as e:
|
||||
self.assertEqual(e.code, 503, path)
|
||||
text = e.read().decode()
|
||||
self.assertIn("starting", text)
|
||||
self.assertNotIn("leaves no room", text)
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
httpd.server_close()
|
||||
|
||||
|
||||
class ModelAliases(unittest.TestCase):
|
||||
"""#297: the config's `aliases` are listed by /v1/models and accepted as model names (answered under that name)."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
tok = ByteTokenizer()
|
||||
cls.svc = Service(MockEngine(tok, "</think>\n\nok", max_context=CTX), tok,
|
||||
ChatTemplate(ROOT / "serve/chat_template.jinja"), model_name="qwen3.8-flash-next-iq3_xxs")
|
||||
cls.svc.set_aliases(["qwen", "local-model", "qwen", " ", "qwen3.8-flash-next-iq3_xxs"])
|
||||
cls.httpd = serve(cls.svc, port=0)
|
||||
cls.base = f"http://127.0.0.1:{cls.httpd.server_address[1]}"
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.httpd.shutdown()
|
||||
cls.httpd.server_close()
|
||||
|
||||
def get(self, path):
|
||||
with urllib.request.urlopen(self.base + path, timeout=30) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
def post(self, path, body):
|
||||
req = urllib.request.Request(self.base + path, data=json.dumps(body).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return r.read().decode()
|
||||
|
||||
def test_set_aliases(self):
|
||||
self.assertEqual(self.svc.aliases, ["qwen", "local-model"]) # duplicates, blanks and the name itself
|
||||
tok = ByteTokenizer()
|
||||
svc = Service(MockEngine(tok, "x", max_context=CTX), tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
|
||||
svc.set_aliases("a, b")
|
||||
self.assertEqual(svc.aliases, ["a", "b"])
|
||||
svc.set_aliases(None)
|
||||
self.assertEqual(svc.aliases, [])
|
||||
for bad in (3, ["a", 1], {"a": 1}):
|
||||
with self.assertRaises(ValueError):
|
||||
svc.set_aliases(bad)
|
||||
|
||||
def test_models_lists_them(self):
|
||||
data = self.get("/v1/models")["data"]
|
||||
self.assertEqual([m["id"] for m in data], ["qwen3.8-flash-next-iq3_xxs", "qwen", "local-model"])
|
||||
self.assertEqual(data[0]["aliases"], ["qwen", "local-model"])
|
||||
self.assertEqual(data[1]["alias_of"], "qwen3.8-flash-next-iq3_xxs")
|
||||
self.assertEqual(self.get("/props?model=local-model")["model_alias"], "qwen3.8-flash-next-iq3_xxs")
|
||||
|
||||
def test_requests_are_answered_under_the_alias(self):
|
||||
msgs = [{"role": "user", "content": "hi"}]
|
||||
for asked, want in (("qwen", "qwen"), ("local-model", "local-model"),
|
||||
("qwen3.8-flash-next-iq3_xxs", "qwen3.8-flash-next-iq3_xxs"),
|
||||
("something-else", "qwen3.8-flash-next-iq3_xxs")): # still served, as before
|
||||
with self.subTest(asked=asked):
|
||||
out = json.loads(self.post("/v1/chat/completions", {"model": asked, "messages": msgs, "max_tokens": 8}))
|
||||
self.assertEqual(out["model"], want)
|
||||
text = self.post("/v1/chat/completions", {"model": asked, "messages": msgs, "max_tokens": 8,
|
||||
"stream": True})
|
||||
first = json.loads(text.split("data: ", 2)[1].strip())
|
||||
self.assertEqual(first["model"], want)
|
||||
out = json.loads(self.post("/v1/messages", {"model": asked, "messages": msgs, "max_tokens": 8}))
|
||||
self.assertEqual(out["model"], want)
|
||||
|
||||
def test_without_aliases_nothing_changes(self):
|
||||
tok = ByteTokenizer()
|
||||
svc = Service(MockEngine(tok, "x", max_context=CTX), tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
|
||||
httpd = serve(svc, port=0)
|
||||
try:
|
||||
with urllib.request.urlopen(f"http://127.0.0.1:{httpd.server_address[1]}/v1/models", timeout=30) as r:
|
||||
data = json.loads(r.read())["data"]
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertNotIn("aliases", data[0])
|
||||
self.assertEqual(svc.model_for({"model": "x"}), svc.model)
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
httpd.server_close()
|
||||
|
||||
|
||||
class AmdTelemetry(unittest.TestCase):
|
||||
"""#301: the AMD backend's readings from a fake amdgpu sysfs tree: KFD node -> render node, as setup numbers the
|
||||
cards (the CPU node skipped), and free_vram_mib on HIP."""
|
||||
|
||||
def tree(self, d):
|
||||
nodes = Path(d) / "class/kfd/kfd/topology/nodes"
|
||||
for n, props in ((0, "cpu_cores_count 16\nsimd_count 0\ngfx_target_version 0\ndrm_render_minor 0\n"),
|
||||
(1, "simd_count 128\ngfx_target_version 120001\ndrm_render_minor 129\n"),
|
||||
(2, "simd_count 128\ngfx_target_version 120001\ndrm_render_minor 128\n")):
|
||||
(nodes / str(n)).mkdir(parents=True)
|
||||
(nodes / str(n) / "properties").write_text(props)
|
||||
for minor, used, busy, temp, power in ((129, 2 << 30, 37, 51000, 85000000), (128, 6 << 30, 99, 64000, None)):
|
||||
dev = Path(d) / f"class/drm/renderD{minor}/device"
|
||||
hw = dev / "hwmon" / "hwmon4"
|
||||
hw.mkdir(parents=True)
|
||||
(dev / "gpu_busy_percent").write_text(f"{busy}\n")
|
||||
(dev / "mem_info_vram_used").write_text(f"{used}\n")
|
||||
(dev / "mem_info_vram_total").write_text(f"{32 << 30}\n")
|
||||
(dev / "product_name").write_text("AMD Radeon AI PRO R9700\n")
|
||||
(hw / "temp1_input").write_text(f"{temp}\n")
|
||||
if power is not None:
|
||||
(hw / "power1_average").write_text(f"{power}\n")
|
||||
else:
|
||||
(hw / "power1_input").write_text("120000000\n")
|
||||
(hw / "power1_cap").write_text("300000000\n")
|
||||
|
||||
def test_readings(self):
|
||||
from serve import telemetry
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
self.tree(d)
|
||||
with mock.patch.object(telemetry, "SYSFS", d):
|
||||
self.assertTrue(telemetry.amd_device_dir(0).endswith(os.path.join("renderD129", "device")))
|
||||
self.assertTrue(telemetry.amd_device_dir(1).endswith(os.path.join("renderD128", "device")))
|
||||
self.assertIsNone(telemetry.amd_device_dir(2))
|
||||
g = telemetry.gpu_reader(0, amd=True)
|
||||
self.assertTrue(g.ok())
|
||||
self.assertEqual(g.name(), "AMD Radeon AI PRO R9700")
|
||||
self.assertEqual(g.read(), {"util": 37, "mem_used": 2 << 30, "mem_total": 32 << 30, "temp": 51.0,
|
||||
"power": 85.0, "power_limit": 300.0})
|
||||
r = telemetry.gpu_reader(1, amd=True).read()
|
||||
self.assertEqual((r["util"], r["temp"], r["power"]), (99, 64.0, 120.0)) # power1_input
|
||||
self.assertEqual(telemetry.free_vram_mib(0, amd=True), 30 << 10)
|
||||
self.assertIsNone(telemetry.free_vram_mib(5, amd=True))
|
||||
t = telemetry.Telemetry(gpu_index=0, gpu_indices=[0, 1], amd=True)
|
||||
s = t.sample()
|
||||
self.assertEqual(t.static["gpu_name"], "AMD Radeon AI PRO R9700 + AMD Radeon AI PRO R9700")
|
||||
self.assertEqual((s["gpu_mem_used"], s["gpu_util"], s["gpu_temp"], s["gpu_power"]),
|
||||
(8 << 30, 68.0, 64.0, 205.0))
|
||||
with tempfile.TemporaryDirectory() as d: # no amdgpu: nothing, and nothing breaks
|
||||
with mock.patch.object(telemetry, "SYSFS", d):
|
||||
self.assertFalse(telemetry.gpu_reader(0, amd=True).ok())
|
||||
self.assertIsNone(telemetry.free_vram_mib(0, amd=True))
|
||||
|
||||
def test_free_vram_on_hip(self):
|
||||
from serve import telemetry
|
||||
tok = ByteTokenizer()
|
||||
svc = Service(MockEngine(tok, "x", max_context=CTX), tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
|
||||
svc.backend, svc.gpu_index = "hip", 1
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
self.tree(d)
|
||||
with mock.patch.object(telemetry, "SYSFS", d):
|
||||
self.assertEqual(svc.free_vram_mib(), 26 << 10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,167 @@
|
||||
"""Exercise the JSON contract through HTTP, including H3's strict Story shape."""
|
||||
import json
|
||||
import unittest
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from serve.frontend import ChatTemplate
|
||||
from serve import structured
|
||||
from serve.server import ByteTokenizer, MockEngine, Service, serve
|
||||
|
||||
try:
|
||||
import jsonschema # noqa: F401
|
||||
HAVE_JSONSCHEMA = True
|
||||
except ImportError: # optional: json_schema answers are then only checked to be one object
|
||||
HAVE_JSONSCHEMA = False
|
||||
|
||||
|
||||
STORY = {"title": "Photobooth", "sheet": "A single continuous shot.", "cast": [],
|
||||
"boxes": [{"box": 1, "duration_ms": 15000, "new_shot": True, "beat": "Hailee changes poses.",
|
||||
"on_screen": ["Hailee"], "references": [], "ends": "She smiles."}], "suggestions": []}
|
||||
|
||||
|
||||
def object_schema(properties):
|
||||
return {"type": "object", "properties": properties, "required": list(properties), "additionalProperties": False}
|
||||
|
||||
|
||||
TEXT = {"type": "string"}
|
||||
STORY_SCHEMA = object_schema({
|
||||
"title": TEXT, "sheet": TEXT, "cast": {"type": "array", "items": object_schema({"name": TEXT,
|
||||
"references": {"type": "array", "items": TEXT, "maxItems": 0}})},
|
||||
"boxes": {"type": "array", "minItems": 1, "maxItems": 1, "items": object_schema({
|
||||
"box": {"type": "integer", "minimum": 1, "maximum": 1},
|
||||
"duration_ms": {"type": "integer", "minimum": 1, "maximum": 15000}, "new_shot": {"type": "boolean"},
|
||||
"beat": TEXT, "on_screen": {"type": "array", "items": TEXT},
|
||||
"references": {"type": "array", "items": TEXT, "maxItems": 0}, "ends": TEXT})},
|
||||
"suggestions": {"type": "array", "items": object_schema({"what": TEXT, "why": TEXT,
|
||||
"boxes": {"type": "array", "items": {"type": "integer"}}, "description": TEXT})}})
|
||||
FORMAT = {"type": "json_schema", "json_schema": {"name": "h3_story", "strict": True, "schema": STORY_SCHEMA}}
|
||||
|
||||
|
||||
class Structured(unittest.TestCase):
|
||||
def request(self, path, body=None, headers=None):
|
||||
req = urllib.request.Request(self.base + path, data=json.dumps(body).encode() if body is not None else None,
|
||||
headers={"Content-Type": "application/json", **(headers or {})})
|
||||
try:
|
||||
response = urllib.request.urlopen(req, timeout=10)
|
||||
except urllib.error.HTTPError as error:
|
||||
response = error
|
||||
with response:
|
||||
data = response.read().decode()
|
||||
return response.status, data if "event-stream" in response.headers.get("Content-Type", "") else json.loads(data)
|
||||
|
||||
def setUp(self):
|
||||
self.tok = ByteTokenizer()
|
||||
self.engine = MockEngine(self.tok, json.dumps(STORY), max_context=16384)
|
||||
self.svc = Service(self.engine, self.tok, ChatTemplate(Path(__file__).parent / "chat_template.jinja"))
|
||||
self.httpd = serve(self.svc, port=0)
|
||||
self.base = f"http://127.0.0.1:{self.httpd.server_address[1]}"
|
||||
|
||||
def tearDown(self):
|
||||
self.httpd.shutdown()
|
||||
self.httpd.server_close()
|
||||
|
||||
def chat(self, script=None, **kwargs):
|
||||
if script is not None:
|
||||
self.engine = self.svc.engine = MockEngine(self.tok, script, max_context=16384)
|
||||
req = {"messages": [{"role": "system", "content": "Write a film plan."},
|
||||
{"role": "user", "content": "Photobooth. 15 seconds."}],
|
||||
"response_format": FORMAT, "reasoning_effort": "none", **kwargs}
|
||||
return self.request("/v1/chat/completions", req)
|
||||
|
||||
def test_h3_schema_success_and_stream_with_reasoning(self):
|
||||
code, reply = self.chat()
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(json.loads(reply["choices"][0]["message"]["content"]), STORY)
|
||||
prompt = self.tok.decode(self.engine.last_prompt)
|
||||
self.assertIn("OUTPUT FORMAT REQUIREMENT", prompt)
|
||||
self.assertIn('"duration_ms"', prompt)
|
||||
code, raw = self.chat("Consider the shot.</think>\n" + json.dumps(STORY), stream=True, reasoning_effort="low")
|
||||
self.assertEqual(code, 200)
|
||||
chunks = [json.loads(line[6:]) for line in raw.splitlines() if line.startswith("data: {")]
|
||||
content = "".join(c["choices"][0]["delta"].get("content", "") for c in chunks)
|
||||
self.assertEqual(json.loads(content), STORY)
|
||||
self.assertIn("reasoning_content", raw)
|
||||
self.assertIn("data: [DONE]", raw)
|
||||
|
||||
@unittest.skipUnless(HAVE_JSONSCHEMA, "jsonschema is not installed")
|
||||
def test_invalid_generation_is_an_error_and_never_streams_content(self):
|
||||
for script in ("## Film Plan - Photobooth", '{"title":"Only a title"}',
|
||||
json.dumps({**STORY, "extra": True}),
|
||||
json.dumps({**STORY, "boxes": [{**STORY["boxes"][0], "duration_ms": "15000"}]})):
|
||||
with self.subTest(script=script):
|
||||
code, reply = self.chat(script)
|
||||
self.assertEqual(code, 502)
|
||||
self.assertEqual(reply["error"]["code"], "structured_output_failed")
|
||||
_, raw = self.chat(script, stream=True)
|
||||
self.assertIn('"structured_output_failed"', raw)
|
||||
self.assertNotIn('"delta"', raw)
|
||||
self.assertIn("data: [DONE]", raw)
|
||||
|
||||
def test_object_mode_strict_json_and_truncation(self):
|
||||
fmt = {"type": "json_object"}
|
||||
self.assertEqual(self.chat('{"answer":4}', response_format=fmt)[0], 200)
|
||||
for script in ('[]', '{"x":NaN}', '{"x":1,"x":2}', '{"x":1e999}', '```json\n{}\n```'):
|
||||
self.assertEqual(self.chat(script, response_format=fmt)[0], 502)
|
||||
self.assertEqual(self.chat('{"answer":4}', response_format=fmt, max_tokens=5)[0], 502)
|
||||
# Plain text callers retain normal streaming, without a JSON validation gate.
|
||||
self.assertEqual(self.chat("Prose.", response_format={"type": "text"})[0], 200)
|
||||
|
||||
def test_rejected_schema_does_not_load_the_engine_or_retry(self):
|
||||
with mock.patch.object(self.svc, "load") as load:
|
||||
code, _ = self.chat(response_format={"type": "json_schema"})
|
||||
self.assertEqual(code, 400)
|
||||
load.assert_not_called()
|
||||
self.engine = self.svc.engine = MockEngine(self.tok, "Markdown is not JSON", max_context=16384)
|
||||
with mock.patch.object(self.engine, "generate", wraps=self.engine.generate) as generate:
|
||||
self.assertEqual(self.chat()[0], 502)
|
||||
self.assertEqual(generate.call_count, 1)
|
||||
|
||||
@unittest.skipUnless(HAVE_JSONSCHEMA, "jsonschema is not installed")
|
||||
def test_invalid_schema_is_400_and_local_refs_work(self):
|
||||
for schema in ({"type": "object", "properties": {"x": {"type": "invalid"}}},
|
||||
{"type": "object", "$ref": "https://other.example/schema"}):
|
||||
fmt = {"type": "json_schema", "json_schema": {"name": "test", "strict": True, "schema": schema}}
|
||||
self.assertEqual(self.chat(response_format=fmt)[0], 400)
|
||||
schema = object_schema({"answer": {"$ref": "#/$defs/n"}})
|
||||
schema["$defs"] = {"n": {"type": "integer"}}
|
||||
fmt = {"type": "json_schema", "json_schema": {"name": "local", "schema": schema}}
|
||||
self.assertEqual(self.chat('{"answer":4}', response_format=fmt)[0], 200)
|
||||
|
||||
def test_malformed_formats_and_tools_are_rejected_before_loading(self):
|
||||
formats = ([], {"type": "unsupported"},
|
||||
{"type": "json_schema", "json_schema": {"schema": {"type": "object"}}},
|
||||
{"type": "json_schema", "json_schema": {"name": "x", "strict": "true", "schema": {"type": "object"}}})
|
||||
with mock.patch.object(self.svc, "load") as load:
|
||||
for fmt in formats:
|
||||
with self.subTest(fmt=fmt):
|
||||
self.assertEqual(self.chat(response_format=fmt)[0], 400)
|
||||
self.assertEqual(self.chat(tools=[{"type": "function", "function": {"name": "search", "parameters": {"type": "object"}}}])[0], 400)
|
||||
self.assertEqual(self.chat(strata_mcp=True)[0], 400)
|
||||
self.assertEqual(self.request("/v1/chat/completions", [])[0], 400)
|
||||
load.assert_not_called()
|
||||
|
||||
def test_without_jsonschema_an_object_is_still_required(self):
|
||||
# jsonschema is optional: without it json_schema answers are checked to be one JSON object, json_object
|
||||
# never needs it, and nothing is imported at server start
|
||||
with mock.patch.object(structured, "_jsonschema", False):
|
||||
self.assertEqual(self.chat()[0], 200)
|
||||
for script in ("## Film Plan - Photobooth", "[1, 2]"):
|
||||
code, reply = self.chat(script)
|
||||
self.assertEqual(code, 502, script)
|
||||
self.assertEqual(reply["error"]["code"], "structured_output_failed")
|
||||
self.assertEqual(self.chat('{"answer":4}', response_format={"type": "json_object"})[0], 200)
|
||||
|
||||
def test_the_server_does_not_import_jsonschema_at_start(self):
|
||||
import subprocess
|
||||
import sys
|
||||
code = ("import sys; import serve.server; "
|
||||
"sys.exit(1 if any(m == 'jsonschema' or m.startswith('jsonschema.') for m in sys.modules) else 0)")
|
||||
r = subprocess.run([sys.executable, "-c", code], cwd=str(Path(__file__).resolve().parents[1]))
|
||||
self.assertEqual(r.returncode, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,37 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Strata API Monitor</title>
|
||||
<link rel="stylesheet" href="/web/tokens.css">
|
||||
<style>
|
||||
*{box-sizing:border-box}body{margin:0;background:var(--st-bg);color:var(--st-ink);font:15px/1.5 var(--st-font)}
|
||||
button,input,select{font:inherit}button{cursor:pointer;color:inherit;background:var(--st-surface-2);border:1px solid var(--st-line);border-radius:9px;padding:8px 13px}button:hover{border-color:var(--st-accent)}button:disabled{opacity:.4;cursor:default}button:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--st-focus);outline-offset:3px}[hidden]{display:none!important}
|
||||
header{height:70px;padding:0 32px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--st-line);background:var(--st-surface)}.brand{font-size:23px;font-weight:700;letter-spacing:-.5px}.brand span{font-size:13px;font-weight:400;margin-left:14px;color:var(--st-ink-muted)}main{max-width:1560px;margin:auto;padding:28px 32px}.top{display:flex;align-items:center;justify-content:space-between;gap:24px}h1{margin:0;font-size:29px;letter-spacing:-.7px}h2{font-size:17px;margin:0}.sub,.muted{color:var(--st-ink-muted)}.sub{margin:4px 0 0}.controls{display:flex;gap:8px;flex-wrap:wrap}.primary{background:var(--st-accent);color:var(--st-accent-ink);border-color:transparent}.notice{margin-top:16px;padding:10px 14px;background:var(--st-danger-tint);color:var(--st-danger-text);border-radius:9px}.cards{display:grid;grid-template-columns:2fr repeat(3,1fr);gap:14px;margin:24px 0}.card{padding:18px 20px;border:1px solid var(--st-line);border-radius:13px;background:var(--st-surface)}.label{font-size:12px;color:var(--st-ink-muted);text-transform:uppercase;letter-spacing:.8px}.value{font-size:28px;font-weight:500;line-height:1.4;margin-top:3px}.model{font:12px var(--st-font-mono);overflow-wrap:anywhere;margin-top:4px;color:var(--st-ink-muted)}.dot{display:inline-block;width:9px;height:9px;border-radius:100%;background:var(--st-ink-muted);margin-right:9px}.loaded .dot{background:var(--st-accent)}.busy .dot{background:var(--st-warn)}.panel{border:1px solid var(--st-line);border-radius:14px;background:var(--st-surface);overflow:hidden}.panel-bar{padding:15px 20px;border-bottom:1px solid var(--st-line);display:flex;align-items:center;justify-content:space-between;gap:12px}.filters{display:flex;gap:8px}input,select{color:inherit;border:1px solid var(--st-line);background:var(--st-bg);padding:7px 10px;border-radius:8px}#search{width:190px}.workspace{display:grid;grid-template-columns:minmax(340px,38%) minmax(0,1fr);min-height:480px}.requests{border-right:1px solid var(--st-line);max-height:650px;overflow:auto}.request{display:block;width:100%;border:0;border-bottom:1px solid var(--st-line-soft);border-radius:0;padding:15px 20px;text-align:left;background:transparent}.request.selected{background:var(--st-accent-tint);box-shadow:inset 3px 0 var(--st-accent)}.request:hover{background:var(--st-surface-2)}.row{display:flex;justify-content:space-between;gap:12px}.endpoint{font:12px var(--st-font-mono);margin:8px 0 5px;overflow-wrap:anywhere}.tag{font-size:11px;border-radius:5px;padding:2px 6px;background:var(--st-surface-2);color:var(--st-ink-muted)}.tag.completed{color:var(--st-accent-text);background:var(--st-accent-tint)}.tag.error{color:var(--st-danger-text);background:var(--st-danger-tint)}.tag.loading,.tag.generating,.tag.queued{color:var(--st-warn-text);background:var(--st-warn-tint)}.small{font-size:12px;color:var(--st-ink-muted)}.detail{min-width:0;padding:20px}.empty{padding:70px 24px;text-align:center;color:var(--st-ink-muted)}.empty strong{display:block;color:var(--st-ink);font-size:18px;margin-bottom:6px}.detail-title{font:13px var(--st-font-mono)}.facts{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}.fact{background:var(--st-bg);border-radius:8px;padding:9px 11px}.fact span{font-size:11px;color:var(--st-ink-muted);display:block}.fact b{font-weight:500;font-size:16px}.tabs{display:flex;align-items:center;gap:5px;border-bottom:1px solid var(--st-line);padding-bottom:10px}.tabs button{padding:6px 10px;border-color:transparent;background:transparent;font-size:13px}.tabs button[aria-selected=true]{background:var(--st-surface-2);color:var(--st-accent-text)}.tabs #copy{margin-left:auto;border-color:var(--st-line)}pre{margin:14px 0 0;padding:15px;background:var(--st-bg);border:1px solid var(--st-line-soft);border-radius:9px;font:12px/1.6 var(--st-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;max-height:400px;overflow:auto;min-height:180px}.detail-error{margin:12px 0;padding:10px 12px;border-radius:8px;background:var(--st-danger-tint);color:var(--st-danger-text);font-size:12px;overflow-wrap:anywhere}footer{margin-top:14px;font-size:12px;color:var(--st-ink-muted);display:flex;justify-content:space-between;gap:20px}#auth{margin-top:16px;padding:14px;display:flex;gap:10px;align-items:center}#auth input{flex:1;max-width:400px}
|
||||
@media(max-width:950px){.cards{grid-template-columns:repeat(2,1fr)}.workspace{grid-template-columns:1fr}.requests{border-right:0;border-bottom:1px solid var(--st-line);max-height:260px}.top{align-items:flex-start}.brand span{display:none}}@media(max-width:600px){main{padding:20px 14px}header{padding:0 16px}.top{flex-direction:column;gap:14px}.cards{gap:8px}.card{padding:12px}.value{font-size:23px}.panel-bar{align-items:flex-start;flex-direction:column}#search{width:160px}.facts{grid-template-columns:repeat(2,1fr)}footer{flex-direction:column;gap:4px}}
|
||||
</style>
|
||||
<script defer src="/web/monitor.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="brand">Strata <span>Local inference</span></div><div class="controls"><button id="key">API key</button><button id="theme" aria-label="Switch color theme">Light / dark</button></div></header>
|
||||
<main>
|
||||
<div class="top"><div><h1>API Monitor</h1><p class="sub">Model status and every request, in one place.</p></div><div class="controls"><button id="load" class="primary" disabled>Load model</button><button id="unload" disabled>Unload model</button></div></div>
|
||||
<div id="error" class="notice" role="status" hidden></div>
|
||||
<form id="auth" class="panel" hidden><label for="api-key">API key</label><input id="api-key" type="password" autocomplete="off" placeholder="Only needed if the server has a key"><button class="primary">Connect</button></form>
|
||||
<div class="cards">
|
||||
<div class="card"><div class="label">Model</div><div id="model-state" class="value"><span class="dot"></span><span id="loaded">Connecting…</span></div><div id="model" class="model">127.0.0.1:5001/v1</div></div>
|
||||
<div class="card"><div class="label">Requests retained</div><div id="count" class="value">—</div><div id="active" class="small">Waiting for server</div></div>
|
||||
<div class="card"><div class="label">Last wall-clock</div><div id="wall" class="value">—</div><div class="small">Includes queue and model loading</div></div>
|
||||
<div class="card"><div class="label">Last decode speed</div><div id="speed" class="value">—</div><div class="small">Engine timing · tokens / second</div></div>
|
||||
</div>
|
||||
<section class="panel"><div class="panel-bar"><h2>Requests</h2><div class="filters"><input id="search" aria-label="Filter requests" placeholder="Search ID or endpoint"><select id="filter" aria-label="Request status"><option value="all">All statuses</option><option value="active">Active</option><option value="completed">Completed</option><option value="error">Errors</option></select></div></div>
|
||||
<div class="workspace"><div id="requests" class="requests" aria-label="Request list"></div><div class="detail">
|
||||
<div id="no-selection" class="empty"><strong>Inspect a request</strong>Select a request to see its input, output, and timings.</div>
|
||||
<div id="selection" hidden><div class="row"><div id="detail-title" class="detail-title"></div><span id="detail-status" class="tag"></span></div><div id="detail-time" class="small"></div>
|
||||
<div class="facts"><div class="fact"><span>Wall-clock</span><b id="detail-wall">—</b></div><div class="fact"><span>Model load</span><b id="detail-load">—</b></div><div class="fact"><span>Queue wait</span><b id="detail-queue">—</b></div><div class="fact"><span>First token</span><b id="detail-first">—</b></div><div class="fact"><span>Prompt tokens</span><b id="detail-prompt">—</b></div><div class="fact"><span>Output tokens</span><b id="detail-tokens">—</b></div><div class="fact"><span>Decode speed</span><b id="detail-speed">—</b></div><div class="fact"><span>Response format</span><b id="detail-format">—</b></div></div>
|
||||
<div id="detail-error" class="detail-error" hidden></div><div class="tabs" role="tablist" aria-label="Request content"><button role="tab" data-pane="input" aria-selected="true">Input</button><button role="tab" data-pane="output" aria-selected="false">Output</button><button role="tab" data-pane="reasoning" aria-selected="false">Reasoning</button><button role="tab" data-pane="response" aria-selected="false">API response</button><button id="copy">Copy</button></div>
|
||||
<div id="content-note" class="small" style="margin-top:10px"></div><pre id="content" role="tabpanel" aria-label="Request content"></pre></div>
|
||||
</div></div></section>
|
||||
<footer><span>Last 100 requests kept in memory until restart. Inputs and outputs stay on this machine.</span><span id="updated">Connecting…</span></footer>
|
||||
</main>
|
||||
</body></html>
|
||||
@@ -0,0 +1,111 @@
|
||||
"use strict";
|
||||
const $ = id => document.getElementById(id);
|
||||
let selected = null, detail = null, pane = "input", records = [], state = null, operating = false, stamp = "", actionError = "";
|
||||
let apiKey = sessionStorage.getItem("strata.monitor.key") || "";
|
||||
document.documentElement.dataset.theme = localStorage.getItem("strata.theme") || "dark";
|
||||
const seconds = n => typeof n === "number" ? `${n.toFixed(2)} s` : "—";
|
||||
const speed = n => typeof n === "number" ? n.toFixed(1) : "—";
|
||||
const active = r => !["completed", "error", "disconnected"].includes(r.state);
|
||||
function text(id, value) { const el = $(id); if (el.textContent !== String(value)) el.textContent = value; }
|
||||
async function api(path, body) {
|
||||
const response = await fetch(path, {cache:"no-store", headers:{...(apiKey ? {Authorization:`Bearer ${apiKey}`} : {}), ...(body !== undefined ? {"Content-Type":"application/json"} : {})}, ...(body !== undefined ? {method:"POST", body:JSON.stringify(body)} : {})});
|
||||
const value = await response.json();
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) $("auth").hidden = false;
|
||||
throw new Error(value.error?.message || `HTTP ${response.status}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function notice(message) { $("error").hidden = !message; text("error", message || ""); }
|
||||
function list() {
|
||||
const query = $("search").value.toLowerCase(), filter = $("filter").value;
|
||||
const visible = records.filter(r => `${r.id} ${r.path} ${r.model}`.toLowerCase().includes(query) && (filter === "all" || (filter === "active" ? active(r) : r.state === filter)));
|
||||
const nextStamp = JSON.stringify([visible, selected]);
|
||||
if (stamp === nextStamp) return;
|
||||
stamp = nextStamp;
|
||||
const focusId = document.activeElement?.dataset.request;
|
||||
$("requests").replaceChildren();
|
||||
if (!visible.length) {
|
||||
const empty = document.createElement("div"); empty.className = "empty";
|
||||
empty.textContent = records.length ? "No matching requests." : "No requests yet. API calls will appear here automatically.";
|
||||
$("requests").append(empty);
|
||||
}
|
||||
for (const r of visible) {
|
||||
const button = document.createElement("button"); button.className = `request${selected === r.id ? " selected" : ""}`;
|
||||
button.dataset.request = r.id; button.setAttribute("aria-label", `Request ${r.id}`); button.setAttribute("aria-pressed", String(selected === r.id));
|
||||
const row = document.createElement("div"); row.className = "row";
|
||||
const time = document.createElement("span"); time.textContent = new Date(r.started_at * 1000).toLocaleTimeString();
|
||||
const tag = document.createElement("span"); tag.className = `tag ${r.state}`; tag.textContent = r.state;
|
||||
row.append(time, tag);
|
||||
const endpoint = document.createElement("div"); endpoint.className = "endpoint"; endpoint.textContent = `POST ${r.path}`;
|
||||
const meta = document.createElement("div"); meta.className = "small";
|
||||
meta.textContent = `${r.id} · ${seconds(r.wallclock_s)} · ${r.stream ? "stream" : "JSON response"}${r.http_status ? ` · HTTP ${r.http_status}` : ""}`;
|
||||
button.append(row, endpoint, meta);
|
||||
button.onclick = async () => { selected = r.id; detail = null; list(); await showDetail(); };
|
||||
$("requests").append(button);
|
||||
if (focusId === r.id) button.focus({preventScroll:true});
|
||||
}
|
||||
}
|
||||
function content() {
|
||||
if (!detail) return;
|
||||
let value = detail[pane] || (pane === "response" && detail.stream ? "Streaming response: see Output and the request error, if any." : "No content.");
|
||||
if (pane === "output") { try { value = JSON.stringify(JSON.parse(value), null, 2); } catch (_) {} }
|
||||
text("content", value);
|
||||
const note = pane === "output" && detail.error ? "Raw model output retained for diagnosis. The API returned an error." : pane === "input" ? "Original request body." : pane === "output" ? "Model answer. JSON is formatted here for readability." : pane === "reasoning" ? "Separate reasoning content, when enabled." : "API response body.";
|
||||
text("content-note", note + (detail[`${pane}_truncated`] ? " Monitor capture truncated at 256K characters; the API response was not shortened." : ""));
|
||||
}
|
||||
async function showDetail() {
|
||||
if (!selected) return;
|
||||
const id = selected;
|
||||
const value = await api(`/api/requests?id=${encodeURIComponent(id)}`);
|
||||
if (selected !== id) return;
|
||||
detail = value; $("selection").hidden = false; $("no-selection").hidden = true;
|
||||
text("detail-title", `${value.id} · ${value.path}`);
|
||||
text("detail-status", value.state); $("detail-status").className = `tag ${value.state}`;
|
||||
text("detail-time", new Date(value.started_at * 1000).toLocaleString());
|
||||
text("detail-wall", seconds(value.wallclock_s ?? ((Date.now() / 1000) - value.started_at)));
|
||||
text("detail-load", seconds(value.load_s)); text("detail-queue", seconds(value.queue_s)); text("detail-first", seconds(value.first_token_s));
|
||||
text("detail-prompt", value.usage?.prompt_tokens ?? value.usage?.input_tokens ?? "—");
|
||||
text("detail-tokens", value.usage?.completion_tokens ?? value.usage?.output_tokens ?? "—");
|
||||
text("detail-speed", speed(value.timings?.predicted_per_second)); text("detail-format", value.response_format || "text");
|
||||
$("detail-error").hidden = !value.error; text("detail-error", value.error?.message || ""); content();
|
||||
}
|
||||
async function refresh() {
|
||||
try {
|
||||
const [status, history] = await Promise.all([api("/v1/status"), api("/api/requests")]);
|
||||
state = status; records = history.requests;
|
||||
status.loaded = history.loaded; status.auto_load = history.auto_load;
|
||||
if (selected && !records.some(r => r.id === selected)) {
|
||||
selected = detail = null; $("selection").hidden = true; $("no-selection").hidden = false;
|
||||
}
|
||||
const inflight = Math.max(status.activity.in_flight, records.filter(active).length);
|
||||
const loading = records.some(r => r.state === "loading");
|
||||
text("loaded", loading ? "Loading…" : inflight ? "In use" : status.loaded ? "Loaded" : "Unloaded");
|
||||
$("model-state").className = `value ${inflight || loading ? "busy" : status.loaded ? "loaded" : ""}`;
|
||||
text("model", status.model); text("count", records.length);
|
||||
text("active", inflight ? `${inflight} active / queued` : status.auto_load ? "Loads automatically on the next request" : "No active requests");
|
||||
const last = records.find(r => !active(r));
|
||||
text("wall", last ? seconds(last.wallclock_s) : "—"); text("speed", speed(last?.timings?.predicted_per_second));
|
||||
$("load").disabled = operating || Boolean(inflight) || status.loaded; $("unload").disabled = operating || Boolean(inflight) || !status.loaded;
|
||||
list(); if (selected) await showDetail();
|
||||
text("updated", `Live · updated ${new Date().toLocaleTimeString()}`);
|
||||
if (!operating) notice(actionError);
|
||||
} catch (error) { notice(error.message); text("updated", "Disconnected · retrying"); $("load").disabled = $("unload").disabled = true; }
|
||||
}
|
||||
async function control(load) {
|
||||
actionError = "";
|
||||
operating = true; $("load").disabled = $("unload").disabled = true;
|
||||
text("loaded", load ? "Loading…" : "Unloading…");
|
||||
try { await api(load ? "/load" : "/unload", {}); notice(""); }
|
||||
catch (error) { actionError = error.message; notice(actionError); }
|
||||
finally { operating = false; await refresh(); }
|
||||
}
|
||||
$("load").onclick = () => control(true); $("unload").onclick = () => control(false);
|
||||
$("key").onclick = () => { $("auth").hidden = !$("auth").hidden; if (!$("auth").hidden) $("api-key").focus(); };
|
||||
$("auth").onsubmit = async event => { event.preventDefault(); apiKey = $("api-key").value.trim(); sessionStorage.setItem("strata.monitor.key", apiKey); $("auth").hidden = true; await refresh(); };
|
||||
$("theme").onclick = () => { const theme = document.documentElement.dataset.theme === "dark" ? "light" : "dark"; document.documentElement.dataset.theme = theme; localStorage.setItem("strata.theme", theme); };
|
||||
$("search").oninput = list; $("filter").onchange = list;
|
||||
document.querySelectorAll("[data-pane]").forEach(button => { button.onclick = () => { pane = button.dataset.pane; document.querySelectorAll("[data-pane]").forEach(b => b.setAttribute("aria-selected", String(b === button))); content(); }; });
|
||||
$("copy").onclick = async () => { try { await navigator.clipboard.writeText($("content").textContent); text("copy", "Copied"); setTimeout(() => text("copy", "Copy"), 1500); } catch (_) { notice("Clipboard unavailable; select the text to copy it."); } };
|
||||
async function poll() { await refresh(); setTimeout(poll, 2000); }
|
||||
poll();
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
# Strata for Linux: the first run installs everything and starts the model; later runs just start it.
|
||||
# Needs only an NVIDIA driver (or, experimental: an AMD RX 7900 XT/XTX with the amdgpu driver, see docs/AMD_HIP.md).
|
||||
# Needs only an NVIDIA driver (or, for an AMD Radeon card, the kernel's amdgpu driver: see docs/AMD_HIP.md).
|
||||
# Python (with venv) is installed through apt/dnf if it is missing (asks for sudo).
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
# Python 3.10+ that can make a venv WITH pip: Debian/Ubuntu ship `venv` without `ensurepip` (that is the separate
|
||||
|
||||
@@ -142,6 +142,67 @@ int main() {
|
||||
check(cache.size() == 1, "oversized put leaves cache unchanged");
|
||||
check(!cache.make_room(0, one*2+1), "held larger than budget cannot underflow");
|
||||
}
|
||||
{
|
||||
// #342: MAIN -> SUB (9 turns) -> MAIN with 4 slots. Every SUB turn parks the previous turn's live state
|
||||
// (its reply as generated, which the next request re-rendered) with a chain that holds that turn's
|
||||
// boundary checkpoint; the copy a turn back adds only that stale tail and is dropped, so MAIN survives.
|
||||
auto cp = [](std::vector<int32_t> ids) { ConversationCheckpoint c; c.ids = std::move(ids); return c; };
|
||||
auto with = [](std::vector<int32_t> prefix, std::initializer_list<int32_t> more) {
|
||||
prefix.insert(prefix.end(), more); return prefix;
|
||||
};
|
||||
const std::vector<int32_t> root = {1, 2, 3, 4};
|
||||
SavedConversation main = image({});
|
||||
main.live.ids = with(with(root, {10, 11, 12}), {13, 14});
|
||||
main.checkpoints = {cp(root), cp(with(root, {10, 11, 12}))};
|
||||
const size_t big = 1 << 20;
|
||||
ConversationCache cache(big, 4);
|
||||
check(cache.put(std::move(main)), "park MAIN");
|
||||
std::vector<int32_t> history = with(root, {20}); // SUB's conversation so far, re-rendered
|
||||
std::vector<ConversationCheckpoint> chain = {cp(root)};
|
||||
for (int turn = 1; turn <= 9; ++turn) {
|
||||
chain.push_back(cp(history)); // the turn boundary the next request resumes from
|
||||
SavedConversation sub = image({});
|
||||
sub.live.ids = with(history, {900, (int32_t) turn}); // + the reply with its thinking (stale tail)
|
||||
sub.checkpoints = chain;
|
||||
check(cache.put(std::move(sub)), "park SUB turn");
|
||||
check(cache.size() <= 2, "one parked copy of SUB at a time");
|
||||
history = with(history, {30, (int32_t) turn}); // the reply as the next request renders it
|
||||
}
|
||||
check(cache.evictions() == 0 && cache.superseded() == 8, "8 superseded copies dropped, nothing evicted");
|
||||
const auto m = cache.best(with(with(root, {10, 11, 12}), {13, 14, 15}), {}, true);
|
||||
check(m.tokens == 9 && m.live, "MAIN still restores in full");
|
||||
const auto s = cache.best(with(history, {40}), {}, true);
|
||||
check(s.tokens == (int64_t) history.size() - 2, "SUB resumes from its last turn boundary");
|
||||
}
|
||||
{
|
||||
// what is NOT superseded: another conversation sharing only the root, an entry without checkpoints, the
|
||||
// other steering mode, a branch whose deepest checkpoint the new chain does not hold
|
||||
auto cp = [](std::vector<int32_t> ids) { ConversationCheckpoint c; c.ids = std::move(ids); return c; };
|
||||
ConversationCache cache(1 << 20, 8);
|
||||
SavedConversation other = image({1, 2, 3, 4, 50, 51, 52});
|
||||
other.checkpoints = {cp({1, 2, 3, 4}), cp({1, 2, 3, 4, 50, 51})};
|
||||
cache.put(std::move(other));
|
||||
cache.put(image({1, 2, 3, 4, 7, 7})); // no checkpoints
|
||||
SavedConversation steered = image({1, 2, 3, 4, 60, 61}, false);
|
||||
steered.checkpoints = {cp({1, 2, 3, 4, 60})};
|
||||
cache.put(std::move(steered));
|
||||
SavedConversation branch = image({1, 2, 3, 4, 60, 70, 71});
|
||||
branch.checkpoints = {cp({1, 2, 3, 4}), cp({1, 2, 3, 4, 60, 70})};
|
||||
cache.put(std::move(branch));
|
||||
SavedConversation incoming = image({1, 2, 3, 4, 60, 80, 81});
|
||||
incoming.checkpoints = {cp({1, 2, 3, 4}), cp({1, 2, 3, 4, 60})};
|
||||
check(cache.put(std::move(incoming)), "park a conversation sharing roots with all of them");
|
||||
check(cache.size() == 5 && cache.superseded() == 0, "none of them is superseded");
|
||||
SavedConversation same_state = image({1, 2, 3, 4, 60, 70}); // live equal to the branch's deepest point
|
||||
same_state.checkpoints = {cp({1, 2, 3, 4})};
|
||||
check(cache.put(std::move(same_state)) && cache.superseded() == 1 && cache.size() == 5,
|
||||
"the live state equal to an entry's deepest checkpoint supersedes it");
|
||||
SavedConversation huge = image({1, 2, 3, 4, 60, 80});
|
||||
huge.checkpoints = {cp({1, 2, 3, 4}), cp({1, 2, 3, 4, 60})};
|
||||
huge.live.gdn.resize(2 << 20);
|
||||
check(!cache.put(std::move(huge)) && cache.size() == 5 && cache.superseded() == 1,
|
||||
"an oversized put drops nothing");
|
||||
}
|
||||
{
|
||||
ConversationCache disabled(0,4), no_slots(1024,0);
|
||||
check(!disabled.enabled() && !no_slots.enabled(), "both disable switches");
|
||||
|
||||
@@ -43,8 +43,12 @@ bool layout(const QsaState& st, const ModelGeometry& g, int64_t upto, bool index
|
||||
// Include the moving spare row, not only completed blocks. The checkpoint
|
||||
// restore reconstructs that row when rewinding to an earlier prefix.
|
||||
const int64_t pooled = index && upto > 0 ? upto / s.idx_block + 1 : 0;
|
||||
// Format 3 identifies snapshot K8V4 only; never pass it to the block movers.
|
||||
l = {st.kv_hybrid ? 3 : qsa_kv_format(st), cells, pooled, s.page_size, 0, 0, 0, 0, 0};
|
||||
// Format 3 identifies snapshot K8V4 only; never pass it to the block movers. Rotated INT8 K/V (#293,
|
||||
// STRATA_KV_ROT=1) is another format too (+16): its bytes mean nothing to a state that does not rotate, so a
|
||||
// snapshot or prompt checkpoint taken with the rotation never restores into one without it, nor the reverse.
|
||||
// (Q4_0 is always rotated: nothing to tell apart; without the rotation the format is the one it always was.)
|
||||
const int rotated = st.kv_rot && !st.kv_q4 && !st.kv_hybrid ? 16 : 0;
|
||||
l = {(st.kv_hybrid ? 3 : qsa_kv_format(st)) + rotated, cells, pooled, s.page_size, 0, 0, 0, 0, 0};
|
||||
using conversation_detail::product;
|
||||
if (!product(l.data, {(uint64_t) cells, (uint64_t) g.n_head_kv, per}) ||
|
||||
!product(l.scales, {(uint64_t) cells, (uint64_t) g.n_head_kv,
|
||||
|
||||
@@ -259,6 +259,17 @@ int main() {
|
||||
}
|
||||
auto bad=a; bad.head_dim++;
|
||||
check(!conversation_kv_restore(bad,f.state,f.g,upto,index,err),"reject incompatible geometry");
|
||||
if (fmt==kKvInt8) {
|
||||
// #293: INT8 K/V saved through the Hadamard rotation (STRATA_KV_ROT=1) never restores into a state
|
||||
// without it, nor the reverse
|
||||
f.state.kv_rot=true;
|
||||
ConversationKv rotated;
|
||||
check(conversation_kv_save(rotated,f.state,f.g,upto,index,err),"save rotated INT8");
|
||||
check(!conversation_kv_restore(a,f.state,f.g,upto,index,err),"unrotated image into a rotated state rejected");
|
||||
f.state.kv_rot=false;
|
||||
check(!conversation_kv_restore(rotated,f.state,f.g,upto,index,err),"rotated image into an unrotated state rejected");
|
||||
check(conversation_kv_restore(a,f.state,f.g,upto,index,err),"unrotated image still restores");
|
||||
}
|
||||
if (!a.k.empty()) {
|
||||
bad=a; bad.k.pop_back();
|
||||
check(!conversation_kv_restore(bad,f.state,f.g,upto,index,err),"reject malformed payload");
|
||||
|
||||
@@ -73,6 +73,34 @@ const char* compiled_gpu_archs() {
|
||||
#endif
|
||||
}
|
||||
|
||||
int device_count() {
|
||||
int count = 0;
|
||||
if (cudaGetDeviceCount(&count) != cudaSuccess) { // HIP without a usable device reports an error, not 0
|
||||
cudaGetLastError();
|
||||
return 0;
|
||||
}
|
||||
return count < 0 ? 0 : count;
|
||||
}
|
||||
|
||||
bool device_summary(int ordinal, std::string& name, std::string& detail) {
|
||||
cudaDeviceProp p{};
|
||||
if (ordinal < 0 || ordinal >= device_count() || cudaGetDeviceProperties(&p, ordinal) != cudaSuccess) {
|
||||
cudaGetLastError();
|
||||
return false;
|
||||
}
|
||||
char buf[160];
|
||||
#if defined(STRATA_USE_HIP)
|
||||
std::snprintf(buf, sizeof(buf), "arch %s, %.1f GiB, wave%d", base_arch(p.gcnArchName).c_str(),
|
||||
(double) p.totalGlobalMem / (1024.0 * 1024 * 1024), p.warpSize);
|
||||
#else
|
||||
std::snprintf(buf, sizeof(buf), "compute capability %d.%d, %.1f GiB", p.major, p.minor,
|
||||
(double) p.totalGlobalMem / (1024.0 * 1024 * 1024));
|
||||
#endif
|
||||
name = p.name;
|
||||
detail = buf;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string gpu_arch_problem(int ordinal) {
|
||||
#if defined(STRATA_USE_HIP)
|
||||
int count = 0;
|
||||
@@ -92,6 +120,19 @@ std::string gpu_arch_problem(int ordinal) {
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string device_code_error() {
|
||||
#if defined(STRATA_USE_HIP)
|
||||
return ""; // gpu_arch_problem() checks the HIP architectures against STRATA_HIP_ARCHS, before this point
|
||||
#else
|
||||
// every .cu of the engine is compiled for the same CMAKE_CUDA_ARCHITECTURES, so this kernel stands for all
|
||||
cudaFuncAttributes a{};
|
||||
const cudaError_t e = cudaFuncGetAttributes(&a, poison_kernel);
|
||||
if (e == cudaSuccess) return {};
|
||||
cudaGetLastError();
|
||||
return cudaGetErrorString(e);
|
||||
#endif
|
||||
}
|
||||
|
||||
DeviceInfo device_info(int ordinal) {
|
||||
int count = 0;
|
||||
check(cudaGetDeviceCount(&count), "cudaGetDeviceCount");
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#if defined(STRATA_USE_HIP) && defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
static std::string human(uint64_t b) {
|
||||
char buf[64];
|
||||
@@ -19,10 +25,14 @@ static std::string human(uint64_t b) {
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
bool selftest = false;
|
||||
bool list_devices = false;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (std::strcmp(argv[i], "--selftest") == 0) selftest = true;
|
||||
else if (std::strcmp(argv[i], "--list-devices") == 0) list_devices = true;
|
||||
else if (std::strcmp(argv[i], "--help") == 0 || std::strcmp(argv[i], "-h") == 0) {
|
||||
std::printf("usage: strata-device [--selftest]\n");
|
||||
std::printf("usage: strata-device [--selftest] [--list-devices]\n"
|
||||
" --list-devices every GPU the runtime enumerates, numbered as HIP_VISIBLE_DEVICES /\n"
|
||||
" CUDA_VISIBLE_DEVICES number them, and whether this binary can run it\n");
|
||||
return 0;
|
||||
} else {
|
||||
std::fprintf(stderr, "unknown argument: %s\n", argv[i]);
|
||||
@@ -30,6 +40,28 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
|
||||
// The runtime's numbering, which setup needs on Windows: there an integrated Radeon is HIP device 0 and pushes the
|
||||
// discrete card to 1, while setup finds the cards in the display-adapter order (#325). No arch check here - the
|
||||
// cards this binary has no code for are part of the answer. Format (setup.py's hip_devices parses it):
|
||||
// device N: <name>
|
||||
// arch gfx1201, 15.9 GiB, wave32 (CUDA: compute capability 12.0, 11.9 GiB)
|
||||
// cannot run: <why> (only for a card this binary cannot run)
|
||||
if (list_devices) {
|
||||
const int count = strata::core::device_count();
|
||||
if (count == 0) std::printf("(no GPU device)\n");
|
||||
for (int ordinal = 0; ordinal < count; ++ordinal) {
|
||||
std::string name, detail;
|
||||
if (!strata::core::device_summary(ordinal, name, detail)) {
|
||||
std::printf("device %d: (the runtime cannot describe it)\n", ordinal);
|
||||
continue;
|
||||
}
|
||||
std::printf("device %d: %s\n %s\n", ordinal, name.c_str(), detail.c_str());
|
||||
if (const std::string why = strata::core::gpu_arch_problem(ordinal); !why.empty())
|
||||
std::printf(" cannot run: %s\n", why.c_str());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
const strata::core::DeviceInfo d = strata::core::device_info(0);
|
||||
std::printf("device %d: %s\n", d.ordinal, d.name.c_str());
|
||||
@@ -42,6 +74,13 @@ int main(int argc, char** argv) {
|
||||
std::printf(" multiprocessors %d\n", d.multi_processor_count);
|
||||
std::printf(" VRAM total / free %s / %s\n", human(d.total_bytes).c_str(), human(d.free_bytes).c_str());
|
||||
std::printf(" driver / runtime %d / %d\n", d.driver_version, d.runtime_version);
|
||||
#if defined(STRATA_USE_HIP) && defined(_WIN32)
|
||||
// #468 #461: which HIP runtime this process loaded - the one beside the exe, or an AMD driver's System32 copy
|
||||
if (HMODULE h = GetModuleHandleA("amdhip64_7.dll")) {
|
||||
char path[MAX_PATH] = {};
|
||||
if (GetModuleFileNameA(h, path, MAX_PATH) > 0) std::printf(" HIP runtime %s\n", path);
|
||||
}
|
||||
#endif
|
||||
|
||||
// The planner's view against the card's. A plan that does not fit in what is actually FREE is the
|
||||
// failure this print exists to make visible at startup rather than at token 4000.
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <utility>
|
||||
#include <cstring>
|
||||
|
||||
@@ -67,6 +69,77 @@ bool read_expert_profile(const std::string& path, int64_t n_layers, int64_t n_ex
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::pair<int32_t, int32_t>> rank_learned_profile(int64_t n_layers, int64_t n_expert,
|
||||
const std::vector<uint8_t>& resident,
|
||||
const std::vector<double>& heat,
|
||||
const std::vector<std::pair<int32_t, int32_t>>& prior) {
|
||||
const size_t n = (size_t) (n_layers * n_expert);
|
||||
std::vector<int64_t> prior_rank(n, INT64_MAX);
|
||||
for (size_t r = 0; r < prior.size(); ++r) {
|
||||
const auto [l, e] = prior[r];
|
||||
if (l >= 0 && l < n_layers && e >= 0 && e < n_expert) {
|
||||
int64_t& pr = prior_rank[(size_t) (l * n_expert + e)];
|
||||
if (pr == INT64_MAX) pr = (int64_t) r;
|
||||
}
|
||||
}
|
||||
std::vector<int64_t> order(n);
|
||||
for (size_t i = 0; i < n; ++i) order[i] = (int64_t) i;
|
||||
auto res = [&](int64_t i) { return (size_t) i < resident.size() && resident[(size_t) i] != 0; };
|
||||
auto ht = [&](int64_t i) { return (size_t) i < heat.size() ? heat[(size_t) i] : 0.0; };
|
||||
std::stable_sort(order.begin(), order.end(), [&](int64_t a, int64_t b) {
|
||||
if (res(a) != res(b)) return res(a);
|
||||
if (ht(a) != ht(b)) return ht(a) > ht(b);
|
||||
if (prior_rank[(size_t) a] != prior_rank[(size_t) b]) return prior_rank[(size_t) a] < prior_rank[(size_t) b];
|
||||
return a < b;
|
||||
});
|
||||
std::vector<std::pair<int32_t, int32_t>> ranked(n);
|
||||
for (size_t r = 0; r < n; ++r)
|
||||
ranked[r] = {(int32_t) (order[r] / n_expert), (int32_t) (order[r] % n_expert)};
|
||||
return ranked;
|
||||
}
|
||||
|
||||
bool write_expert_profile(const std::string& path, int64_t n_layers, int64_t n_expert,
|
||||
const std::vector<std::pair<int32_t, int32_t>>& ranked, std::string& err) {
|
||||
if (n_layers <= 0 || n_expert <= 0 || n_layers > 65535 || n_expert > 65535) {
|
||||
err = "write_expert_profile: the model's layout does not fit the format";
|
||||
return false;
|
||||
}
|
||||
std::vector<int32_t> table((size_t) (n_layers * n_expert), -1);
|
||||
std::vector<uint16_t> pairs;
|
||||
pairs.reserve(ranked.size() * 2);
|
||||
for (size_t r = 0; r < ranked.size(); ++r) {
|
||||
const auto [l, e] = ranked[r];
|
||||
if (l < 0 || l >= n_layers || e < 0 || e >= n_expert) {
|
||||
err = "write_expert_profile: a ranked pair is out of range";
|
||||
return false;
|
||||
}
|
||||
table[(size_t) (l * n_expert + e)] = (int32_t) r;
|
||||
pairs.push_back((uint16_t) l);
|
||||
pairs.push_back((uint16_t) e);
|
||||
}
|
||||
const uint32_t hdr[5] = {1u, (uint32_t) n_layers, (uint32_t) n_expert, (uint32_t) ranked.size(),
|
||||
(uint32_t) ranked.size()};
|
||||
const std::string tmp = path + ".tmp";
|
||||
std::FILE* f = std::fopen(tmp.c_str(), "wb");
|
||||
if (f == nullptr) {
|
||||
err = "write_expert_profile: cannot create " + tmp;
|
||||
return false;
|
||||
}
|
||||
// the format is little-endian (make_profile.py's "<"): so is every machine this engine runs on
|
||||
bool ok = std::fwrite("STRP", 1, 4, f) == 4 && std::fwrite(hdr, 4, 5, f) == 5 &&
|
||||
(pairs.empty() || std::fwrite(pairs.data(), 2, pairs.size(), f) == pairs.size()) &&
|
||||
std::fwrite(table.data(), 4, table.size(), f) == table.size();
|
||||
ok = (std::fclose(f) == 0) && ok;
|
||||
std::error_code ec;
|
||||
if (ok) std::filesystem::rename(tmp, path, ec); // replaces an existing file (MoveFileEx / rename(2))
|
||||
if (!ok || ec) {
|
||||
std::filesystem::remove(tmp, ec);
|
||||
err = "write_expert_profile: cannot write " + path;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ExpertCache::~ExpertCache() { close(); }
|
||||
|
||||
#if defined(STRATA_USE_HIP)
|
||||
@@ -182,7 +255,14 @@ bool ExpertCache::open_sized(const std::vector<int64_t>& slot_bytes, int64_t n_l
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
// #369: each layer's cursor at the bottom of its own range, as open() seeds it - open() above ran on byte-sized
|
||||
// "slots", so its seeds are not slot indices
|
||||
layer_next_.assign((size_t) (n_layers > 0 ? n_layers : 0), 0);
|
||||
for (int64_t l = 0; l < n_layers; ++l) {
|
||||
int64_t lo = 0, hi = 0;
|
||||
layer_slot_range(l, lo, hi);
|
||||
layer_next_[(size_t) l] = (int32_t) lo;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1024,6 +1024,23 @@ bool FileExpertSource::pin_cache_complement(
|
||||
uint64_t bytes = 0;
|
||||
if (!detail::make_cache_complement_plan(n_layers_, n_expert_, layer_blob_bytes_, primary_gpu_pairs,
|
||||
additional_gpu_pairs, offsets, bytes, err)) return false;
|
||||
#if defined(_WIN32)
|
||||
// #467: the GPU cache's pre-fill touched its experts through the mapping (~19 GiB on a 24 GB card), and Windows
|
||||
// counts those file pages in this process's working set, not as available: a 32 GB PC read 0.44 GiB here
|
||||
// (20.7 GiB before the start). Trimmed, they move to the standby list (still cached, counted as available).
|
||||
// Resident mode only: nothing else calls this function. Locked/pinned pages stay; the rest fault back softly.
|
||||
{
|
||||
uint64_t before = 0, after = 0;
|
||||
const bool read_before = available_memory_bytes(before);
|
||||
(void) SetProcessWorkingSetSize(GetCurrentProcess(), (SIZE_T) -1, (SIZE_T) -1);
|
||||
if (read_before && available_memory_bytes(after))
|
||||
std::fprintf(stderr, "FileExpertSource: available RAM %.2f GiB, %.2f GiB after the mapped experts left the "
|
||||
"process working set (#467)\n",
|
||||
(double) before / 1073741824.0, (double) after / 1073741824.0);
|
||||
}
|
||||
#endif
|
||||
const bool what_fits = budget_bytes == kResidentWhatFits; // #467: the soft mode's second try
|
||||
uint64_t budget_physical = 0; // #403: the RAM reading a budget was sized from (0: no budget)
|
||||
if (budget_bytes > 0) {
|
||||
// CS-T: a RAM budget. The complement's experts in `rank` order (the expert profile, hottest first) while
|
||||
// they fit, the rest left on the mapped files; clamped to what the RAM has room for.
|
||||
@@ -1032,13 +1049,25 @@ bool FileExpertSource::pin_cache_complement(
|
||||
err = "FileExpertSource: cannot determine available RAM for --resident-budget-gib";
|
||||
return false;
|
||||
}
|
||||
budget_physical = physical;
|
||||
const uint64_t room = physical > headroom_bytes ? physical - headroom_bytes : 0;
|
||||
if (budget_bytes > room) {
|
||||
std::fprintf(stderr, "FileExpertSource: --resident-budget-gib %.2f is more than the RAM has room for "
|
||||
"(%.2f GiB available minus %.0f GiB headroom): %.2f GiB\n",
|
||||
(double) budget_bytes / 1073741824.0, (double) physical / 1073741824.0,
|
||||
(double) headroom_bytes / 1073741824.0, (double) room / 1073741824.0);
|
||||
budget_bytes = room;
|
||||
// #403: 256 MiB under the room, so the engine's own allocations after this reading still leave the
|
||||
// headroom (a budget clamped to exactly the room failed the safety check below on a reading a few MB
|
||||
// lower). An unclamped budget is unchanged.
|
||||
const uint64_t margin = 256ull << 20;
|
||||
const uint64_t clamped = room > margin ? room - margin : 0;
|
||||
if (what_fits)
|
||||
std::fprintf(stderr, "FileExpertSource: RAM room for the complement: %.2f GiB (%.2f GiB available "
|
||||
"minus %.0f GiB headroom and a 0.25 GiB margin)\n",
|
||||
(double) clamped / 1073741824.0, (double) physical / 1073741824.0,
|
||||
(double) headroom_bytes / 1073741824.0);
|
||||
else
|
||||
std::fprintf(stderr, "FileExpertSource: --resident-budget-gib %.2f is more than the RAM has room for "
|
||||
"(%.2f GiB available minus %.0f GiB headroom and a 0.25 GiB margin): %.2f GiB\n",
|
||||
(double) budget_bytes / 1073741824.0, (double) physical / 1073741824.0,
|
||||
(double) headroom_bytes / 1073741824.0, (double) clamped / 1073741824.0);
|
||||
budget_bytes = clamped;
|
||||
}
|
||||
std::vector<uint64_t> ranked(offsets.size(), kNoComplement);
|
||||
uint64_t at = 0;
|
||||
@@ -1054,6 +1083,10 @@ bool FileExpertSource::pin_cache_complement(
|
||||
at += b;
|
||||
++held;
|
||||
}
|
||||
if (what_fits && held == 0) { // #467: nothing to keep - the caller's plain mmap fallback, not an empty copy
|
||||
err = "FileExpertSource: the RAM has no room for any expert of the complement";
|
||||
return false;
|
||||
}
|
||||
std::fprintf(stderr, "FileExpertSource: RAM budget %.2f GiB: %lld of the %.2f GiB of experts the GPU cache does "
|
||||
"not hold, by profile rank; the rest are read from the files\n",
|
||||
(double) budget_bytes / 1073741824.0, (long long) held, (double) bytes / 1073741824.0);
|
||||
@@ -1065,8 +1098,10 @@ bool FileExpertSource::pin_cache_complement(
|
||||
const bool lend = lend_from_slot >= 0 && lend_from_slot < n_slots && additional_gpu_pairs.empty();
|
||||
uint64_t budget = std::numeric_limits<uint64_t>::max();
|
||||
if (bytes > 0 || lend) {
|
||||
uint64_t physical = 0;
|
||||
if (!available_memory_bytes(physical)) {
|
||||
// #403: with a budget, the reading it was sized from - a second reading a few MB lower (the engine's own
|
||||
// allocations, the file cache) failed a budget the first one had clamped. (A budget turns `lend` off.)
|
||||
uint64_t physical = budget_physical;
|
||||
if (physical == 0 && !available_memory_bytes(physical)) {
|
||||
err = "FileExpertSource: cannot determine available RAM for the resident-memory safety check";
|
||||
return false;
|
||||
}
|
||||
|
||||
+15
-6
@@ -22,6 +22,7 @@
|
||||
#include "strata/kernels/native_router.hpp"
|
||||
#include "strata/kernels/native_qsa.hpp"
|
||||
#include "strata/kernels/native_qsa_indexer.hpp"
|
||||
#include "strata/kernels/mrope.hpp"
|
||||
#include "strata/kernels/native_rope.hpp"
|
||||
#include "strata/kernels/native_flash_attn.hpp"
|
||||
#include "strata/kernels/fused_gr.hpp"
|
||||
@@ -172,8 +173,9 @@ if (!w.wants_q8k()) { s_gemv_q8_0_split(x80, p.codes, p.scales, p.offset,
|
||||
// namespace
|
||||
void layer_set_native_bf16(bool enabled) { native_bf16_projections = enabled; }
|
||||
void layer_set_native_flash_attn_short(bool enabled) { native_flash_attn_short = enabled; }
|
||||
namespace { bool g_kv_int8 = false, g_kv_q4 = false, g_kv_hybrid = false; }
|
||||
namespace { bool g_kv_int8 = false, g_kv_q4 = false, g_kv_hybrid = false, g_kv_int8_rot = false; }
|
||||
void qsa_set_kv_int8(bool enabled) { g_kv_int8 = enabled; }
|
||||
void qsa_set_kv_int8_rotate(bool enabled) { g_kv_int8_rot = enabled; }
|
||||
bool qsa_kv_int8() { return g_kv_int8; }
|
||||
void qsa_set_kv_q4(bool enabled) { g_kv_q4 = enabled; }
|
||||
bool qsa_kv_q4() { return g_kv_q4; }
|
||||
@@ -563,6 +565,7 @@ uint64_t qsa_state_init(const ModelGeometry& g, int64_t max_cells, void* base, Q
|
||||
st.kv_int8 = false;
|
||||
st.kv_q4 = false;
|
||||
}
|
||||
st.kv_rot = st.kv_q4 || (st.kv_int8 && g_kv_int8_rot); // K8V4 rotates only V (below)
|
||||
st.kv_mode = p.mode;
|
||||
st.n_slots = p.slots;
|
||||
const uint64_t rows = (uint64_t) p.slots * s.n_head_kv * s.page_size; // VRAM rows: the slots
|
||||
@@ -662,6 +665,9 @@ uint64_t qsa_state_init(const ModelGeometry& g, int64_t max_cells, void* base, Q
|
||||
hc.data(), hs.data());
|
||||
cudaMemcpy(st.cos_tab, hc.data(), hc.size() * 4, cudaMemcpyHostToDevice);
|
||||
cudaMemcpy(st.sin_tab, hs.data(), hs.size() * 4, cudaMemcpyHostToDevice);
|
||||
st.owns_rope = true;
|
||||
if (s.n_rot == 64) // the native and prompt-path rope kernels read it (mrope.hpp, STRATA_ROPE_TABLE=1)
|
||||
strata::kernels::rope_table_set(st.cos_tab, st.sin_tab, (int) max_cells, strata::kernels::rope_scaling());
|
||||
}
|
||||
// the page table starts as the IDENTITY, which is the simplest legal mapping and what a caller that does
|
||||
// not page at all wants; a streamed state starts with nothing resident, a ring at `block % n_slots`.
|
||||
@@ -899,11 +905,14 @@ if (st.kv_hybrid) {
|
||||
strata::kernels::fwht256_inplace_cuda(b.vcur, g.n_head_kv, stream);
|
||||
kv_append_q8_step(st.k_q, st.k_q, st.k_scale, st.k_scale, st.page_table, st.step, b.kcur, b.kcur, s, stream, nullptr); // mode 0: no host mirror
|
||||
strata::kernels::kv_append_q4_step(st.v_q4, st.v_q4, st.page_table, st.step, b.vcur, b.vcur, s, stream, nullptr);
|
||||
} else if (st.kv_q4) {
|
||||
strata::kernels::fwht256_inplace_cuda(b.kcur, g.n_head_kv, stream); // Q4_0: rotated K and V (kv_q4.hpp)
|
||||
} else {
|
||||
if (st.kv_rot) { // rotated K and V (kv_q4.hpp): Q4_0, and INT8 with STRATA_KV_ROT=1
|
||||
strata::kernels::fwht256_inplace_cuda(b.kcur, g.n_head_kv, stream);
|
||||
strata::kernels::fwht256_inplace_cuda(b.vcur, g.n_head_kv, stream);
|
||||
}
|
||||
if (st.kv_q4) {
|
||||
strata::kernels::kv_append_q4_step(st.k_q4, st.v_q4, st.page_table, st.step, b.kcur, b.vcur, s, stream, &st.host);
|
||||
} else if (st.kv_int8) kv_append_q8_step(st.k_q, st.v_q, st.k_scale, st.v_scale, st.page_table, st.step, b.kcur, b.vcur, s, stream, &st.host); else kv_append_step(st.k_pool, st.v_pool, st.page_table, st.step, b.kcur, b.vcur, s, stream, &st.host); { const uint64_t nvk = (uint64_t) g.n_head_kv * g.head_dim; const uint64_t base = (uint64_t) 2 * g.n_embd + 2 * g.hc + (uint64_t) g.n_head * g.head_dim + 2 * nvk + 8; if (!st.kv_int8 && !st.kv_hybrid) dump_slot(dump, g, layer, (const float*) st.k_pool, base, nvk / 2, stream); if (!st.kv_int8 && !st.kv_hybrid) dump_slot(dump, g, layer, (const float*) st.v_pool, base + nvk / 2, nvk / 2, stream); dump_slot(dump, g, layer, b.vcur, base + nvk, nvk, stream); dump_slot(dump, g, layer, b.kcur, base + 2 * nvk, nvk, stream); } { const QsaIndexerBuffers ib{st.idx_tail, st.idx_dead, st.idx_pooled, st.idx_block_pos}; if (native_qsa_indexer_enabled()) {
|
||||
} else if (st.kv_int8) kv_append_q8_step(st.k_q, st.v_q, st.k_scale, st.v_scale, st.page_table, st.step, b.kcur, b.vcur, s, stream, &st.host); else kv_append_step(st.k_pool, st.v_pool, st.page_table, st.step, b.kcur, b.vcur, s, stream, &st.host); } /* not K8V4 */ { const uint64_t nvk = (uint64_t) g.n_head_kv * g.head_dim; const uint64_t base = (uint64_t) 2 * g.n_embd + 2 * g.hc + (uint64_t) g.n_head * g.head_dim + 2 * nvk + 8; if (!st.kv_int8 && !st.kv_hybrid) dump_slot(dump, g, layer, (const float*) st.k_pool, base, nvk / 2, stream); if (!st.kv_int8 && !st.kv_hybrid) dump_slot(dump, g, layer, (const float*) st.v_pool, base + nvk / 2, nvk / 2, stream); dump_slot(dump, g, layer, b.vcur, base + nvk, nvk, stream); dump_slot(dump, g, layer, b.kcur, base + 2 * nvk, nvk, stream); } { const QsaIndexerBuffers ib{st.idx_tail, st.idx_dead, st.idx_pooled, st.idx_block_pos}; if (native_qsa_indexer_enabled()) {
|
||||
try {
|
||||
native_qsa_indexer_append(b.idx_raw, st.step + kStepPos, pos_base,
|
||||
(const float*) w_ikn->data, RMS_EPS, ib, s, st.max_cells, rope_scaling(), stream);
|
||||
@@ -937,7 +946,7 @@ int64_t max_blocks = (st.max_cells / s.idx_block) + 2;
|
||||
}
|
||||
// KV streaming: every block the selection names is made resident before anything reads it
|
||||
qsa_kv_resolve(st, g, b.ids, st.step, 1, cap, stream);
|
||||
if (st.kv_q4) strata::kernels::fwht256_inplace_cuda(b.qcur, g.n_head, stream); // <Hq, Hk> = <q, k>
|
||||
if (st.kv_rot) strata::kernels::fwht256_inplace_cuda(b.qcur, g.n_head, stream); // <Hq, Hk> = <q, k>
|
||||
if (g_fast_attn && !native_flash_attn_short && dump == nullptr) {
|
||||
const strata::kernels::QsaAttnPools pools = qsa_attn_pools(st);
|
||||
strata::kernels::qsa_decode_attn_step(b.qcur, pools, b.ids, st.step, cap, s, b.attn_scratch, b.attn, stream);
|
||||
@@ -964,7 +973,7 @@ int64_t max_blocks = (st.max_cells / s.idx_block) + 2;
|
||||
}
|
||||
} else qsa_attend_step(b.qcur, b.k_scratch, b.v_scratch, st.step, cap, s, b.attn, nullptr, stream);
|
||||
}
|
||||
if (st.kv_q4 || st.kv_hybrid) strata::kernels::fwht256_inplace_cuda(b.attn, g.n_head, stream); // the output back: H is self-inverse
|
||||
if (st.kv_rot || st.kv_hybrid) strata::kernels::fwht256_inplace_cuda(b.attn, g.n_head, stream); // the output back: H is self-inverse
|
||||
dump_slot(dump, g, layer, b.attn, (uint64_t) 2 * g.n_embd + 2 * g.hc, (uint64_t) g.n_head * g.head_dim, stream); { const uint64_t vs = (uint64_t) 2 * g.n_embd + 2 * g.hc + (uint64_t) g.n_head * g.head_dim + (uint64_t) 2 * g.n_head_kv * g.head_dim; dump_slot(dump, g, layer, (const float*) b.v_scratch, vs, (uint64_t) g.n_head_kv * g.head_dim / 2, stream); dump_slot(dump, g, layer, (const float*) b.ids, vs + (uint64_t) g.n_head_kv * g.head_dim / 2, 4, stream); dump_slot(dump, g, layer, (const float*) st.step, vs + (uint64_t) g.n_head_kv * g.head_dim / 2 + 4, 4, stream); }
|
||||
// ---- 9. Gate the attention output before its projection. Native CUDA keeps F32
|
||||
// sigmoid/multiply arithmetic and uses Q8_1 for the native quantized projection.
|
||||
|
||||
+47
-4
@@ -372,6 +372,45 @@ void MtpDrafter::set_draft_history(const int32_t* tail, int64_t n_tail, int32_t
|
||||
coupled_hist_base(tail, n_tail, next, h, h_chist_ + (kCoupledHistCap - h));
|
||||
}
|
||||
|
||||
namespace {
|
||||
// #474: what to change when the draft head's token subset does not fit the VRAM left - a smaller subset that setup
|
||||
// ships (data/draft_vocab_*.bin; their token counts below), and how to pick it. The start used to stop at "the draft
|
||||
// head does not fit" with no hint. Text only, after the failure: nothing changes for a start that fits.
|
||||
void draft_head_hint(int64_t n_tokens, int64_t row_bytes) {
|
||||
size_t free_b = 0, total_b = 0;
|
||||
const bool have_free = cudaMemGetInfo(&free_b, &total_b) == cudaSuccess;
|
||||
auto mib = [&](int64_t n) { return (double) (n * row_bytes) / 1048576.0; };
|
||||
std::string free_s;
|
||||
if (have_free) {
|
||||
char b[64];
|
||||
std::snprintf(b, sizeof(b), " and %.0f MiB is free", (double) free_b / 1048576.0);
|
||||
free_s = b;
|
||||
}
|
||||
std::fprintf(stderr, "strata mtp: the draft head over %lld tokens needs %.0f MiB of VRAM%s.\n",
|
||||
(long long) n_tokens, mib(n_tokens), free_s.c_str());
|
||||
struct Subset { const char* name; int64_t tokens; const char* what; };
|
||||
static const Subset smaller[] = {{"cyrillic", 58963, "English, code and the Cyrillic script"},
|
||||
{"en", 40525, "English and code"}};
|
||||
std::string opts;
|
||||
for (const Subset& s : smaller)
|
||||
if (s.tokens < n_tokens) {
|
||||
char b[160];
|
||||
std::snprintf(b, sizeof(b), "%s--draft-vocab %s (%s, ~%.0f MiB)", opts.empty() ? "" : " or ", s.name,
|
||||
s.what, mib(s.tokens));
|
||||
opts += b;
|
||||
}
|
||||
if (!opts.empty())
|
||||
std::fprintf(stderr, "strata mtp: hint: a smaller draft vocabulary needs less VRAM: %s. Start once with it - "
|
||||
"START-HERE.bat --draft-vocab en (Windows) or ./setup.sh --draft-vocab en - and the model "
|
||||
"keeps it (\"draft_vocab\" in its strata-*.json config); or a smaller --context in "
|
||||
"setup.\n",
|
||||
opts.c_str());
|
||||
else
|
||||
std::fprintf(stderr, "strata mtp: hint: this is already the smallest shipped draft vocabulary: a smaller "
|
||||
"--context (or closing what else uses the GPU) leaves it room.\n");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool MtpDrafter::bind(const WeightTable& wt, const NativeHead* head, const float* window_R, std::string& err) {
|
||||
const OnDevice on_device(device_);
|
||||
wt_ = &wt;
|
||||
@@ -395,6 +434,7 @@ bool MtpDrafter::bind(const WeightTable& wt, const NativeHead* head, const float
|
||||
if (cudaMalloc((void**) &dvocab_, raw.size()) != cudaSuccess ||
|
||||
cudaMalloc((void**) &dhead_, (size_t) (n_dvocab_ * row_bytes)) != cudaSuccess) {
|
||||
err = "mtp: the draft head does not fit";
|
||||
draft_head_hint(n_dvocab_, row_bytes);
|
||||
return false;
|
||||
}
|
||||
cudaMemcpy(dvocab_, raw.data(), raw.size(), cudaMemcpyHostToDevice);
|
||||
@@ -471,12 +511,15 @@ bool MtpDrafter::record_forward(int T, int step_row0, cudaStream_t cs, std::stri
|
||||
native_mmvq(GGML_Q8_0, q8("self_attn.v_proj.weight"), xq_, vcur_, (int) N, (int) (NKV * HD), T, cs);
|
||||
for (int t = 0; t < T; ++t) {
|
||||
norm_rope(kcur_ + t * NKV * HD, f32("self_attn.k_norm.weight"), (int) NKV, (int) HD, pos + t * NH);
|
||||
if (st_.kv_q4) { // Q4_0 KV (kv_q4.hpp): rotated K and V
|
||||
if (st_.kv_rot) { // rotated K and V (kv_q4.hpp): Q4_0, and INT8 with STRATA_KV_ROT=1
|
||||
fwht256_inplace_cuda(kcur_ + t * NKV * HD, NKV, cs);
|
||||
fwht256_inplace_cuda(vcur_ + t * NKV * HD, NKV, cs);
|
||||
}
|
||||
// stored in the state's own format (#293 appended rotated INT8 K/V as Q4_0, into pools INT8 never has)
|
||||
if (st_.kv_q4)
|
||||
kv_append_q4_step(st_.k_q4, st_.v_q4, st_.page_table, step + t * 4, kcur_ + t * NKV * HD,
|
||||
vcur_ + t * NKV * HD, s, cs, &st_.host);
|
||||
} else if (st_.kv_int8)
|
||||
else if (st_.kv_int8)
|
||||
kv_append_q8_step(st_.k_q, st_.v_q, st_.k_scale, st_.v_scale, st_.page_table, step + t * 4,
|
||||
kcur_ + t * NKV * HD, vcur_ + t * NKV * HD, s, cs, &st_.host);
|
||||
else
|
||||
@@ -493,12 +536,12 @@ bool MtpDrafter::record_forward(int T, int step_row0, cudaStream_t cs, std::stri
|
||||
return false;
|
||||
}
|
||||
norm_rope(qc, f32("self_attn.q_norm.weight"), (int) NH, (int) HD, pos + t * NH);
|
||||
if (st_.kv_q4) fwht256_inplace_cuda(qc, NH, cs);
|
||||
if (st_.kv_rot) fwht256_inplace_cuda(qc, NH, cs);
|
||||
}
|
||||
const QsaAttnPools pools = qsa_attn_pools(st_);
|
||||
if (window_ > 0) window_ids(const_cast<int32_t*>(step), T, (int) window_, ident_, cap_, cs);
|
||||
qsa_decode_attn_batch(qcur_, pools, ident_, step, cap_, s, attn_scratch_, attn_, T, cs);
|
||||
if (st_.kv_q4) fwht256_inplace_cuda(attn_, (int64_t) T * NH, cs);
|
||||
if (st_.kv_rot) fwht256_inplace_cuda(attn_, (int64_t) T * NH, cs);
|
||||
for (int t = 0; t < T; ++t)
|
||||
native_qsa_gate_apply(attn_ + t * NH * HD, qfull_ + t * NH * 2 * HD, attn32_ + t * NH * HD, (int) NH, (int) HD, cs);
|
||||
native_quantize_q8_1(attn32_, xq_, (int) (NH * HD), T, cs);
|
||||
|
||||
@@ -53,6 +53,16 @@ bool NativeDense::served_names(const std::vector<std::string>& shards, bool incl
|
||||
}
|
||||
}
|
||||
|
||||
bool NativeDense::keep_unquantized_ple_key(const std::string& pack_dir, std::set<std::string>& skip,
|
||||
std::string& err) {
|
||||
const std::string key = "blk.1.ple_key.weight";
|
||||
if (!skip.count(key)) return true;
|
||||
int code_bits = -1;
|
||||
if (!WeightTable::index_code_bits(pack_dir, key, code_bits, err)) return false;
|
||||
if (code_bits == 0) skip.erase(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
NativeDense::~NativeDense() {
|
||||
if (scratch_) cudaFree(scratch_);
|
||||
for (void* p : weights_) cudaFree(p);
|
||||
@@ -139,6 +149,8 @@ bool NativeDense::load(const std::vector<std::string>& shards, WeightTable& tabl
|
||||
auto& ref = found->second;
|
||||
if (ref.native_data) { err = "native dense: override already attached"; return false; }
|
||||
if (!strata::kernels::native_mmvq_supported(tensor.type)) continue;
|
||||
// #326: the pack keeps an unquantized (--compat-bf16) key, which the PLE reads from the arena
|
||||
if (tensor.name == "blk.1.ple_key.weight" && !ref.quantized()) continue;
|
||||
if (!ref.quantized() || tensor.shape.size() != 2 ||
|
||||
ref.ne0 <= 0 || ref.ne0 > INT_MAX || ref.ne1 <= 0 || ref.ne1 > INT_MAX ||
|
||||
tensor.shape[0] != (uint64_t) ref.ne0 || tensor.shape[1] != (uint64_t) ref.ne1) {
|
||||
|
||||
@@ -108,13 +108,15 @@ NativeEmbed::~NativeEmbed() {
|
||||
bool NativeEmbed::load(const std::vector<std::string>& shards, int64_t n_embd, int64_t n_vocab, std::string& err) {
|
||||
try {
|
||||
const strata::GgufModel model(shards);
|
||||
err = strata::check_architecture(model.meta());
|
||||
// --embd-gguf's one-tensor file (tools/embd_bf16_pack.py) says "strata-embd": only its tensor is checked
|
||||
const strata::MetaValue* arch = model.meta().get("general.architecture");
|
||||
err = arch != nullptr && arch->s == "strata-embd" ? std::string() : strata::check_architecture(model.meta());
|
||||
if (!err.empty()) { err = "native embedding: " + err; return false; }
|
||||
size_t at = 0;
|
||||
const strata::TensorInfo* t = model.find("token_embd.weight", &at);
|
||||
const strata::GgufFile& gguf = model.shard(at);
|
||||
if (!t || t->shape.size() != 2 || t->shape[0] != (uint64_t) n_embd || t->shape[1] != (uint64_t) n_vocab ||
|
||||
!strata::kernels::iq_supported((int) t->type) || n_embd % 256) {
|
||||
!strata::kernels::embed_type_supported((int) t->type) || n_embd % 256) {
|
||||
err = "native embedding: token_embd.weight is absent, of another shape, or of a type without a GPU "
|
||||
"dequantizer";
|
||||
return false;
|
||||
@@ -148,10 +150,31 @@ bool NativeEmbed::load(const std::vector<std::string>& shards, int64_t n_embd, i
|
||||
} else {
|
||||
std::memcpy(host_, gguf.tensor_data(*t), bytes_);
|
||||
void* d = nullptr;
|
||||
#if defined(STRATA_USE_HIP)
|
||||
// #325: the Windows HIP stack can refuse the device alias of a mapped allocation (and, when it gives
|
||||
// one, it is the host address itself - unified addressing; kernels read it correctly there, a
|
||||
// device-to-device copy into it does not land: tests/hip/mapped_alias). The table is only gathered from,
|
||||
// so without an alias it goes into VRAM like the unpinnable case above, instead of failing the start.
|
||||
if (cudaHostGetDevicePointer(&d, host_, 0) != cudaSuccess || d == nullptr) {
|
||||
cudaGetLastError();
|
||||
d = nullptr;
|
||||
if (cudaMalloc(&d, bytes_) != cudaSuccess ||
|
||||
cudaMemcpy(d, gguf.tensor_data(*t), bytes_, cudaMemcpyHostToDevice) != cudaSuccess) {
|
||||
if (d) cudaFree(d);
|
||||
cudaGetLastError();
|
||||
err = "native embedding: no device alias for the mapped table, and no VRAM to copy it into";
|
||||
return false;
|
||||
}
|
||||
cudaFreeHost(host_); // the destructor frees dev_ when host_ is null
|
||||
host_ = nullptr;
|
||||
std::fprintf(stderr, "strata: native embedding: no device alias for the mapped table, kept in VRAM\n");
|
||||
}
|
||||
#else
|
||||
if (cudaHostGetDevicePointer(&d, host_, 0) != cudaSuccess) {
|
||||
err = "native embedding: no device alias for the mapped table";
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
dev_ = d;
|
||||
}
|
||||
type_ = (int) t->type;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// src/core/session.cpp - one token through all 48 layers. See the header for why the graphs are per-layer.
|
||||
#include "strata/core/session.hpp"
|
||||
#include "strata/kernels/mrope.hpp"
|
||||
#include "strata/core/progress.hpp"
|
||||
|
||||
#include "strata/kernels/qsa.hpp"
|
||||
@@ -127,6 +128,14 @@ uint64_t session_init(const ModelGeometry& g, int64_t max_cells, int64_t k, void
|
||||
return used;
|
||||
}
|
||||
|
||||
void session_release(SessionState& s) {
|
||||
for (int64_t j = 0; s.qsa_states != nullptr && j < s.qsa_alloc; ++j)
|
||||
if (s.qsa_states[j].owns_rope) {
|
||||
strata::kernels::rope_table_release(s.qsa_states[j].cos_tab);
|
||||
s.qsa_states[j].owns_rope = false;
|
||||
}
|
||||
}
|
||||
|
||||
void session_zero(SessionState& s, const ModelGeometry& g, const float* R_init, void* stream) {
|
||||
cudaStream_t cs = (cudaStream_t) stream;
|
||||
// the residual: `hc` copies of the one vector a caller hands in. A real sequence's first token is the
|
||||
|
||||
+74
-13
@@ -177,6 +177,7 @@ Verifier::~Verifier() {
|
||||
if (commit_exec_) cudaGraphExecDestroy(commit_exec_);
|
||||
if (cs_) cudaStreamDestroy(cs_);
|
||||
if (copy_) { cudaStreamSynchronize(copy_); cudaStreamDestroy(copy_); }
|
||||
if (commit_done_) cudaEventDestroy(commit_done_);
|
||||
if (arena_) cudaFree(arena_);
|
||||
void* hosts[] = {h_tok_, h_step_, h_pos_, h_commit_, h_ple_, h_out_, h_x_, h_ids_, h_w_, h_seq_, h_flag_, h_ymiss_,
|
||||
h_flagA_, h_plan_, h_flagB_};
|
||||
@@ -194,6 +195,7 @@ bool Verifier::init(const WeightTable& wt, const ModelGeometry& g, SessionState&
|
||||
}
|
||||
release_gpu_fn().store(&release_live_verifiers);
|
||||
cudaGetDevice(&device_); // a layer split's stage on another GPU: its streams, graphs and buffers live there
|
||||
strata::kernels::fused_gr_check(); // once per card: which bitwise-equal hyper-connection read runs there
|
||||
wt_ = &wt;
|
||||
g_ = &g;
|
||||
ss_ = &ss;
|
||||
@@ -346,6 +348,10 @@ bool Verifier::init(const WeightTable& wt, const ModelGeometry& g, SessionState&
|
||||
err = "verify: stream create failed";
|
||||
return false;
|
||||
}
|
||||
if (cudaEventCreateWithFlags(&commit_done_, cudaEventDisableTiming) != cudaSuccess) {
|
||||
err = "verify: event create failed";
|
||||
return false;
|
||||
}
|
||||
// E-6: a layer whose routed experts are all resident is planned on the device (STRATA_VERIFY_DEVICE_PLAN=1: on;
|
||||
// exact, but neutral on RIBPC 1-2 GPUs: off by default)
|
||||
{
|
||||
@@ -567,7 +573,7 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) {
|
||||
native_mmvq(wv->native_type, wv->native_data, xq_, vcur_ + tb * NKV * HD, (int) N, (int) (NKV * HD), n, cs);
|
||||
if (qb) norm_rope(kcur_ + tb * NKV * HD, wkn, (int) (n * NKV), (int) HD, pos_k + tb * NKV);
|
||||
else for (int t = tb; t < te; ++t) norm_rope(kcur_ + t * NKV * HD, wkn, (int) NKV, (int) HD, pos_ + t * NH);
|
||||
if (st.kv_q4) { // Q4_0 KV (kv_q4.hpp): K and V rotated before they are stored
|
||||
if (st.kv_rot) { // K and V rotated before they are stored (kv_q4.hpp)
|
||||
fwht256_inplace_cuda(kcur_ + tb * NKV * HD, (int64_t) n * NKV, cs);
|
||||
fwht256_inplace_cuda(vcur_ + tb * NKV * HD, (int64_t) n * NKV, cs);
|
||||
} else if (st.kv_hybrid) { // K8V4: only V is rotated
|
||||
@@ -607,6 +613,7 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) {
|
||||
return false;
|
||||
}
|
||||
norm_rope(qcur_ + tb * NH * HD, wqn, (int) (n * NH), (int) HD, pos_ + tb * NH);
|
||||
if (st.kv_rot) fwht256_inplace_cuda(qcur_ + tb * NH * HD, (int64_t) n * NH, cs); // <Hq, Hk> = <q, k>
|
||||
bf16_gemv_fp32_mmvf_multi(mixed_ + tb * N, N, (const uint16_t*) wiq->data, qidx_ + tb * IQ * ID, IQ * ID,
|
||||
N, IQ * ID, n, cs);
|
||||
norm_rope(qidx_ + tb * IQ * ID, wiqn, (int) (n * IQ), (int) ID, pos_i + tb * IQ);
|
||||
@@ -619,7 +626,7 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) {
|
||||
return false;
|
||||
}
|
||||
norm_rope(qc, wqn, (int) NH, (int) HD, pos_ + t * NH);
|
||||
if (st.kv_q4) fwht256_inplace_cuda(qc, NH, cs); // <Hq, Hk> = <q, k>
|
||||
if (st.kv_rot) fwht256_inplace_cuda(qc, NH, cs); // <Hq, Hk> = <q, k>
|
||||
}
|
||||
for (int t = tb; t < te; ++t) {
|
||||
float* qx = qidx_ + t * IQ * ID;
|
||||
@@ -640,7 +647,7 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) {
|
||||
qsa_decode_attn_batch(qcur_ + tb * NH * HD, pools, sel_ + (size_t) tb * cap_, step_ + tb * kStepCount, cap_,
|
||||
s, attn_scratch_ + (size_t) tb * attn_scratch_floats_, attn_ + tb * NH * HD, n, cs);
|
||||
stamp(l, 13, grp);
|
||||
if (st.kv_q4 || st.kv_hybrid) fwht256_inplace_cuda(attn_ + tb * NH * HD, (int64_t) n * NH, cs); // back: H^-1 = H
|
||||
if (st.kv_rot || st.kv_hybrid) fwht256_inplace_cuda(attn_ + tb * NH * HD, (int64_t) n * NH, cs); // back: H^-1 = H
|
||||
if (qb) native_qsa_gate_apply(attn_ + tb * NH * HD, qfull_ + tb * NH * 2 * HD, attn32_ + tb * NH * HD,
|
||||
(int) (n * NH), (int) HD, cs);
|
||||
else
|
||||
@@ -857,7 +864,7 @@ std::string Verifier::profile_report() {
|
||||
"k/v+norm-rope", "kv+idx append", "q+q-idx", "scores+topk", "kv-resolve",
|
||||
"attention", "gate", "", "out-proj", "hc-read1+router", "shared+quant",
|
||||
"waitA", "VRAM hits", "waitB", "PCIe grp", "waitCPU", "copy+combine",
|
||||
"(gap)", "head", " hc0 norm", " hc0 down", " hc0 up", "", ""};
|
||||
"(gap)", "head", " hc0 norm", " hc0 down", " hc0 up", "", "", ""};
|
||||
std::string out;
|
||||
char b[80];
|
||||
double total = 0;
|
||||
@@ -1126,6 +1133,13 @@ bool Verifier::run(int T, const int32_t* tokens, int64_t pos0, PoolMultiFn pool,
|
||||
cudaMemcpy(prof_h_.data(), prof_, prof_h_.size() * 8, cudaMemcpyDeviceToHost);
|
||||
const int64_t L = g.n_layers;
|
||||
auto at = [&](int64_t l, int i) { return prof_h_[(size_t) (l * kProfPer + i)]; };
|
||||
// D8: the derived columns below read stamps the hc-read kernels write themselves or the next
|
||||
// layer's first. A slot no kernel stamped is 0 and its unsigned difference wrapped to ~1e19 ns -
|
||||
// which is why (gap)/head/hc0 printed ~1e14 ms per window. A missing or out-of-order stamp now
|
||||
// contributes nothing.
|
||||
const auto gap = [](unsigned long long to, unsigned long long from) {
|
||||
return (from != 0 && to != 0 && to >= from) ? (double) (to - from) : 0.0;
|
||||
};
|
||||
for (int64_t l = 0; l < L; ++l) {
|
||||
const int kind = is_qsa_layer(g, l) ? 1 : 0;
|
||||
unsigned long long prev = at(l, 0);
|
||||
@@ -1135,13 +1149,16 @@ bool Verifier::run(int T, const int32_t* tokens, int64_t pos0, PoolMultiFn pool,
|
||||
prof_sum_[kind][i] += (double) (x - prev);
|
||||
prev = x;
|
||||
}
|
||||
if (l + 1 < L) prof_sum_[kind][25] += (double) (at(l + 1, 0) - at(l, 24));
|
||||
prof_sum_[kind][27] += (double) (at(l, 27) - at(l, 0)); // hc-read0: norm
|
||||
prof_sum_[kind][28] += (double) (at(l, 28) - at(l, 27)); // down
|
||||
prof_sum_[kind][29] += (double) (at(l, 1) - at(l, 28)); // up
|
||||
prof_sum_[kind][1] -= (double) (at(l, 1) - at(l, 0)); // (hc-read0 shown split)
|
||||
if (l + 1 < L) prof_sum_[kind][25] += gap(at(l + 1, 0), at(l, 24));
|
||||
const double dn = gap(at(l, 27), at(l, 0)), dd = gap(at(l, 28), at(l, 27)), du = gap(at(l, 1), at(l, 28));
|
||||
if (dn > 0 && dd > 0 && du > 0) { // the split exists: show it split, not twice
|
||||
prof_sum_[kind][27] += dn; // hc-read0: norm
|
||||
prof_sum_[kind][28] += dd; // down
|
||||
prof_sum_[kind][29] += du; // up (through both halves, as before)
|
||||
prof_sum_[kind][1] -= gap(at(l, 1), at(l, 0)); // (hc-read0 shown split)
|
||||
}
|
||||
}
|
||||
prof_sum_[0][26] += (double) (at(L, 1) - at(L, 0));
|
||||
prof_sum_[0][26] += gap(at(L, 1), at(L, 0));
|
||||
++prof_windows_;
|
||||
}
|
||||
// ---- a sampled or penalized request: the head's sampling again, host-side so its parameters are this call's
|
||||
@@ -1274,14 +1291,32 @@ bool Verifier::window_logprobs(const int32_t* targets, int T, int64_t pos0, int3
|
||||
const double extra = has_extra ? (double) row[extra_id] - lse : NAN;
|
||||
const double without = has_extra && tgt != extra_id && sum_without > 0.0
|
||||
? (double) row[tgt] - (maxv + std::log(sum_without)) : NAN;
|
||||
std::fprintf(out, "%lld\t%d\t%.9f\t%lld\t%.9f\t%d\t%.9f\t%.9f\n", (long long) (pos0 + t), (int) tgt,
|
||||
std::fprintf(out, "%lld\t%d\t%.9f\t%lld\t%.9f\t%d\t%.9f\t%.9f", (long long) (pos0 + t), (int) tgt,
|
||||
(double) row[tgt] - lse, (long long) top, maxv - lse, (int) (top == (int64_t) tgt), extra,
|
||||
without);
|
||||
// STRATA_LOGPOS_TOPK=K: the K most likely tokens and their log-probabilities too (`id:logprob`), for a
|
||||
// top-k comparison with another engine on the same tokens (docs/UNSLOTH_Q4.md)
|
||||
static const int topk = [] {
|
||||
const char* v = std::getenv("STRATA_LOGPOS_TOPK");
|
||||
return v != nullptr ? std::max(0, std::min(256, std::atoi(v))) : 0;
|
||||
}();
|
||||
if (topk > 0) {
|
||||
std::vector<int32_t> order((size_t) n_vocab_);
|
||||
for (int64_t v = 0; v < n_vocab_; ++v) order[(size_t) v] = (int32_t) v;
|
||||
std::partial_sort(order.begin(), order.begin() + topk, order.end(),
|
||||
[&](int32_t a, int32_t b) { return row[a] > row[b]; });
|
||||
for (int j = 0; j < topk; ++j)
|
||||
std::fprintf(out, "\t%d:%.6f", order[(size_t) j], (double) row[order[(size_t) j]] - lse);
|
||||
}
|
||||
std::fprintf(out, "\n");
|
||||
}
|
||||
std::fflush(out);
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace { bool g_commit_async = false; }
|
||||
void Verifier::set_commit_async(bool on) { g_commit_async = on && std::getenv("STRATA_COMMIT_SYNC") == nullptr; }
|
||||
|
||||
bool Verifier::commit(int n_keep, std::string& err) {
|
||||
const OnDevice on_device(device_);
|
||||
if (n_keep < 1 || n_keep > last_t_) { err = "verify: commit count out of range"; return false; }
|
||||
@@ -1292,8 +1327,17 @@ bool Verifier::commit(int n_keep, std::string& err) {
|
||||
std::atomic_thread_fence(std::memory_order_seq_cst);
|
||||
const cudaError_t le = cudaGraphLaunch(commit_exec_, cs_);
|
||||
if (le != cudaSuccess) { err = std::string("verify: commit launch: ") + cudaGetErrorString(le); return false; }
|
||||
const cudaError_t se = cudaStreamSynchronize(cs_);
|
||||
if (se != cudaSuccess) { err = std::string("verify: commit: ") + cudaGetErrorString(se); return false; }
|
||||
// set_commit_async: no wait here - the next window runs on the same stream after it, and the drafter (its own
|
||||
// stream) reads only this window's final rows and its own K/V. h_commit_ is next written after the next window's
|
||||
// results are read, i.e. after this graph has run. Everything else waits on commit_done_ (wait_commit).
|
||||
if (!g_commit_async || next_ != nullptr) {
|
||||
const cudaError_t se = cudaStreamSynchronize(cs_);
|
||||
if (se != cudaSuccess) { err = std::string("verify: commit: ") + cudaGetErrorString(se); return false; }
|
||||
} else {
|
||||
const cudaError_t re = cudaEventRecord(commit_done_, cs_);
|
||||
if (re != cudaSuccess) { err = std::string("verify: commit event: ") + cudaGetErrorString(re); return false; }
|
||||
commit_pending_ = true;
|
||||
}
|
||||
if (ple_stage()) // stages that share one session must advance it once
|
||||
for (int t = 0; t < n_keep; ++t) {
|
||||
ss_->ple_prev[0] = ss_->ple_prev[1];
|
||||
@@ -1303,4 +1347,21 @@ bool Verifier::commit(int n_keep, std::string& err) {
|
||||
return next_ == nullptr || next_->commit(n_keep, err);
|
||||
}
|
||||
|
||||
bool Verifier::wait_commit(std::string& err) {
|
||||
if (commit_pending_) {
|
||||
const OnDevice on_device(device_);
|
||||
commit_pending_ = false;
|
||||
const cudaError_t se = cudaEventSynchronize(commit_done_);
|
||||
if (se != cudaSuccess) { err = std::string("verify: commit: ") + cudaGetErrorString(se); return false; }
|
||||
}
|
||||
return next_ == nullptr || next_->wait_commit(err);
|
||||
}
|
||||
|
||||
bool Verifier::copy_logits(int t, float* host) const {
|
||||
if (next_ != nullptr) return next_->copy_logits(t, host); // a layer split: the head is on the last stage
|
||||
if (head_logits_ == nullptr || host == nullptr || t < 0 || n_vocab_ <= 0) return false;
|
||||
return cudaMemcpy(host, head_logits_ + (size_t) t * (size_t) n_vocab_, (size_t) n_vocab_ * sizeof(float),
|
||||
cudaMemcpyDeviceToHost) == cudaSuccess;
|
||||
}
|
||||
|
||||
} // namespace strata::core
|
||||
@@ -118,6 +118,29 @@ bool WeightTable::pool_bytes(const std::string& pack_dir, uint64_t& out, std::st
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WeightTable::index_code_bits(const std::string& pack_dir, const std::string& name, int& code_bits,
|
||||
std::string& err) {
|
||||
const std::string path = pack_dir + "/index.txt";
|
||||
std::FILE* f = std::fopen(path.c_str(), "rb");
|
||||
if (!f) { err = "cannot open " + path; return false; }
|
||||
char line[1024];
|
||||
code_bits = -1;
|
||||
while (std::fgets(line, sizeof line, f)) {
|
||||
if (line[0] == '#') continue;
|
||||
char row[256] = {0};
|
||||
unsigned long long dummy = 0;
|
||||
long long ne = 0;
|
||||
int i1 = 0, i2 = 0, bits = 0;
|
||||
// the first ten fields of the row `load` parses: name file kind src_off src_bytes dst_off dst_bytes ne0 ne1 code_bits
|
||||
if (std::sscanf(line, "%255s %d %d %llu %llu %llu %llu %lld %lld %d", row, &i1, &i2, &dummy, &dummy, &dummy,
|
||||
&dummy, &ne, &ne, &bits) != 10)
|
||||
continue;
|
||||
if (name == row) { code_bits = bits; break; }
|
||||
}
|
||||
std::fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WeightTable::load(const std::string& pack_dir, void* arena_base, uint64_t arena_bytes, std::string& err,
|
||||
const std::set<std::string>* skip) {
|
||||
const std::string path = pack_dir + "/index.txt";
|
||||
|
||||
+205
-30
@@ -27,8 +27,8 @@ constexpr uint64_t pack_head(uint32_t epoch, uint32_t n, uint32_t i) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<int> physical_cores(bool skip_first) {
|
||||
std::vector<int> cores;
|
||||
CpuTopology detect_cpu_topology(bool skip_first, PoolAffinity affinity) {
|
||||
CpuTopology topo;
|
||||
#if defined(_WIN32)
|
||||
// Ask the OS rather than assuming a layout. `hardware_concurrency()` returns LOGICAL processors, and on
|
||||
// every SMT machine half of them are siblings - pinning one worker to each of the first N would put two
|
||||
@@ -36,30 +36,111 @@ std::vector<int> physical_cores(bool skip_first) {
|
||||
DWORD len = 0;
|
||||
GetLogicalProcessorInformationEx(RelationProcessorCore, nullptr, &len);
|
||||
if (len == 0) {
|
||||
for (unsigned i = 0; i < std::thread::hardware_concurrency(); ++i) cores.push_back((int) i);
|
||||
} else {
|
||||
std::vector<char> buf(len);
|
||||
if (GetLogicalProcessorInformationEx(RelationProcessorCore,
|
||||
(PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX) buf.data(), &len)) {
|
||||
const char* p = buf.data();
|
||||
const char* end = p + len;
|
||||
while (p < end) {
|
||||
const auto* e = (const SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*) p;
|
||||
if (e->Relationship == RelationProcessorCore) {
|
||||
const GROUP_AFFINITY& g = e->Processor.GroupMask[0];
|
||||
for (int bit = 0; bit < 64; ++bit)
|
||||
if (g.Mask & (1ull << bit)) { cores.push_back((int) (g.Group * 64 + bit)); break; }
|
||||
for (unsigned i = 0; i < std::thread::hardware_concurrency(); ++i) topo.worker_cores.push_back((int) i);
|
||||
if (skip_first && !topo.worker_cores.empty()) {
|
||||
topo.host_core = topo.worker_cores.front();
|
||||
topo.worker_cores.erase(topo.worker_cores.begin());
|
||||
}
|
||||
return topo;
|
||||
}
|
||||
std::vector<char> buf(len);
|
||||
if (GetLogicalProcessorInformationEx(RelationProcessorCore,
|
||||
(PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX) buf.data(), &len)) {
|
||||
const char* p = buf.data();
|
||||
const char* end = p + len;
|
||||
struct CoreDesc {
|
||||
uint8_t efficiency = 0;
|
||||
bool has_smt = false;
|
||||
std::vector<int> lps;
|
||||
};
|
||||
std::vector<CoreDesc> descs;
|
||||
while (p < end) {
|
||||
const auto* e = (const SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*) p;
|
||||
if (e->Relationship == RelationProcessorCore) {
|
||||
CoreDesc cd;
|
||||
cd.efficiency = e->Processor.EfficiencyClass;
|
||||
cd.has_smt = (e->Processor.Flags & LTP_PC_SMT) != 0;
|
||||
const GROUP_AFFINITY& g = e->Processor.GroupMask[0];
|
||||
for (int bit = 0; bit < 64; ++bit) {
|
||||
if (g.Mask & (1ull << bit)) {
|
||||
cd.lps.push_back((int) (g.Group * 64 + bit));
|
||||
}
|
||||
}
|
||||
p += e->Size;
|
||||
if (!cd.lps.empty()) {
|
||||
descs.push_back(std::move(cd));
|
||||
}
|
||||
}
|
||||
p += e->Size;
|
||||
}
|
||||
|
||||
uint8_t min_eff = 255, max_eff = 0;
|
||||
for (const auto& c : descs) {
|
||||
min_eff = (std::min)(min_eff, c.efficiency);
|
||||
max_eff = (std::max)(max_eff, c.efficiency);
|
||||
}
|
||||
|
||||
topo.is_hybrid = (max_eff > min_eff);
|
||||
if (topo.is_hybrid) {
|
||||
for (const auto& c : descs) {
|
||||
if (c.efficiency == max_eff) {
|
||||
topo.p_cores++;
|
||||
topo.p_threads += (int) c.lps.size();
|
||||
} else {
|
||||
topo.e_cores++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
topo.p_cores = (int) descs.size();
|
||||
for (const auto& c : descs) topo.p_threads += (int) c.lps.size();
|
||||
}
|
||||
|
||||
if (affinity == PoolAffinity::All || !topo.is_hybrid) {
|
||||
for (const auto& c : descs) topo.worker_cores.push_back(c.lps[0]);
|
||||
if (skip_first && !topo.worker_cores.empty()) {
|
||||
topo.host_core = topo.worker_cores.front();
|
||||
topo.worker_cores.erase(topo.worker_cores.begin());
|
||||
}
|
||||
return topo;
|
||||
}
|
||||
|
||||
// Hybrid CPU with Auto or PCores affinity:
|
||||
// Prioritize Performance cores:
|
||||
// 1. Primary logical processor of each P-core (avoids SMT resource contention)
|
||||
// 2. SMT sibling logical processors of P-cores
|
||||
// 3. E-cores (only as overflow in Auto mode)
|
||||
std::vector<int> p_primaries;
|
||||
std::vector<int> p_siblings;
|
||||
std::vector<int> e_cores;
|
||||
|
||||
for (const auto& c : descs) {
|
||||
if (c.efficiency == max_eff) {
|
||||
p_primaries.push_back(c.lps[0]);
|
||||
for (size_t s = 1; s < c.lps.size(); ++s) {
|
||||
p_siblings.push_back(c.lps[s]);
|
||||
}
|
||||
} else {
|
||||
for (int lp : c.lps) e_cores.push_back(lp);
|
||||
}
|
||||
}
|
||||
|
||||
if (skip_first && !p_primaries.empty()) {
|
||||
topo.host_core = p_primaries.front();
|
||||
p_primaries.erase(p_primaries.begin());
|
||||
}
|
||||
|
||||
for (int cpu : p_primaries) topo.worker_cores.push_back(cpu);
|
||||
for (int cpu : p_siblings) topo.worker_cores.push_back(cpu);
|
||||
if (affinity != PoolAffinity::PCores) {
|
||||
for (int cpu : e_cores) topo.worker_cores.push_back(cpu);
|
||||
}
|
||||
return topo;
|
||||
}
|
||||
#else
|
||||
// The logical CPUs this process may run on, ONE PER PHYSICAL CORE (issue #40): SMT siblings share a core's
|
||||
// load/store bandwidth, so a worker on each would put two workers on one core, as the Windows branch above
|
||||
// explains. sysfs names each CPU's (package, core); the first allowed CPU of each pair is kept, so a taskset
|
||||
// that leaves out the first sibling still gets its core. Without sysfs every allowed CPU counts, as before.
|
||||
auto topo = [](int cpu, const char* what) -> long {
|
||||
auto topo_read = [](int cpu, const char* what) -> long {
|
||||
char path[96];
|
||||
std::snprintf(path, sizeof path, "/sys/devices/system/cpu/cpu%d/topology/%s", cpu, what);
|
||||
long v = -1;
|
||||
@@ -69,6 +150,17 @@ std::vector<int> physical_cores(bool skip_first) {
|
||||
}
|
||||
return v;
|
||||
};
|
||||
auto cap_read = [](int cpu) -> long {
|
||||
char path[96];
|
||||
std::snprintf(path, sizeof path, "/sys/devices/system/cpu/cpu%d/cpu_capacity", cpu);
|
||||
long v = -1;
|
||||
if (std::FILE* f = std::fopen(path, "r")) {
|
||||
if (std::fscanf(f, "%ld", &v) != 1) v = -1;
|
||||
std::fclose(f);
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
std::vector<int> allowed;
|
||||
cpu_set_t set;
|
||||
CPU_ZERO(&set);
|
||||
@@ -78,19 +170,96 @@ std::vector<int> physical_cores(bool skip_first) {
|
||||
} else {
|
||||
for (unsigned i = 0; i < std::thread::hardware_concurrency(); ++i) allowed.push_back((int) i);
|
||||
}
|
||||
std::vector<std::pair<long, long>> seen;
|
||||
|
||||
struct CoreLinux {
|
||||
int cpu = -1;
|
||||
long pkg = -1;
|
||||
long core = -1;
|
||||
long cap = -1;
|
||||
bool is_sibling = false;
|
||||
};
|
||||
std::vector<CoreLinux> all_cpus;
|
||||
std::vector<std::pair<long, long>> seen_phys;
|
||||
long max_cap = 0, min_cap = 1000000;
|
||||
|
||||
for (int cpu : allowed) {
|
||||
const long pkg = topo(cpu, "physical_package_id"), core = topo(cpu, "core_id");
|
||||
if (pkg >= 0 && core >= 0) {
|
||||
const std::pair<long, long> key{pkg, core};
|
||||
if (std::find(seen.begin(), seen.end(), key) != seen.end()) continue; // an SMT sibling
|
||||
seen.push_back(key);
|
||||
CoreLinux cl;
|
||||
cl.cpu = cpu;
|
||||
cl.pkg = topo_read(cpu, "physical_package_id");
|
||||
cl.core = topo_read(cpu, "core_id");
|
||||
cl.cap = cap_read(cpu);
|
||||
if (cl.cap > 0) {
|
||||
max_cap = (std::max)(max_cap, cl.cap);
|
||||
min_cap = (std::min)(min_cap, cl.cap);
|
||||
}
|
||||
cores.push_back(cpu);
|
||||
if (cl.pkg >= 0 && cl.core >= 0) {
|
||||
const std::pair<long, long> key{cl.pkg, cl.core};
|
||||
if (std::find(seen_phys.begin(), seen_phys.end(), key) != seen_phys.end()) {
|
||||
cl.is_sibling = true;
|
||||
} else {
|
||||
seen_phys.push_back(key);
|
||||
}
|
||||
}
|
||||
all_cpus.push_back(cl);
|
||||
}
|
||||
|
||||
topo.is_hybrid = (max_cap > 0 && max_cap > min_cap);
|
||||
if (topo.is_hybrid) {
|
||||
for (const auto& cl : all_cpus) {
|
||||
if (cl.cap == max_cap) {
|
||||
if (!cl.is_sibling) topo.p_cores++;
|
||||
topo.p_threads++;
|
||||
} else {
|
||||
if (!cl.is_sibling) topo.e_cores++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
topo.p_cores = (int) seen_phys.size();
|
||||
topo.p_threads = (int) all_cpus.size();
|
||||
}
|
||||
|
||||
if (affinity == PoolAffinity::All || !topo.is_hybrid) {
|
||||
for (const auto& cl : all_cpus) {
|
||||
if (!cl.is_sibling) topo.worker_cores.push_back(cl.cpu);
|
||||
}
|
||||
if (skip_first && !topo.worker_cores.empty()) {
|
||||
topo.host_core = topo.worker_cores.front();
|
||||
topo.worker_cores.erase(topo.worker_cores.begin());
|
||||
}
|
||||
return topo;
|
||||
}
|
||||
|
||||
// Hybrid CPU on Linux:
|
||||
std::vector<int> p_primaries;
|
||||
std::vector<int> p_siblings;
|
||||
std::vector<int> e_cores;
|
||||
|
||||
for (const auto& cl : all_cpus) {
|
||||
if (cl.cap == max_cap) {
|
||||
if (!cl.is_sibling) p_primaries.push_back(cl.cpu);
|
||||
else p_siblings.push_back(cl.cpu);
|
||||
} else {
|
||||
e_cores.push_back(cl.cpu);
|
||||
}
|
||||
}
|
||||
|
||||
if (skip_first && !p_primaries.empty()) {
|
||||
topo.host_core = p_primaries.front();
|
||||
p_primaries.erase(p_primaries.begin());
|
||||
}
|
||||
|
||||
for (int cpu : p_primaries) topo.worker_cores.push_back(cpu);
|
||||
for (int cpu : p_siblings) topo.worker_cores.push_back(cpu);
|
||||
if (affinity != PoolAffinity::PCores) {
|
||||
for (int cpu : e_cores) topo.worker_cores.push_back(cpu);
|
||||
}
|
||||
return topo;
|
||||
#endif
|
||||
if (skip_first && !cores.empty()) cores.erase(cores.begin());
|
||||
return cores;
|
||||
return topo;
|
||||
}
|
||||
|
||||
std::vector<int> physical_cores(bool skip_first, PoolAffinity affinity) {
|
||||
return detect_cpu_topology(skip_first, affinity).worker_cores;
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -173,11 +342,17 @@ void ExpertPool::diag(std::FILE* f) const {
|
||||
std::fprintf(f, " for %lld ms\n", (long long) (now_ms() - hstate_ms_.load()));
|
||||
}
|
||||
|
||||
ExpertPool::ExpertPool(int n_workers, bool pin, bool host_works) : host_works_(host_works) {
|
||||
ExpertPool::ExpertPool(int n_workers, bool pin, bool host_works, PoolAffinity affinity)
|
||||
: host_works_(host_works), affinity_(affinity), topo_(detect_cpu_topology(true, affinity)) {
|
||||
if (const char* e = std::getenv("STRATA_POOL_SPIN_US")) // a test knob; see kSpinBeforeSleep
|
||||
spin_before_sleep_ = std::chrono::microseconds((std::max)(0, std::atoi(e)));
|
||||
const std::vector<int> cores = physical_cores(true);
|
||||
n_ = n_workers > 0 ? n_workers : (int) cores.size();
|
||||
if (n_workers > 0) {
|
||||
n_ = n_workers;
|
||||
} else if (topo_.is_hybrid && affinity_ != PoolAffinity::All) {
|
||||
n_ = (std::max)(1, topo_.p_cores - 1);
|
||||
} else {
|
||||
n_ = (int) topo_.worker_cores.size();
|
||||
}
|
||||
if (n_ < 1) n_ = 1;
|
||||
scratch_.resize((size_t) n_);
|
||||
wstate_.reset(new std::atomic<int32_t>[(size_t) n_]);
|
||||
@@ -189,7 +364,7 @@ ExpertPool::ExpertPool(int n_workers, bool pin, bool host_works) : host_works_(h
|
||||
split_multi_.resize((size_t) kMaxSplitMulti);
|
||||
threads_.reserve((size_t) n_);
|
||||
for (int i = 0; i < n_; ++i) {
|
||||
const int core = pin ? (i < (int) cores.size() ? cores[(size_t) i] : -1) : -1;
|
||||
const int core = pin ? (i < (int) topo_.worker_cores.size() ? topo_.worker_cores[(size_t) i] : -1) : -1;
|
||||
threads_.emplace_back([this, i, core] {
|
||||
pin_this_thread(core);
|
||||
worker(i);
|
||||
|
||||
@@ -67,6 +67,17 @@ __device__ __forceinline__ void group32(const uint8_t* row_blocks, int gi_in_row
|
||||
put(out, j, (float) (((b[6 + j] & 0x0F) | xh0) - 16) * d);
|
||||
put(out, j + 16, (float) (((b[6 + j] >> 4) | xh1) - 16) * d);
|
||||
}
|
||||
} else if constexpr (TYPE == 7) { // Q5_1
|
||||
const uint8_t* b = row_blocks + (size_t) gi_in_row * 24;
|
||||
const float d = h2f(b), m = h2f(b + 2);
|
||||
const uint32_t qh = (uint32_t) b[4] | ((uint32_t) b[5] << 8) |
|
||||
((uint32_t) b[6] << 16) | ((uint32_t) b[7] << 24);
|
||||
for (int j = 0; j < 16; ++j) {
|
||||
const int xh0 = ((qh >> j) & 1) << 4;
|
||||
const int xh1 = ((qh >> (j + 16)) & 1) << 4;
|
||||
put(out, j, (float) ((b[8 + j] & 15) | xh0) * d + m);
|
||||
put(out, j + 16, (float) ((b[8 + j] >> 4) | xh1) * d + m);
|
||||
}
|
||||
} else if constexpr (TYPE == 8) { // Q8_0
|
||||
const uint8_t* b = row_blocks + (size_t) gi_in_row * 34;
|
||||
const float d = h2f(b);
|
||||
@@ -167,6 +178,7 @@ bool geometry(int type, int& block_elems, int& block_bytes) {
|
||||
switch (type) {
|
||||
case 2: block_elems = 32; block_bytes = 18; return true;
|
||||
case 6: block_elems = 32; block_bytes = 22; return true;
|
||||
case 7: block_elems = 32; block_bytes = 24; return true;
|
||||
case 8: block_elems = 32; block_bytes = 34; return true;
|
||||
case 20: block_elems = 32; block_bytes = 18; return true;
|
||||
case 11: block_elems = 256; block_bytes = 110; return true;
|
||||
@@ -195,6 +207,7 @@ void launch(int type, const void* blocks, int64_t row0, int64_t rows, int64_t co
|
||||
switch (type) {
|
||||
case 2: STRATA_DQ(2);
|
||||
case 6: STRATA_DQ(6);
|
||||
case 7: STRATA_DQ(7);
|
||||
case 8: STRATA_DQ(8);
|
||||
case 11: STRATA_DQ(11);
|
||||
case 12: STRATA_DQ(12);
|
||||
|
||||
+502
-81
@@ -1,12 +1,18 @@
|
||||
// src/kernels/cuda/fused_gr.cu - see include/strata/kernels/fused_gr.hpp.
|
||||
#include "strata/core/emulate.hpp"
|
||||
#include "strata/kernels/fused_gr.hpp"
|
||||
#include "strata/kernels/bf16_bits.hpp"
|
||||
#include "strata/kernels/verify_kernels.hpp"
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace strata::kernels {
|
||||
namespace {
|
||||
@@ -489,6 +495,309 @@ __global__ void __launch_bounds__(THREADS) gr_up_v3_kernel(GrMulti m, const floa
|
||||
m.a[k].mixed[d0 + col] = sum / (float) HC;
|
||||
}
|
||||
}
|
||||
// ================================ #315: split / staged - the same arithmetic, split differently ==================
|
||||
// The multi read's variants beside the plain read (0.1.31's default; not main's opt-in STRATA_GR_V3 read, which sums
|
||||
// in another order). Every variant computes each output with exactly the plain read's operations in its order (so
|
||||
// bitwise the plain read and the single-token read); only who does what, and when, changes. `fused_gr_check`
|
||||
// compares them on the card before a verify window uses them (STRATA_HC_SPLIT below).
|
||||
// - the norm (split and staged): one block per token AND stream instead of one per token. Each thread visits
|
||||
// exactly the elements, in the order, it visited for that stream in the plain read, so every sum of squares and rs
|
||||
// is the same; then it recomputes its R' * w_norm and writes it times rs (the plain read stored the product and
|
||||
// scaled it in place: the same two roundings). On an RTX 4080 SUPER, the plain read's norm took 0.58 ms per
|
||||
// 4-token round (96 launches of 1-4 blocks), this one 0.23 ms.
|
||||
// - the down projection, split: the plain read's kernel.
|
||||
// - the down projection, staged: the plain read's 8 rows per block and lane order, but the activations arrive by
|
||||
// cp.async in half-stream tiles, two in flight, so no thread waits on a chain of loads, and they sit in shared
|
||||
// memory as two planes (floats 0-3 and 4-7 of every 8), so a warp's reads hit no bank twice. The next tile's
|
||||
// weights are loaded while the current one is used. A tile is 160 = 5 x 32 chunks of 8, so a lane still
|
||||
// accumulates its chunks lane + 32 q in ascending order, as in the plain read with either tile. Before sm_80
|
||||
// (and on HIP) the staging is a plain copy: the same bits, only not asynchronous.
|
||||
constexpr int kHcPlain = 1, kHcSplit = 2, kHcStaged = 3;
|
||||
constexpr int H_TILE = 1280; // staged tile: half a stream = 160 chunks of 8, 5 per lane
|
||||
constexpr int HQ = H_TILE / 8 / 32;
|
||||
constexpr int N_HTILES = D / H_TILE; // 8
|
||||
static_assert(N % H_TILE == 0, "a staged tile never straddles two streams");
|
||||
static_assert(H_TILE % 256 == 0, "a staged tile holds whole rounds of 32 chunks: the plain read's lane order");
|
||||
|
||||
__global__ void __launch_bounds__(THREADS) gr_norm_split_kernel(GrMulti m) {
|
||||
__shared__ float part[WARPS];
|
||||
__shared__ float s_rs;
|
||||
const FusedGrArgs& a = m.a[blockIdx.x];
|
||||
const int c = blockIdx.y;
|
||||
float* xn = m.xn + (size_t) blockIdx.x * D;
|
||||
const int t = threadIdx.x, lane = t & 31, warp = t >> 5;
|
||||
const float gw = a.apply ? 2.0f * sigmoidf_(a.inj_prev[c] / (float) HC) : 0.0f;
|
||||
float ss = 0.0f;
|
||||
for (int i = t * 4; i < D; i += THREADS * 4) {
|
||||
if (i / N != c) continue; // another stream's element: another block's
|
||||
const int d = i - c * N;
|
||||
float4 r = *reinterpret_cast<const float4*>(a.R + i);
|
||||
if (a.apply) {
|
||||
const float4 b = *reinterpret_cast<const float4*>(a.bo_prev + d);
|
||||
r.x = fmaf(b.x, gw, r.x); r.y = fmaf(b.y, gw, r.y);
|
||||
r.z = fmaf(b.z, gw, r.z); r.w = fmaf(b.w, gw, r.w);
|
||||
}
|
||||
const float sq = r.x * r.x + r.y * r.y + r.z * r.z + r.w * r.w;
|
||||
ss += sq;
|
||||
}
|
||||
const float v = warp_sum(ss);
|
||||
if (lane == 0) part[warp] = v;
|
||||
__syncthreads();
|
||||
if (t == 0) {
|
||||
float s = 0.0f;
|
||||
for (int w = 0; w < WARPS; ++w) s += part[w];
|
||||
s_rs = rsqrtf(s / (float) N + a.eps);
|
||||
a.rs[c] = s_rs;
|
||||
}
|
||||
__syncthreads();
|
||||
const float rs = s_rs;
|
||||
for (int i = t * 4; i < D; i += THREADS * 4) {
|
||||
if (i / N != c) continue;
|
||||
const int d = i - c * N;
|
||||
float4 r = *reinterpret_cast<const float4*>(a.R + i);
|
||||
if (a.apply) {
|
||||
const float4 b = *reinterpret_cast<const float4*>(a.bo_prev + d);
|
||||
r.x = fmaf(b.x, gw, r.x); r.y = fmaf(b.y, gw, r.y);
|
||||
r.z = fmaf(b.z, gw, r.z); r.w = fmaf(b.w, gw, r.w);
|
||||
}
|
||||
const float4 g = *reinterpret_cast<const float4*>(a.w_norm + i);
|
||||
const float px = r.x * g.x, py = r.y * g.y, pz = r.z * g.z, pw = r.w * g.w;
|
||||
*reinterpret_cast<float4*>(xn + i) = make_float4(px * rs, py * rs, pz * rs, pw * rs);
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 800 && !defined(__HIPCC__)
|
||||
#define STRATA_GR_CP_ASYNC 1
|
||||
#endif
|
||||
__device__ __forceinline__ void cp_async16(void* smem, const void* gmem) {
|
||||
#if defined(STRATA_GR_CP_ASYNC)
|
||||
const unsigned sa = (unsigned) __cvta_generic_to_shared(smem);
|
||||
asm volatile("cp.async.cg.shared.global [%0], [%1], 16;\n" ::"r"(sa), "l"(gmem) : "memory");
|
||||
#else
|
||||
*reinterpret_cast<float4*>(smem) = *reinterpret_cast<const float4*>(gmem);
|
||||
#endif
|
||||
}
|
||||
__device__ __forceinline__ void cp_async_commit() {
|
||||
#if defined(STRATA_GR_CP_ASYNC)
|
||||
asm volatile("cp.async.commit_group;\n" ::: "memory");
|
||||
#endif
|
||||
}
|
||||
__device__ __forceinline__ void cp_async_wait1() {
|
||||
#if defined(STRATA_GR_CP_ASYNC)
|
||||
asm volatile("cp.async.wait_group 1;\n" ::: "memory");
|
||||
#endif
|
||||
}
|
||||
__device__ __forceinline__ void cp_async_wait0() {
|
||||
#if defined(STRATA_GR_CP_ASYNC)
|
||||
asm volatile("cp.async.wait_group 0;\n" ::: "memory");
|
||||
#endif
|
||||
}
|
||||
|
||||
// `dot8` with its 8 activations as two float4: the same eight fmaf in the same order
|
||||
__device__ __forceinline__ float dot8v(const uint4 w, const float4 x0, const float4 x1) {
|
||||
float acc = 0.0f;
|
||||
acc = fmaf(__uint_as_float(w.x << 16), x0.x, acc);
|
||||
acc = fmaf(__uint_as_float(w.x & 0xffff0000u), x0.y, acc);
|
||||
acc = fmaf(__uint_as_float(w.y << 16), x0.z, acc);
|
||||
acc = fmaf(__uint_as_float(w.y & 0xffff0000u), x0.w, acc);
|
||||
acc = fmaf(__uint_as_float(w.z << 16), x1.x, acc);
|
||||
acc = fmaf(__uint_as_float(w.z & 0xffff0000u), x1.y, acc);
|
||||
acc = fmaf(__uint_as_float(w.w << 16), x1.z, acc);
|
||||
acc = fmaf(__uint_as_float(w.w & 0xffff0000u), x1.w, acc);
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Stage tile `h` of every token into `buf`: [T][2 planes][160 chunks] float4, plane 0 = floats 0-3 of a chunk.
|
||||
__device__ __forceinline__ void stage_htile(const GrMulti& m, int T, int h, float4* buf, int t) {
|
||||
for (int i = t; i < T * (H_TILE / 4); i += THREADS) {
|
||||
const int k = i / (H_TILE / 4), s4 = i - k * (H_TILE / 4); // s4: float4 of the tile, chunk s4/2, half s4&1
|
||||
const float* src = m.xn + (size_t) k * D + (size_t) h * H_TILE + (size_t) s4 * 4;
|
||||
cp_async16(buf + (size_t) k * (H_TILE / 4) + (s4 & 1) * (H_TILE / 8) + (s4 >> 1), src);
|
||||
}
|
||||
}
|
||||
|
||||
__global__ void __launch_bounds__(THREADS) gr_down_staged_kernel(GrMulti m) {
|
||||
extern __shared__ __align__(16) float4 hbuf[]; // 2 buffers x [T][2][160] float4
|
||||
const int t = threadIdx.x, lane = t & 31, warp = t >> 5;
|
||||
const int T = m.T;
|
||||
const bool inject_block = blockIdx.x == DOWN_BLOCKS;
|
||||
const int row = inject_block ? warp : blockIdx.x * WARPS + warp;
|
||||
const bool active = !(inject_block && (m.a[0].w_inject == nullptr || warp >= HC));
|
||||
const uint16_t* wrow = (inject_block ? m.a[0].w_inject : m.a[0].w_down) + (size_t) (active ? row : 0) * D;
|
||||
const uint4* w4 = reinterpret_cast<const uint4*>(wrow);
|
||||
const size_t buf_f4 = (size_t) T * (H_TILE / 4); // float4 per buffer (the second one follows the first)
|
||||
float acc[kFusedGrMaxT];
|
||||
#pragma unroll
|
||||
for (int k = 0; k < kFusedGrMaxT; ++k) acc[k] = 0.0f;
|
||||
uint4 wv[HQ], wnext[HQ];
|
||||
if (active) {
|
||||
#pragma unroll
|
||||
for (int q = 0; q < HQ; ++q) wv[q] = __ldg(w4 + lane + 32 * q);
|
||||
}
|
||||
stage_htile(m, T, 0, hbuf, t);
|
||||
cp_async_commit();
|
||||
stage_htile(m, T, 1, hbuf + buf_f4, t);
|
||||
cp_async_commit();
|
||||
#pragma unroll 1
|
||||
for (int h = 0; h < N_HTILES; ++h) {
|
||||
if (active && h + 1 < N_HTILES) {
|
||||
#pragma unroll
|
||||
for (int q = 0; q < HQ; ++q) wnext[q] = __ldg(w4 + (h + 1) * (H_TILE / 8) + lane + 32 * q);
|
||||
}
|
||||
if (h + 1 < N_HTILES) cp_async_wait1(); // tile h has landed (h + 1 may still be on its way)
|
||||
else cp_async_wait0();
|
||||
__syncthreads();
|
||||
const float4* cur = hbuf + (h & 1) * buf_f4;
|
||||
if (active) {
|
||||
#pragma unroll
|
||||
for (int q = 0; q < HQ; ++q) {
|
||||
const int j = lane + 32 * q;
|
||||
#pragma unroll
|
||||
for (int k = 0; k < kFusedGrMaxT; ++k) {
|
||||
if (k < T) {
|
||||
const float4* pk = cur + (size_t) k * (H_TILE / 4);
|
||||
acc[k] += dot8v(wv[q], pk[j], pk[H_TILE / 8 + j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
__syncthreads(); // every warp is done with buffer h & 1
|
||||
if (h + 2 < N_HTILES) stage_htile(m, T, h + 2, hbuf + (h & 1) * buf_f4, t);
|
||||
cp_async_commit(); // an empty group at the end keeps the wait counts simple
|
||||
if (h + 1 < N_HTILES) {
|
||||
#pragma unroll
|
||||
for (int q = 0; q < HQ; ++q) wv[q] = wnext[q];
|
||||
}
|
||||
}
|
||||
if (!active) return;
|
||||
float s[kFusedGrMaxT];
|
||||
#pragma unroll
|
||||
for (int k = 0; k < kFusedGrMaxT; ++k) s[k] = k < T ? warp_sum(acc[k]) : 0.0f;
|
||||
// lane k writes token k (every lane holds every sum after the xor reduction)
|
||||
#pragma unroll
|
||||
for (int k = 0; k < kFusedGrMaxT; ++k) {
|
||||
if (k >= T || lane != k) continue;
|
||||
if (inject_block) {
|
||||
m.a[k].inject_out[row] = s[k];
|
||||
} else {
|
||||
const float x = s[k] / (float) HC;
|
||||
m.a[k].lo[row] = x / (1.0f + __expf(-x));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The tokens a down kernel may carry in one launch on the current card, and the plain read's tile: the plain read
|
||||
// stages n_tok * TILEV floats (1280 on sm_75, 2560 elsewhere), staged two tiles of n_tok * H_TILE. The shared-memory
|
||||
// opt-in is set here once per device (a per-DEVICE setting: a layer split runs these kernels on two cards).
|
||||
int down_chunk(bool staged, int* tile_out) {
|
||||
static bool attr[64] = {};
|
||||
static int chunk[64] = {}; // tokens the plain down kernel may carry in one launch on this card
|
||||
static int chunk_staged[64] = {}; // the same for staged
|
||||
static int tile[64] = {}; // the plain down kernel's TILEV on this card (1280 on sm_75, 2560 elsewhere)
|
||||
int dev = 0;
|
||||
cudaGetDevice(&dev);
|
||||
if (dev < 0 || dev >= 64) {
|
||||
*tile_out = 2560;
|
||||
return kFusedGrMaxT;
|
||||
}
|
||||
if (!attr[dev]) {
|
||||
int optin = 0;
|
||||
cudaDeviceGetAttribute(&optin, cudaDevAttrMaxSharedMemoryPerBlockOptin, dev);
|
||||
optin = strata::smem_optin_of(optin); // STRATA_EMULATE_CC (tests only)
|
||||
// the down kernel stages n_tok*TILEV floats of dynamic shared memory - 80 KB at the full 8 tokens of the
|
||||
// CUDA tile. sm_75 gets the smaller tile: all eight tokens fit one 40 KiB launch there (no more slicing),
|
||||
// the TQ-5 prefetch holds half the registers, and the smaller blocks raise how many of the 41-block grid
|
||||
// share an SM. Cards whose opt-in is still below that (or that report no opt-in at all) slice the tokens;
|
||||
// the down kernel's outputs (lo, inject_out) are strictly per-token, so the chunk boundaries are safe, and
|
||||
// the up kernel below still sees every token of the batch in one launch.
|
||||
#if defined(__HIPCC__)
|
||||
const bool small_tile = true; // all eight tokens fit gfx1100's 64 KiB LDS at this tile
|
||||
#else
|
||||
int cc_maj = 0, cc_min = 0;
|
||||
cudaDeviceGetAttribute(&cc_maj, cudaDevAttrComputeCapabilityMajor, dev);
|
||||
cudaDeviceGetAttribute(&cc_min, cudaDevAttrComputeCapabilityMinor, dev);
|
||||
const bool small_tile = strata::cc_major_of(cc_maj) * 10 + strata::cc_minor_of(cc_min) == 75;
|
||||
#endif
|
||||
const int tv = small_tile ? 1280 : 2560;
|
||||
tile[dev] = tv;
|
||||
int want = (int) (kFusedGrMaxT * tv * sizeof(float));
|
||||
if (optin > 0 && want > optin) want = optin;
|
||||
if (small_tile) {
|
||||
cudaFuncSetAttribute(gr_down_multi_kernel<1280>, cudaFuncAttributeMaxDynamicSharedMemorySize, want);
|
||||
} else {
|
||||
cudaFuncSetAttribute(gr_down_multi_kernel<2560>, cudaFuncAttributeMaxDynamicSharedMemorySize, want);
|
||||
}
|
||||
int want_staged = (int) (2 * kFusedGrMaxT * H_TILE * sizeof(float));
|
||||
if (optin > 0 && want_staged > optin) want_staged = optin;
|
||||
cudaFuncSetAttribute(gr_down_staged_kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, want_staged);
|
||||
cudaGetLastError(); // drop any error the attempt left behind
|
||||
// The opt-in is a promise a pre-Volta card does not keep: an sm_60 answers 65536 and accepts the
|
||||
// cudaFuncSetAttribute for 61440 B, then fails the LAUNCH with "invalid argument". What such a card
|
||||
// will launch is its per-block limit, so the capacity comes from that below sm_70 - the same tokens
|
||||
// the "no opt-in" branch assumes, but taken from the attribute that is actually enforced.
|
||||
#if defined(__HIPCC__)
|
||||
const int usable = optin > 0 ? optin : 48 * 1024;
|
||||
#else
|
||||
int cc = 0, per_block = 0;
|
||||
cudaDeviceGetAttribute(&cc, cudaDevAttrComputeCapabilityMajor, dev);
|
||||
cudaDeviceGetAttribute(&per_block, cudaDevAttrMaxSharedMemoryPerBlock, dev);
|
||||
cc = strata::cc_major_of(cc);
|
||||
const int usable = (cc >= 7 && optin > 0) ? optin : per_block;
|
||||
#endif
|
||||
const int capacity = usable / (int) (tv * sizeof(float));
|
||||
chunk[dev] = capacity < 1 ? 1 : (capacity > kFusedGrMaxT ? kFusedGrMaxT : capacity);
|
||||
const int capacity_staged = usable / (int) (2 * H_TILE * sizeof(float));
|
||||
chunk_staged[dev] = capacity_staged < 1 ? 1 : (capacity_staged > kFusedGrMaxT ? kFusedGrMaxT : capacity_staged);
|
||||
attr[dev] = true;
|
||||
}
|
||||
*tile_out = tile[dev] ? tile[dev] : 2560;
|
||||
return staged ? chunk_staged[dev] : chunk[dev];
|
||||
}
|
||||
|
||||
// The multi read as `variant` (kHcPlain, kHcSplit or kHcStaged): the norm, the down projection in launches of as
|
||||
// many tokens as fit the card, the up projection; the profile's stamps after the norm and after the down projection.
|
||||
// kHcPlain is the default read exactly as before (never main's opt-in STRATA_GR_V3 path, which fused_gr_read_multi
|
||||
// takes first).
|
||||
void launch_multi(const GrMulti& m, int variant, cudaStream_t st, unsigned long long* stamp_buf, int stamp_i0) {
|
||||
const int n_tok = m.T;
|
||||
if (variant >= kHcSplit) gr_norm_split_kernel<<<dim3((unsigned) n_tok, HC), THREADS, 0, st>>>(m);
|
||||
else gr_norm_multi_kernel<<<n_tok, THREADS, 0, st>>>(m);
|
||||
if (stamp_buf) gpu_stamp(stamp_buf, stamp_i0, (void*) st);
|
||||
const bool staged = variant >= kHcStaged;
|
||||
int tv = 2560;
|
||||
const int chunk_tok = down_chunk(staged, &tv);
|
||||
const size_t per_tok = (staged ? (size_t) 2 * H_TILE : (size_t) tv) * sizeof(float);
|
||||
for (int c0 = 0; c0 < n_tok; c0 += chunk_tok) {
|
||||
const int ct = n_tok - c0 < chunk_tok ? n_tok - c0 : chunk_tok;
|
||||
GrMulti c{};
|
||||
if (ct == n_tok) {
|
||||
c = m;
|
||||
} else {
|
||||
c.xn = m.xn + (size_t) c0 * D;
|
||||
c.T = ct;
|
||||
for (int k = 0; k < ct; ++k) c.a[k] = m.a[c0 + k];
|
||||
}
|
||||
const size_t smem = (size_t) ct * per_tok;
|
||||
if (staged) gr_down_staged_kernel<<<DOWN_BLOCKS + 1, THREADS, smem, st>>>(c);
|
||||
else if (tv == 1280) gr_down_multi_kernel<1280><<<DOWN_BLOCKS + 1, THREADS, smem, st>>>(c);
|
||||
else gr_down_multi_kernel<2560><<<DOWN_BLOCKS + 1, THREADS, smem, st>>>(c);
|
||||
}
|
||||
if (stamp_buf) gpu_stamp(stamp_buf, stamp_i0 + 1, (void*) st);
|
||||
gr_up_multi_kernel<<<UPM_BLOCKS, THREADS, 0, st>>>(m);
|
||||
}
|
||||
|
||||
/// STRATA_HC_SPLIT: unset or 2 = the newest the check accepts (staged), 1 = at most split, 0 = the plain read
|
||||
int env_variant() {
|
||||
const char* e = std::getenv("STRATA_HC_SPLIT");
|
||||
if (e == nullptr || e[0] == '\0') return kHcStaged;
|
||||
if (e[0] == '0') return kHcPlain;
|
||||
if (e[0] == '1') return kHcSplit;
|
||||
return kHcStaged;
|
||||
}
|
||||
|
||||
// per device: the variant `fused_gr_check` chose (0 = not checked yet)
|
||||
std::atomic<int> g_variant[64];
|
||||
|
||||
} // namespace
|
||||
|
||||
void fused_gr_read_multi(const FusedGrArgs* a, int n_tok, float* xn_scratch, void* stream, unsigned long long* stamp_buf,
|
||||
@@ -526,9 +835,11 @@ void fused_gr_read_multi(const FusedGrArgs* a, int n_tok, float* xn_scratch, voi
|
||||
if (need1 <= limit &&
|
||||
cudaFuncSetAttribute(gr_down_v3_kernel<1>, cudaFuncAttributeMaxDynamicSharedMemorySize, need1) == cudaSuccess)
|
||||
split = 1;
|
||||
else if (need2 <= limit &&
|
||||
cudaFuncSetAttribute(gr_down_v3_kernel<2>, cudaFuncAttributeMaxDynamicSharedMemorySize, need2) == cudaSuccess)
|
||||
split = 2;
|
||||
else if (need2 <= limit)
|
||||
// #375 (kenh0u): the S = 2 split (a 64 KB opt-in card: Turing) disagrees with itself in gr_parity
|
||||
// (graph replay vs direct call) - such a card keeps the default read until that split is fixed
|
||||
std::fprintf(stderr, "strata: STRATA_GR_V3=1 needs the two-half split on this card, which fails its "
|
||||
"checks (#375): the default read is used\n");
|
||||
cudaGetLastError(); // drop any error the attempts left behind
|
||||
split3[dev3] = split;
|
||||
}
|
||||
@@ -551,84 +862,8 @@ void fused_gr_read_multi(const FusedGrArgs* a, int n_tok, float* xn_scratch, voi
|
||||
}
|
||||
return;
|
||||
}
|
||||
gr_norm_multi_kernel<<<n_tok, THREADS, 0, st>>>(m);
|
||||
if (stamp_buf) gpu_stamp(stamp_buf, stamp_i0, stream);
|
||||
// the shared-memory opt-in is a per-DEVICE setting: once per device, not once per process (a layer split
|
||||
// runs this kernel on two cards)
|
||||
static bool attr[64] = {};
|
||||
static int chunk[64] = {}; // tokens the down kernel may carry in one launch on this card
|
||||
static int tile[64] = {}; // the down kernel's TILEV on this card (1280 on sm_75, 2560 elsewhere; see above)
|
||||
int dev = 0;
|
||||
cudaGetDevice(&dev);
|
||||
if (dev >= 0 && dev < 64 && !attr[dev]) {
|
||||
int optin = 0;
|
||||
cudaDeviceGetAttribute(&optin, cudaDevAttrMaxSharedMemoryPerBlockOptin, dev);
|
||||
// the down kernel stages n_tok*TILEV floats of dynamic shared memory - 80 KB at the full 8 tokens of the
|
||||
// CUDA tile. sm_75 gets the smaller tile: all eight tokens fit one 40 KiB launch there (no more slicing),
|
||||
// the TQ-5 prefetch holds half the registers, and the smaller blocks raise how many of the 41-block grid
|
||||
// share an SM. Cards whose opt-in is still below that (or that report no opt-in at all) slice the tokens;
|
||||
// the down kernel's outputs (lo, inject_out) are strictly per-token, so the chunk boundaries are safe, and
|
||||
// the up kernel below still sees every token of the batch in one launch.
|
||||
#if defined(__HIPCC__)
|
||||
const bool small_tile = true; // all eight tokens fit gfx1100's 64 KiB LDS at this tile
|
||||
#else
|
||||
int cc_maj = 0, cc_min = 0;
|
||||
cudaDeviceGetAttribute(&cc_maj, cudaDevAttrComputeCapabilityMajor, dev);
|
||||
cudaDeviceGetAttribute(&cc_min, cudaDevAttrComputeCapabilityMinor, dev);
|
||||
const bool small_tile = cc_maj * 10 + cc_min == 75;
|
||||
#endif
|
||||
const int tv = small_tile ? 1280 : 2560;
|
||||
tile[dev] = tv;
|
||||
int want = (int) (kFusedGrMaxT * tv * sizeof(float));
|
||||
if (optin > 0 && want > optin) want = optin;
|
||||
if (small_tile) {
|
||||
cudaFuncSetAttribute(gr_down_multi_kernel<1280>, cudaFuncAttributeMaxDynamicSharedMemorySize, want);
|
||||
} else {
|
||||
cudaFuncSetAttribute(gr_down_multi_kernel<2560>, cudaFuncAttributeMaxDynamicSharedMemorySize, want);
|
||||
}
|
||||
cudaGetLastError(); // drop any error the attempt left behind
|
||||
// The opt-in is a promise a pre-Volta card does not keep: an sm_60 answers 65536 and accepts the
|
||||
// cudaFuncSetAttribute for 61440 B, then fails the LAUNCH with "invalid argument". What such a card
|
||||
// will launch is its per-block limit, so the capacity comes from that below sm_70 - the same tokens
|
||||
// the "no opt-in" branch assumes, but taken from the attribute that is actually enforced.
|
||||
#if defined(__HIPCC__)
|
||||
const int usable = optin > 0 ? optin : 48 * 1024;
|
||||
#else
|
||||
int cc = 0, per_block = 0;
|
||||
cudaDeviceGetAttribute(&cc, cudaDevAttrComputeCapabilityMajor, dev);
|
||||
cudaDeviceGetAttribute(&per_block, cudaDevAttrMaxSharedMemoryPerBlock, dev);
|
||||
const int usable = (cc >= 7 && optin > 0) ? optin : per_block;
|
||||
#endif
|
||||
const int capacity = usable / (int) (tv * sizeof(float));
|
||||
chunk[dev] = capacity < 1 ? 1 : (capacity > kFusedGrMaxT ? kFusedGrMaxT : capacity);
|
||||
attr[dev] = true;
|
||||
}
|
||||
const int tv = (dev >= 0 && dev < 64 && tile[dev]) ? tile[dev] : 2560;
|
||||
const int chunk_tok = (dev >= 0 && dev < 64 && chunk[dev]) ? chunk[dev] : kFusedGrMaxT;
|
||||
if (chunk_tok >= n_tok) {
|
||||
const size_t smem = (size_t) n_tok * tv * sizeof(float);
|
||||
if (tv == 1280) {
|
||||
gr_down_multi_kernel<1280><<<DOWN_BLOCKS + 1, THREADS, smem, st>>>(m);
|
||||
} else {
|
||||
gr_down_multi_kernel<2560><<<DOWN_BLOCKS + 1, THREADS, smem, st>>>(m);
|
||||
}
|
||||
} else {
|
||||
for (int c0 = 0; c0 < n_tok; c0 += chunk_tok) {
|
||||
const int ct = n_tok - c0 < chunk_tok ? n_tok - c0 : chunk_tok;
|
||||
GrMulti c{};
|
||||
c.xn = xn_scratch + (size_t) c0 * D;
|
||||
c.T = ct;
|
||||
for (int k = 0; k < ct; ++k) c.a[k] = a[c0 + k];
|
||||
const size_t smem = (size_t) ct * tv * sizeof(float);
|
||||
if (tv == 1280) {
|
||||
gr_down_multi_kernel<1280><<<DOWN_BLOCKS + 1, THREADS, smem, st>>>(c);
|
||||
} else {
|
||||
gr_down_multi_kernel<2560><<<DOWN_BLOCKS + 1, THREADS, smem, st>>>(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stamp_buf) gpu_stamp(stamp_buf, stamp_i0 + 1, stream);
|
||||
gr_up_multi_kernel<<<UPM_BLOCKS, THREADS, 0, st>>>(m);
|
||||
// the default read (STRATA_GR_V3 unset): v1, or the bitwise-equal v2 / v3 this card's check accepted
|
||||
launch_multi(m, fused_gr_variant(), st, stamp_buf, stamp_i0);
|
||||
const cudaError_t e = cudaGetLastError();
|
||||
if (e != cudaSuccess) {
|
||||
std::fprintf(stderr, "fused_gr_read_multi: %s\n", cudaGetErrorString(e));
|
||||
@@ -657,4 +892,190 @@ void fused_gr_read(const FusedGrArgs& a, void* stream) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
/// The plain read, split, staged and (for one token) the single-token read on random bf16 weights and random inputs,
|
||||
/// 1..8 tokens, with and without the pending write; every output of split and staged compared with the plain read's
|
||||
/// bit for bit (and the plain read's with the single-token read's). `why[v]` gets the first difference of variant
|
||||
/// v; false if the check itself could not run.
|
||||
bool fused_gr_selftest(bool ok_variant[4], std::string why[4]) {
|
||||
constexpr int TM = kFusedGrMaxT;
|
||||
constexpr int NV = 4; // sets: 0 = plain, 1 = split, 2 = staged, 3 = single-token
|
||||
std::mt19937 rng(20260930u);
|
||||
std::normal_distribution<float> nd(0.0f, 1.0f);
|
||||
std::vector<uint16_t> h_down((size_t) LR * D), h_up((size_t) D * LR), h_inj((size_t) HC * D);
|
||||
std::vector<float> h_norm(D), h_R((size_t) TM * D), h_bo((size_t) TM * N), h_ip((size_t) TM * HC);
|
||||
for (auto& w : h_down) w = bf16_from_f32(0.02f * nd(rng));
|
||||
for (auto& w : h_up) w = bf16_from_f32(0.05f * nd(rng));
|
||||
for (auto& w : h_inj) w = bf16_from_f32(0.02f * nd(rng));
|
||||
for (auto& w : h_norm) w = 1.0f + 0.1f * nd(rng);
|
||||
for (auto& x : h_R) x = nd(rng);
|
||||
for (auto& x : h_bo) x = 0.5f * nd(rng);
|
||||
for (auto& x : h_ip) x = 2.0f * nd(rng);
|
||||
for (int v = 0; v < 4; ++v) { ok_variant[v] = v == 1; why[v].clear(); }
|
||||
|
||||
const size_t n_set = (size_t) TM * (D + D + LR + HC + HC + N); // R_out, xn, lo, rs, inject, mixed (floats)
|
||||
const size_t bytes = h_down.size() * 2 + h_up.size() * 2 + h_inj.size() * 2 +
|
||||
(h_norm.size() + h_R.size() + h_bo.size() + h_ip.size() + NV * n_set) * 4 + 32 * 256;
|
||||
uint8_t* base = nullptr;
|
||||
if (cudaMalloc((void**) &base, bytes) != cudaSuccess) {
|
||||
cudaGetLastError();
|
||||
why[0] = "no room for the check (" + std::to_string(bytes >> 20) + " MiB)";
|
||||
return false;
|
||||
}
|
||||
size_t off = 0;
|
||||
auto take = [&](size_t b) { void* p = base + off; off += (b + 255) / 256 * 256; return p; };
|
||||
uint16_t* d_down = (uint16_t*) take(h_down.size() * 2);
|
||||
uint16_t* d_up = (uint16_t*) take(h_up.size() * 2);
|
||||
uint16_t* d_inj = (uint16_t*) take(h_inj.size() * 2);
|
||||
float* d_norm = (float*) take(h_norm.size() * 4);
|
||||
float* d_R = (float*) take(h_R.size() * 4);
|
||||
float* d_bo = (float*) take(h_bo.size() * 4);
|
||||
float* d_ip = (float*) take(h_ip.size() * 4);
|
||||
struct Set { float *R_out, *xn, *lo, *rs, *inj, *mixed; } set[NV];
|
||||
for (Set& x : set) {
|
||||
x.R_out = (float*) take((size_t) TM * D * 4); x.xn = (float*) take((size_t) TM * D * 4);
|
||||
x.lo = (float*) take((size_t) TM * LR * 4); x.rs = (float*) take((size_t) TM * HC * 4);
|
||||
x.inj = (float*) take((size_t) TM * HC * 4); x.mixed = (float*) take((size_t) TM * N * 4);
|
||||
}
|
||||
cudaStream_t st = nullptr;
|
||||
bool ok = off <= bytes && cudaStreamCreateWithFlags(&st, cudaStreamNonBlocking) == cudaSuccess &&
|
||||
cudaMemcpyAsync(d_down, h_down.data(), h_down.size() * 2, cudaMemcpyHostToDevice, st) == cudaSuccess &&
|
||||
cudaMemcpyAsync(d_up, h_up.data(), h_up.size() * 2, cudaMemcpyHostToDevice, st) == cudaSuccess &&
|
||||
cudaMemcpyAsync(d_inj, h_inj.data(), h_inj.size() * 2, cudaMemcpyHostToDevice, st) == cudaSuccess &&
|
||||
cudaMemcpyAsync(d_norm, h_norm.data(), h_norm.size() * 4, cudaMemcpyHostToDevice, st) == cudaSuccess &&
|
||||
cudaMemcpyAsync(d_R, h_R.data(), h_R.size() * 4, cudaMemcpyHostToDevice, st) == cudaSuccess &&
|
||||
cudaMemcpyAsync(d_bo, h_bo.data(), h_bo.size() * 4, cudaMemcpyHostToDevice, st) == cudaSuccess &&
|
||||
cudaMemcpyAsync(d_ip, h_ip.data(), h_ip.size() * 4, cudaMemcpyHostToDevice, st) == cudaSuccess;
|
||||
if (!ok) why[0] = "setting up the check failed";
|
||||
std::vector<float> h1, h2;
|
||||
// true when equal; false with the first difference in `w` (or a read-back failure in `ok`)
|
||||
auto same = [&](const float* d1, const float* d2, size_t n, const char* what, int T, int apply, std::string& w) {
|
||||
h1.resize(n);
|
||||
h2.resize(n);
|
||||
if (cudaMemcpyAsync(h1.data(), d1, n * 4, cudaMemcpyDeviceToHost, st) != cudaSuccess ||
|
||||
cudaMemcpyAsync(h2.data(), d2, n * 4, cudaMemcpyDeviceToHost, st) != cudaSuccess ||
|
||||
cudaStreamSynchronize(st) != cudaSuccess) {
|
||||
why[0] = "reading back the check failed";
|
||||
ok = false;
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
uint32_t u1, u2;
|
||||
std::memcpy(&u1, &h1[i], 4);
|
||||
std::memcpy(&u2, &h2[i], 4);
|
||||
if (u1 != u2) {
|
||||
char buf[192];
|
||||
std::snprintf(buf, sizeof buf, "%s differs for %d token(s)%s at %zu (%08x, not %08x)", what, T,
|
||||
apply ? " with the pending write" : "", i, u2, u1);
|
||||
w = buf;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
auto all_same = [&](const Set& s1, const Set& s2, int T, int apply, std::string& w) {
|
||||
return same(s1.lo, s2.lo, (size_t) T * LR, "lo", T, apply, w) &&
|
||||
same(s1.rs, s2.rs, (size_t) T * HC, "rs", T, apply, w) &&
|
||||
same(s1.inj, s2.inj, (size_t) T * HC, "inject", T, apply, w) &&
|
||||
same(s1.mixed, s2.mixed, (size_t) T * N, "mixed", T, apply, w) &&
|
||||
same(s1.R_out, s2.R_out, (size_t) T * D, "R", T, apply, w);
|
||||
};
|
||||
ok_variant[2] = ok_variant[3] = ok;
|
||||
bool single_ok = ok;
|
||||
for (int apply = 0; apply < 2 && ok; ++apply) {
|
||||
for (int T = 1; T <= TM && ok; ++T) {
|
||||
FusedGrArgs a[NV][TM];
|
||||
for (int v = 0; v < NV; ++v) {
|
||||
const size_t span = (size_t) ((uint8_t*) (set[v].mixed + (size_t) TM * N) - (uint8_t*) set[v].R_out);
|
||||
ok = ok && cudaMemsetAsync(set[v].R_out, 0xFF, span, st) == cudaSuccess; // the whole set
|
||||
for (int k = 0; k < T; ++k) {
|
||||
FusedGrArgs& x = a[v][k];
|
||||
x.R = d_R + (size_t) k * D; x.R_out = set[v].R_out + (size_t) k * D; x.apply = apply != 0;
|
||||
x.bo_prev = d_bo + (size_t) k * N; x.inj_prev = d_ip + (size_t) k * HC;
|
||||
x.w_norm = d_norm; x.w_down = d_down; x.w_up = d_up; x.w_inject = d_inj; x.eps = 1e-6f;
|
||||
x.lo = set[v].lo + (size_t) k * LR; x.rs = set[v].rs + (size_t) k * HC;
|
||||
x.inject_out = set[v].inj + (size_t) k * HC; x.mixed = set[v].mixed + (size_t) k * N;
|
||||
}
|
||||
}
|
||||
for (int v = 0; v < 3; ++v) {
|
||||
GrMulti m;
|
||||
for (int k = 0; k < T; ++k) m.a[k] = a[v][k];
|
||||
m.xn = set[v].xn;
|
||||
m.T = T;
|
||||
launch_multi(m, v + 1, st, nullptr, 0);
|
||||
}
|
||||
if (T == 1) fused_gr_read(a[3][0], st);
|
||||
if (cudaGetLastError() != cudaSuccess || cudaStreamSynchronize(st) != cudaSuccess) {
|
||||
why[0] = "a kernel of the check failed";
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (ok_variant[2] && !all_same(set[0], set[1], T, apply, why[2])) ok_variant[2] = false;
|
||||
if (ok && ok_variant[3] && !all_same(set[0], set[2], T, apply, why[3])) ok_variant[3] = false;
|
||||
if (ok && T == 1 && single_ok && !all_same(set[3], set[0], T, apply, why[1])) single_ok = false;
|
||||
}
|
||||
}
|
||||
if (!single_ok && ok) { // the plain read itself disagrees with the single-token read
|
||||
why[2] = why[3] = "the plain read differs from the single-token read: " + why[1];
|
||||
ok_variant[2] = ok_variant[3] = false;
|
||||
}
|
||||
if (st != nullptr) {
|
||||
cudaStreamSynchronize(st);
|
||||
cudaStreamDestroy(st);
|
||||
}
|
||||
cudaFree(base);
|
||||
cudaGetLastError();
|
||||
if (!ok) ok_variant[2] = ok_variant[3] = false;
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int fused_gr_variant() {
|
||||
int dev = 0;
|
||||
cudaGetDevice(&dev);
|
||||
const int v = dev >= 0 && dev < 64 ? g_variant[dev].load() : 0;
|
||||
if (v > 0) return v;
|
||||
// not checked on this card: the plain read, unless STRATA_HC_SPLIT names a variant (a test such as gr_parity)
|
||||
const char* e = std::getenv("STRATA_HC_SPLIT");
|
||||
return e != nullptr && (e[0] == '1' || e[0] == '2') ? env_variant() : kHcPlain;
|
||||
}
|
||||
|
||||
void fused_gr_check() {
|
||||
int dev = 0;
|
||||
cudaGetDevice(&dev);
|
||||
if (dev < 0 || dev >= 64 || g_variant[dev].load() > 0) return;
|
||||
const int want = env_variant();
|
||||
if (want == kHcPlain) {
|
||||
g_variant[dev].store(kHcPlain);
|
||||
std::fprintf(stderr, "strata hc: CUDA%d: the hyper-connection read runs as the plain one (STRATA_HC_SPLIT=0)\n",
|
||||
dev);
|
||||
return;
|
||||
}
|
||||
bool okv[4];
|
||||
std::string why[4];
|
||||
const bool ran = fused_gr_selftest(okv, why);
|
||||
int use = kHcPlain;
|
||||
if (want >= kHcStaged && okv[kHcStaged]) use = kHcStaged;
|
||||
else if (okv[kHcSplit]) use = kHcSplit;
|
||||
g_variant[dev].store(use);
|
||||
if (!ran)
|
||||
std::fprintf(stderr, "strata hc: CUDA%d: the check of split/staged could not run (%s)\n", dev, why[0].c_str());
|
||||
static const char* const name[4] = {"", "plain", "split", "staged"};
|
||||
for (int v = kHcStaged; v >= kHcSplit; --v)
|
||||
if (v <= want && !okv[v] && ran)
|
||||
std::fprintf(stderr, "strata hc: CUDA%d: the %s read differs from the plain read on this card - not used: "
|
||||
"%s\n", dev, name[v], why[v].c_str());
|
||||
static const char* const what[4] = {
|
||||
"", "the plain read (the norm per token, the down projection on 41 blocks)",
|
||||
"split (the norm per token and stream, then the plain read's down projection)",
|
||||
"staged (the norm per token and stream, the down projection's activations staged ahead by cp.async)"};
|
||||
std::fprintf(stderr, "strata hc: CUDA%d: the hyper-connection read runs as %s%s\n", dev, what[use],
|
||||
use >= kHcSplit ? "; checked bit for bit against the plain read on this card (STRATA_HC_SPLIT=1 or 0 "
|
||||
"for the earlier ones)" : "");
|
||||
}
|
||||
|
||||
|
||||
} // namespace strata::kernels
|
||||
@@ -1218,7 +1218,15 @@ __device__ void dq_q8_0(const void* vx, int64_t ibs, dst_t* yy, int tid) {
|
||||
for (int j = 0; j < 8; ++j) y[j] = cvt<dst_t>((float) x[ib].qs[8 * il + j] * d);
|
||||
}
|
||||
|
||||
// Every type below must also be in is_iq(): the host entry points refuse the others, so the default is unreachable.
|
||||
// BF16 (the token embedding as the checkpoint ships it, tools/embd_bf16_pack.py): 8 values per thread.
|
||||
template<typename dst_t>
|
||||
__device__ void dq_bf16(const void* vx, int64_t ibs, dst_t* yy, int tid) {
|
||||
const uint16_t* x = (const uint16_t*) vx + ibs * 256 + tid * 8;
|
||||
for (int j = 0; j < 8; ++j) yy[tid * 8 + j] = cvt<dst_t>(__uint_as_float((uint32_t) x[j] << 16));
|
||||
}
|
||||
|
||||
// Every type below must also be in is_iq() (BF16: embed_type_supported): the host entry points refuse the others,
|
||||
// so the default is unreachable.
|
||||
template<typename dst_t>
|
||||
__device__ __forceinline__ void dq_dispatch(int ty, const void* vx, int64_t ibs, dst_t* y, int tid) {
|
||||
switch (ty) {
|
||||
@@ -1236,6 +1244,7 @@ __device__ __forceinline__ void dq_dispatch(int ty, const void* vx, int64_t ibs,
|
||||
case 13: dq_q5_k(vx, ibs, y, tid); break;
|
||||
case 7: dq_q5_1(vx, ibs, y, tid); break;
|
||||
case 8: dq_q8_0(vx, ibs, y, tid); break;
|
||||
case 30: dq_bf16(vx, ibs, y, tid); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -1321,6 +1330,7 @@ void iq_set_old_kernels(bool old) { g_old_kernels = old; }
|
||||
bool iq_old_kernels() { return g_old_kernels; }
|
||||
|
||||
bool iq_supported(int t) noexcept { return is_iq(t); }
|
||||
bool embed_type_supported(int t) noexcept { return is_iq(t) || t == 30; }
|
||||
|
||||
size_t iq_row_bytes(int t, int64_t n) noexcept {
|
||||
switch (t) {
|
||||
@@ -1338,6 +1348,7 @@ size_t iq_row_bytes(int t, int64_t n) noexcept {
|
||||
case 13: return (size_t) (n / 256) * sizeof(block_q5_K);
|
||||
case 7: return (size_t) (n / 32) * sizeof(block_q5_1);
|
||||
case 8: return (size_t) (n / 32) * sizeof(block_q8_0);
|
||||
case 30: return (size_t) n * 2; // BF16: the token embedding only (iq_embed_rows, iq_dequant_f32)
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
@@ -1382,14 +1393,14 @@ __global__ void embed_rows_kernel(int ty, const uint8_t* __restrict__ table, siz
|
||||
void iq_embed_rows(int t, const void* table, size_t row_bytes, const int32_t* tokens, int64_t n_tok, int64_t n_embd,
|
||||
float* out, void* stream) {
|
||||
if (n_tok <= 0) return;
|
||||
if (n_embd % 256 != 0 || !is_iq(t)) { std::fprintf(stderr, "iq_embed_rows: bad arguments\n"); std::exit(1); }
|
||||
if (n_embd % 256 != 0 || !embed_type_supported(t)) { std::fprintf(stderr, "iq_embed_rows: bad arguments\n"); std::exit(1); }
|
||||
embed_rows_kernel<<<dim3((unsigned) (n_embd / 256), (unsigned) n_tok), 32, 0, (cudaStream_t) stream>>>(
|
||||
t, (const uint8_t*) table, row_bytes, tokens, n_embd, out);
|
||||
check("iq_embed_rows");
|
||||
}
|
||||
|
||||
void iq_dequant_f32(int t, const void* src, int64_t n, float* dst, void* stream) {
|
||||
if (n % 256 != 0 || !is_iq(t)) { std::fprintf(stderr, "iq_dequant_f32: bad arguments\n"); std::exit(1); }
|
||||
if (n % 256 != 0 || !embed_type_supported(t)) { std::fprintf(stderr, "iq_dequant_f32: bad arguments\n"); std::exit(1); }
|
||||
dequant_flat_kernel<float><<<(unsigned) (n / 256), 32, 0, (cudaStream_t) stream>>>(t, src, dst);
|
||||
check("iq_dequant_f32");
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ __global__ void gate_softplus(const float* __restrict__ alpha, const float* __re
|
||||
const int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= count) return;
|
||||
const float value = __fadd_rn(alpha[i], dt[i]);
|
||||
const float softplus = value > 20.0f ? value : logf(1.0f + expf(value));
|
||||
const float softplus = value > 20.0f ? value : log1pf(expf(value)); // 1 + e^v loses e^v below ~1e-7
|
||||
gate[i] = softplus * ssm_a[i];
|
||||
}
|
||||
|
||||
|
||||
@@ -1439,7 +1439,7 @@ void native_iq4_nl_f32(const void* weights, const float* x, void* scratch_q8_1,
|
||||
}
|
||||
|
||||
bool native_mmvq_supported(int ggml_type) noexcept {
|
||||
return ggml_type == 2 || ggml_type == 6 || ggml_type == 8 || ggml_type == 11 ||
|
||||
return ggml_type == 2 || ggml_type == 6 || ggml_type == 7 || ggml_type == 8 || ggml_type == 11 ||
|
||||
ggml_type == 12 || ggml_type == 13 || ggml_type == 14 || ggml_type == 20 ||
|
||||
ggml_type == 23 || ggml_type == 42 || ggml_type == 16 || ggml_type == 17 || ggml_type == 18 ||
|
||||
ggml_type == 21 || ggml_type == 22 || ggml_type == 29;
|
||||
@@ -1450,6 +1450,7 @@ std::size_t native_mmvq_weight_bytes(int ggml_type, int n_in, int n_out) {
|
||||
switch (ggml_type) {
|
||||
case 2: block_elems = 32; block_bytes = 18; break;
|
||||
case 6: block_elems = 32; block_bytes = 22; break;
|
||||
case 7: block_elems = 32; block_bytes = 24; break;
|
||||
case 8: block_elems = 32; block_bytes = 34; break;
|
||||
case 20: block_elems = 32; block_bytes = 18; break;
|
||||
case 11: block_elems = 256; block_bytes = 110; break;
|
||||
@@ -1476,6 +1477,7 @@ void native_mmvq(int ggml_type, const void* weights, const void* x_q8_1, float*
|
||||
switch (ggml_type) {
|
||||
case 2: native_q4_0_mmvq(weights, x_q8_1, y, n_in, n_out, ncols, stream); break;
|
||||
case 6: native_q5_0_mmvq(weights, x_q8_1, y, n_in, n_out, ncols, stream); break;
|
||||
case 7: iq_mmvq(ggml_type, weights, x_q8_1, y, n_in, n_out, ncols, stream); break;
|
||||
case 8: native_q8_0_mmvq(weights, x_q8_1, y, n_in, n_out, ncols, stream); break;
|
||||
case 20: native_iq4_nl_mmvq(weights, x_q8_1, y, n_in, n_out, ncols, stream); break;
|
||||
case 11: native_q3_k_mmvq(weights, x_q8_1, y, n_in, n_out, ncols, stream); break;
|
||||
|
||||
@@ -39,13 +39,17 @@ __device__ float warp_sum(float x) {
|
||||
x += __shfl_xor_sync(0xffffffffu, x, offset);
|
||||
return x;
|
||||
}
|
||||
// TAB (#280, STRATA_ROPE_TABLE=1): the angles from the session's float64 table. The host launches <false> whenever
|
||||
// no table applies - the default - so the default kernel is 0.1.31's code exactly (the table read is not in it;
|
||||
// with it merely skipped at run time, the compiled default path changed its results).
|
||||
template <bool TAB>
|
||||
__global__ void append(const float* __restrict__ raw, const int32_t* __restrict__ pos_dev,
|
||||
int pos_base, const float* __restrict__ gamma, float epsilon,
|
||||
float* __restrict__ tail, float* __restrict__ dead,
|
||||
float* __restrict__ pooled, int32_t* __restrict__ block_pos,
|
||||
int max_cells, float theta_scale, float freq_scale, float corr_low,
|
||||
float corr_high, float ext_factor, float mscale,
|
||||
const int32_t* __restrict__ mtab) {
|
||||
const int32_t* __restrict__ mtab, RopeTab rt) {
|
||||
const int pos = *pos_dev, d = threadIdx.x;
|
||||
if (pos < 0 || pos >= max_cells) return;
|
||||
const int slot = pos % R;
|
||||
@@ -87,9 +91,11 @@ __global__ void append(const float* __restrict__ raw, const int32_t* __restrict_
|
||||
const int pair = d % (ROT / 2);
|
||||
// The spare (pos == 0) keeps its zero angle; under YaRN the mscale magnitude still rides in
|
||||
// through cos(0) - which is exactly what the queries are scaled by, so the top-k is unmoved.
|
||||
const float theta_extrap = (pos == 0 ? 0 : mrope_pos(mtab, rope_pos, pair)) * powf(theta_scale, float(pair));
|
||||
float c, s;
|
||||
rope_scaled_angle(theta_extrap, freq_scale, corr_low, corr_high, ext_factor, mscale, pair, c, s);
|
||||
if (!(TAB && rope_tab_cs(rt, pos == 0 ? 0 : mrope_pos(mtab, rope_pos, pair), pair, c, s))) {
|
||||
const float theta_extrap = (pos == 0 ? 0 : mrope_pos(mtab, rope_pos, pair)) * powf(theta_scale, float(pair));
|
||||
rope_scaled_angle(theta_extrap, freq_scale, corr_low, corr_high, ext_factor, mscale, pair, c, s);
|
||||
}
|
||||
const float a = values[pair], z = values[pair + ROT / 2];
|
||||
y = d < ROT / 2 ? a * c - z * s : a * s + z * c;
|
||||
}
|
||||
@@ -103,18 +109,22 @@ __global__ void append(const float* __restrict__ raw, const int32_t* __restrict_
|
||||
// the RMS norm, gamma, the rotation at pos_base + 4b. `k(j)` is the key of the block's cell j: a row of this batch
|
||||
// (cell >= p0) or the tail the previous batch left (cell < p0). Only the batch's last completed block writes the
|
||||
// spare after it (every earlier one's is overwritten by the next block in order).
|
||||
template <bool TAB>
|
||||
__device__ __forceinline__ float pooled_value(const float* values, int d, int rope_pos, float theta_scale,
|
||||
float freq_scale, float corr_low, float corr_high,
|
||||
float ext_factor, float mscale, const int32_t* mtab, bool zero_pos) {
|
||||
float ext_factor, float mscale, const int32_t* mtab, bool zero_pos,
|
||||
const RopeTab& rt) {
|
||||
float y = values[d];
|
||||
if (d < ROT) {
|
||||
const int pair = d % (ROT / 2);
|
||||
// The same rotation the single append applies, rope_scaling.hpp's helper shared: under none this is
|
||||
// today's cosf/sinf; under linear/YaRN the pooled keys move with the cache's K, and the spare's zero
|
||||
// angle carries the mscale magnitude through cos(0) exactly as the single append's does.
|
||||
const float theta_extrap = (zero_pos ? 0 : mrope_pos(mtab, rope_pos, pair)) * powf(theta_scale, float(pair));
|
||||
float c, s;
|
||||
rope_scaled_angle(theta_extrap, freq_scale, corr_low, corr_high, ext_factor, mscale, pair, c, s);
|
||||
if (!(TAB && rope_tab_cs(rt, zero_pos ? 0 : mrope_pos(mtab, rope_pos, pair), pair, c, s))) {
|
||||
const float theta_extrap = (zero_pos ? 0 : mrope_pos(mtab, rope_pos, pair)) * powf(theta_scale, float(pair));
|
||||
rope_scaled_angle(theta_extrap, freq_scale, corr_low, corr_high, ext_factor, mscale, pair, c, s);
|
||||
}
|
||||
const float a = values[pair], z = values[pair + ROT / 2];
|
||||
y = d < ROT / 2 ? a * c - z * s : a * s + z * c;
|
||||
}
|
||||
@@ -131,10 +141,11 @@ __device__ __forceinline__ float norm_scale(float mean, float* partials, int d)
|
||||
return square_sum;
|
||||
}
|
||||
// cell 0 of a sequence: the spare (every gather index names cell 0), written to pooled[0] and dead
|
||||
template <bool TAB>
|
||||
__global__ void append_first(const float* __restrict__ raw, const float* __restrict__ gamma, float epsilon,
|
||||
float* __restrict__ dead, float* __restrict__ pooled, float theta_scale,
|
||||
float freq_scale, float corr_low, float corr_high, float ext_factor, float mscale,
|
||||
const int32_t* __restrict__ mtab) {
|
||||
const int32_t* __restrict__ mtab, RopeTab rt) {
|
||||
const int d = threadIdx.x;
|
||||
__shared__ float values[D];
|
||||
__shared__ float partials[32];
|
||||
@@ -151,17 +162,18 @@ __global__ void append_first(const float* __restrict__ raw, const float* __restr
|
||||
if (d < D) values[d] = scale * mean * gamma[d];
|
||||
__syncthreads();
|
||||
if (d >= D) return;
|
||||
const float y = pooled_value(values, d, 0, theta_scale, freq_scale, corr_low, corr_high, ext_factor, mscale,
|
||||
mtab, true);
|
||||
const float y = pooled_value<TAB>(values, d, 0, theta_scale, freq_scale, corr_low, corr_high, ext_factor, mscale,
|
||||
mtab, true, rt);
|
||||
pooled[d] = y;
|
||||
dead[d] = y;
|
||||
}
|
||||
template <bool TAB>
|
||||
__global__ void append_blocks(const float* __restrict__ raw, int64_t n, int64_t p0, int pos_base,
|
||||
const float* __restrict__ gamma, float epsilon, const float* __restrict__ tail,
|
||||
const float* __restrict__ dead, float* __restrict__ pooled, int32_t* __restrict__ block_pos,
|
||||
int64_t first_block, int64_t last_block, float theta_scale, float freq_scale,
|
||||
float corr_low, float corr_high, float ext_factor, float mscale,
|
||||
const int32_t* __restrict__ mtab) {
|
||||
const int32_t* __restrict__ mtab, RopeTab rt) {
|
||||
const int64_t b = first_block + blockIdx.x;
|
||||
const int d = threadIdx.x;
|
||||
__shared__ float values[D];
|
||||
@@ -183,8 +195,8 @@ __global__ void append_blocks(const float* __restrict__ raw, int64_t n, int64_t
|
||||
__syncthreads();
|
||||
if (d >= D) return;
|
||||
const int rope_pos = pos_base + R * (int) b;
|
||||
pooled[std::size_t(b) * D + d] = pooled_value(values, d, rope_pos, theta_scale, freq_scale, corr_low, corr_high,
|
||||
ext_factor, mscale, mtab, false);
|
||||
pooled[std::size_t(b) * D + d] = pooled_value<TAB>(values, d, rope_pos, theta_scale, freq_scale, corr_low, corr_high,
|
||||
ext_factor, mscale, mtab, false, rt);
|
||||
if (b == last_block) {
|
||||
pooled[std::size_t(b + 1) * D + d] = dead[d];
|
||||
if (d == 0) *block_pos = rope_pos;
|
||||
@@ -228,9 +240,15 @@ void native_qsa_indexer_append(const float* raw, const int32_t* relative_pos_dev
|
||||
if (overlaps(spans[i], spans[j])) throw std::invalid_argument("native QSA indexer buffers overlap");
|
||||
const float theta_scale = powf((float) scaling.freq_base, -2.0f / ROT);
|
||||
const RopeKernelArgs k = scaling.kernel_args(ROT); // none: the identity constants
|
||||
append<<<1,THREADS,0,static_cast<cudaStream_t>(stream)>>>(raw,relative_pos_device,pos_base,gamma,epsilon,
|
||||
b.tail,b.dead,b.pooled,b.block_pos,int(max_cells),theta_scale,k.freq_scale,
|
||||
k.corr_low,k.corr_high,k.ext_factor,k.attn_factor,mrope_table());
|
||||
const RopeTab rt = rope_table_for(scaling);
|
||||
if (rt.cos != nullptr)
|
||||
append<true><<<1,THREADS,0,static_cast<cudaStream_t>(stream)>>>(raw,relative_pos_device,pos_base,gamma,epsilon,
|
||||
b.tail,b.dead,b.pooled,b.block_pos,int(max_cells),theta_scale,k.freq_scale,
|
||||
k.corr_low,k.corr_high,k.ext_factor,k.attn_factor,mrope_table(),rt);
|
||||
else
|
||||
append<false><<<1,THREADS,0,static_cast<cudaStream_t>(stream)>>>(raw,relative_pos_device,pos_base,gamma,epsilon,
|
||||
b.tail,b.dead,b.pooled,b.block_pos,int(max_cells),theta_scale,k.freq_scale,
|
||||
k.corr_low,k.corr_high,k.ext_factor,k.attn_factor,mrope_table(),rt);
|
||||
const auto error = cudaGetLastError();
|
||||
if (error != cudaSuccess) throw std::runtime_error(cudaGetErrorString(error));
|
||||
}
|
||||
@@ -247,15 +265,24 @@ void native_qsa_indexer_append_batch(const float* raw, int64_t n, int64_t p0, in
|
||||
const float fsf = k.freq_scale, cl = k.corr_low, ch = k.corr_high, ef = k.ext_factor, ms = k.attn_factor;
|
||||
const cudaStream_t st = static_cast<cudaStream_t>(stream);
|
||||
const int32_t* mtab = mrope_table();
|
||||
if (p0 == 0) append_first<<<1, THREADS, 0, st>>>(raw, gamma, epsilon, b.dead, b.pooled, theta_scale,
|
||||
fsf, cl, ch, ef, ms, mtab);
|
||||
const RopeTab rt = rope_table_for(scaling);
|
||||
if (p0 == 0) {
|
||||
if (rt.cos != nullptr)
|
||||
append_first<true><<<1, THREADS, 0, st>>>(raw, gamma, epsilon, b.dead, b.pooled, theta_scale,
|
||||
fsf, cl, ch, ef, ms, mtab, rt);
|
||||
else
|
||||
append_first<false><<<1, THREADS, 0, st>>>(raw, gamma, epsilon, b.dead, b.pooled, theta_scale,
|
||||
fsf, cl, ch, ef, ms, mtab, rt);
|
||||
}
|
||||
// completed blocks: those whose last cell (4b+3) lies in [p0, p0 + n)
|
||||
const int64_t first = p0 <= R - 1 ? 0 : (p0 - (R - 1) + R - 1) / R; // the smallest b with 4b+3 >= p0
|
||||
const int64_t hi = p0 + n - 1 >= R - 1 ? (p0 + n - 1 - (R - 1)) / R : -1; // the largest b with 4b+3 <= p0+n-1
|
||||
if (hi >= first)
|
||||
append_blocks<<<(unsigned) (hi - first + 1), THREADS, 0, st>>>(raw, n, p0, pos_base, gamma, epsilon, b.tail,
|
||||
b.dead, b.pooled, b.block_pos, first, hi,
|
||||
theta_scale, fsf, cl, ch, ef, ms, mtab);
|
||||
if (hi >= first && rt.cos != nullptr)
|
||||
append_blocks<true><<<(unsigned) (hi - first + 1), THREADS, 0, st>>>(raw, n, p0, pos_base, gamma, epsilon,
|
||||
b.tail, b.dead, b.pooled, b.block_pos, first, hi, theta_scale, fsf, cl, ch, ef, ms, mtab, rt);
|
||||
else if (hi >= first)
|
||||
append_blocks<false><<<(unsigned) (hi - first + 1), THREADS, 0, st>>>(raw, n, p0, pos_base, gamma, epsilon,
|
||||
b.tail, b.dead, b.pooled, b.block_pos, first, hi, theta_scale, fsf, cl, ch, ef, ms, mtab, rt);
|
||||
append_tail<<<R - 1, D, 0, st>>>(raw, n, p0, b.tail);
|
||||
const auto error = cudaGetLastError();
|
||||
if (error != cudaSuccess) throw std::runtime_error(cudaGetErrorString(error));
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
|
||||
@@ -38,9 +39,13 @@ bool overlaps(const void* a, size_t an, const void* b, size_t bn) {
|
||||
auto x = reinterpret_cast<uintptr_t>(a), y = reinterpret_cast<uintptr_t>(b);
|
||||
return x <= y ? y - x < an : x - y < bn;
|
||||
}
|
||||
// TAB (#280, STRATA_ROPE_TABLE=1): the angles from the session's float64 table. The host launches <false> whenever
|
||||
// no table applies - the default - so the default kernel is 0.1.31's code exactly (the table read is not in it;
|
||||
// with it merely skipped at run time, the compiled default path changed its results).
|
||||
template <bool TAB>
|
||||
__global__ void apply(const float* x, float* out, int rows, int width,
|
||||
int n_rot, float theta_scale, float freq_scale, float corr_low, float corr_high,
|
||||
float ext_factor, float mscale, const int* positions, const int32_t* mtab) {
|
||||
float ext_factor, float mscale, const int* positions, const int32_t* mtab, RopeTab rt) {
|
||||
const int row = blockIdx.y;
|
||||
const int pair = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= rows || pair >= width / 2) return;
|
||||
@@ -52,9 +57,11 @@ __global__ void apply(const float* x, float* out, int rows, int width,
|
||||
}
|
||||
return;
|
||||
}
|
||||
const float theta_extrap = mrope_pos(mtab, positions[row], pair) * powf(theta_scale, float(pair));
|
||||
float c, s;
|
||||
rope_scaled_angle(theta_extrap, freq_scale, corr_low, corr_high, ext_factor, mscale, pair, c, s);
|
||||
if (!(TAB && rope_tab_cs(rt, mrope_pos(mtab, positions[row], pair), pair, c, s))) {
|
||||
const float theta_extrap = mrope_pos(mtab, positions[row], pair) * powf(theta_scale, float(pair));
|
||||
rope_scaled_angle(theta_extrap, freq_scale, corr_low, corr_high, ext_factor, mscale, pair, c, s);
|
||||
}
|
||||
const float a = x[start + pair], b = x[start + pair + n_rot / 2];
|
||||
out[start + pair] = a * c - b * s;
|
||||
out[start + pair + n_rot / 2] = a * s + b * c;
|
||||
@@ -72,6 +79,40 @@ int mrope_dev() {
|
||||
} // namespace
|
||||
void mrope_table_set(const int32_t* device_table) { mrope_tab[mrope_dev()].store(device_table, std::memory_order_relaxed); }
|
||||
const int32_t* mrope_table() { return mrope_tab[mrope_dev()].load(std::memory_order_relaxed); }
|
||||
namespace {
|
||||
// #280: the float64 angle table of each device, set at session init (before any graph is captured)
|
||||
struct RopeReg {
|
||||
RopeTab tab;
|
||||
RopeScaling scaling;
|
||||
};
|
||||
RopeReg rope_tab[kMropeDevices] = {};
|
||||
bool same_scaling(const RopeScaling& a, const RopeScaling& b) {
|
||||
return a.type == b.type && a.freq_base == b.freq_base && a.factor == b.factor && a.freq_scale_in == b.freq_scale_in &&
|
||||
a.orig_ctx == b.orig_ctx && a.ext_factor == b.ext_factor && a.attn_factor == b.attn_factor &&
|
||||
a.beta_fast == b.beta_fast && a.beta_slow == b.beta_slow;
|
||||
}
|
||||
// opt-in: STRATA_ROPE_TABLE=1 (the table's angles differ from the fast-math ones in the last bits, so outputs move)
|
||||
bool rope_table_enabled() {
|
||||
static const bool on = [] {
|
||||
const char* e = std::getenv("STRATA_ROPE_TABLE");
|
||||
return e != nullptr && e[0] == '1';
|
||||
}();
|
||||
return on;
|
||||
}
|
||||
} // namespace
|
||||
void rope_table_set(const float* cos_tab, const float* sin_tab, int max_pos, const RopeScaling& scaling) {
|
||||
rope_tab[mrope_dev()] = RopeReg{RopeTab{cos_tab, sin_tab, max_pos}, scaling};
|
||||
}
|
||||
void rope_table_release(const float* cos_tab) {
|
||||
// every device's entry that points at it (the caller may free it from another current device)
|
||||
for (RopeReg& r : rope_tab)
|
||||
if (cos_tab != nullptr && r.tab.cos == cos_tab) r = RopeReg{};
|
||||
}
|
||||
RopeTab rope_table_for(const RopeScaling& scaling) {
|
||||
if (!rope_table_enabled()) return {};
|
||||
const RopeReg& r = rope_tab[mrope_dev()];
|
||||
return r.tab.cos != nullptr && same_scaling(r.scaling, scaling) ? r.tab : RopeTab{};
|
||||
}
|
||||
void native_rope_set_enabled(bool value) { enabled.store(value, std::memory_order_relaxed); }
|
||||
bool native_rope_enabled() { return enabled.load(std::memory_order_relaxed); }
|
||||
void native_rope_apply(const float* x, float* out, int rows, int head_dim,
|
||||
@@ -92,10 +133,14 @@ void native_rope_apply(const float* x, float* out, int rows, int head_dim,
|
||||
// Match pinned host-side float powf before device fast powf/trigonometry.
|
||||
const float theta_scale = powf((float) scaling.freq_base, -2.0f / n_rot);
|
||||
const RopeKernelArgs k = scaling.kernel_args(n_rot); // none: the identity constants
|
||||
apply<<<dim3((head_dim / 2 + 127) / 128, rows), 128, 0,
|
||||
static_cast<cudaStream_t>(stream)>>>(x, out, rows, head_dim, n_rot, theta_scale,
|
||||
k.freq_scale, k.corr_low, k.corr_high, k.ext_factor,
|
||||
k.attn_factor, positions, mrope_table());
|
||||
const RopeTab rt = rope_table_for(scaling);
|
||||
const dim3 grid((head_dim / 2 + 127) / 128, rows);
|
||||
if (rt.cos != nullptr)
|
||||
apply<true><<<grid, 128, 0, static_cast<cudaStream_t>(stream)>>>(x, out, rows, head_dim, n_rot, theta_scale,
|
||||
k.freq_scale, k.corr_low, k.corr_high, k.ext_factor, k.attn_factor, positions, mrope_table(), rt);
|
||||
else
|
||||
apply<false><<<grid, 128, 0, static_cast<cudaStream_t>(stream)>>>(x, out, rows, head_dim, n_rot, theta_scale,
|
||||
k.freq_scale, k.corr_low, k.corr_high, k.ext_factor, k.attn_factor, positions, mrope_table(), rt);
|
||||
const auto error = cudaGetLastError();
|
||||
if (error != cudaSuccess) throw std::runtime_error(cudaGetErrorString(error));
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
// Every comparison still passed at any sane tolerance, the spare key landing 0.4 f32 ulp away; only a
|
||||
// BIT-EXACT assertion plus a full-precision probe of the intermediate found it. That is the strongest
|
||||
// argument in this file for keeping both.
|
||||
#include "strata/core/emulate.hpp"
|
||||
#include "strata/kernels/qsa.hpp"
|
||||
|
||||
#include "strata/kernels/f16_bits.hpp"
|
||||
@@ -673,6 +674,7 @@ void qsa_attend_step(const float* q, const uint16_t* k_scratch, const uint16_t*
|
||||
int dev = 0, max_shared = 0;
|
||||
cudaGetDevice(&dev);
|
||||
cudaDeviceGetAttribute(&max_shared, cudaDevAttrMaxSharedMemoryPerBlockOptin, dev);
|
||||
max_shared = strata::smem_optin_of(max_shared);
|
||||
if ((int) smem > max_shared) {
|
||||
std::fprintf(stderr, "qsa: qsa_attend: max_ids %lld needs %zu B of shared, over the %d B limit\n",
|
||||
(long long) max_ids, smem, max_shared);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// src/kernels/cuda/qsa_prompt_attn.cu - see include/strata/kernels/qsa_prompt_attn.hpp.
|
||||
#include "strata/core/emulate.hpp"
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include "strata/kernels/qsa_prompt_attn.hpp"
|
||||
#include "strata/kernels/kv_q8.hpp"
|
||||
#include "strata/kernels/kv_q4.hpp"
|
||||
@@ -9,6 +12,7 @@
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <type_traits>
|
||||
|
||||
namespace strata::kernels {
|
||||
@@ -685,6 +689,287 @@ bool launch(const float* q, const QsaAttnPools& pools, const int32_t* ids, const
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(__HIPCC__)
|
||||
// ---- S6: the int8-KV prompt attention on RDNA4 matrix cores (opt-in: STRATA_HIP_WMMA=1, gfx12 only). The design of
|
||||
// the v2 kernel above with gfx12's v_wmma_f32_16x16x16_f16 (wave32) in place of m16n8k16: wave w owns dims
|
||||
// [64w, 64w+64) (int8 scale group w) for q.k and p.v; q and p are split into FP16 hi + lo parts, the int8 codes enter
|
||||
// exactly as FP16, the scales are applied in FP32 and the four groups' q.k partials are added in a fixed order.
|
||||
// FP32-level accuracy, deterministic, but not bitwise equal to qsa_decode_attn_batch (another summation order).
|
||||
// Fragment layout (16x16x16, wave32, checked on gfx1201): A lane l holds A[l % 16][(l / 16) * 8 + i], B lane l holds
|
||||
// B[(l / 16) * 8 + i][l % 16], C/D lane l holds D[(l / 16) * 8 + i][l % 16], i = 0..7.
|
||||
#if defined(__gfx1200__) || defined(__gfx1201__)
|
||||
#define STRATA_PA_WMMA 1
|
||||
#else
|
||||
#define STRATA_PA_WMMA 0
|
||||
#endif
|
||||
typedef _Float16 wh8 __attribute__((ext_vector_type(8)));
|
||||
typedef float wf8 __attribute__((ext_vector_type(8)));
|
||||
constexpr int WCH = 32; // cells per chunk (two 16-cell tiles)
|
||||
constexpr int WVS = 80; // staged V row stride in bytes (64 codes, 16-byte aligned, banks spread)
|
||||
struct alignas(16) SmemW {
|
||||
float part[4][16][WCH + 1]; // q.k per dim group
|
||||
float p[16][WCH + 1];
|
||||
float vs[4][WCH]; // V scale per (group, cell)
|
||||
int valid[WCH]; // the cell has a resident pool row
|
||||
uint8_t v[4][WCH][WVS]; // per wave: the chunk's V codes of its group, one row per cell
|
||||
float qmax[4];
|
||||
float alpha[16];
|
||||
float lsum[16];
|
||||
float mrow[16];
|
||||
};
|
||||
|
||||
__device__ __forceinline__ wf8 wmma_f16(wh8 a, wh8 b, wf8 c) {
|
||||
#if STRATA_PA_WMMA
|
||||
return __builtin_amdgcn_wmma_f32_16x16x16_f16_w32_gfx12(a, b, c);
|
||||
#else
|
||||
__builtin_trap();
|
||||
return c;
|
||||
#endif
|
||||
}
|
||||
|
||||
__device__ __forceinline__ wh8 i8x8_to_h8(uint2 x) { // exact: |code| <= 128
|
||||
wh8 h;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
h[i] = (_Float16) (int) (int8_t) (x.x >> (8 * i));
|
||||
h[4 + i] = (_Float16) (int) (int8_t) (x.y >> (8 * i));
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
__global__ void __launch_bounds__(THREADS) prompt_attn_wmma_kernel(const float* __restrict__ q, QsaAttnPools p,
|
||||
const int32_t* __restrict__ ids,
|
||||
const int32_t* __restrict__ steps, int n_kv_heads,
|
||||
int page_size, float scale_log2,
|
||||
float* __restrict__ attn, int cap) {
|
||||
#if STRATA_PA_WMMA
|
||||
__shared__ SmemW S;
|
||||
const int qi = blockIdx.x, kvh = blockIdx.y;
|
||||
const int n_head = n_kv_heads * G;
|
||||
q += (size_t) qi * n_head * HD + (size_t) kvh * G * HD;
|
||||
attn += (size_t) qi * n_head * HD + (size_t) kvh * G * HD;
|
||||
ids += (size_t) qi * cap;
|
||||
const int n = __ldg(steps + (size_t) qi * kStepCount + kStepWidth);
|
||||
const int t = threadIdx.x, lane = t & 31, warp = t >> 5;
|
||||
const int col = lane & 15, half = lane >> 4; // fragment column / k half
|
||||
const int dim0 = warp * 64;
|
||||
|
||||
float qm = 0.0f;
|
||||
for (int i = t; i < G * HD; i += THREADS) qm = fmaxf(qm, fabsf(q[i]));
|
||||
#pragma unroll
|
||||
for (int o = 16; o > 0; o >>= 1) qm = fmaxf(qm, __shfl_xor_sync(0xffffffffu, qm, o));
|
||||
if (lane == 0) S.qmax[warp] = qm;
|
||||
if (t < 16) { S.mrow[t] = -INFINITY; S.lsum[t] = 0.0f; }
|
||||
__syncthreads();
|
||||
qm = fmaxf(fmaxf(S.qmax[0], S.qmax[1]), fmaxf(S.qmax[2], S.qmax[3]));
|
||||
int qe = 0;
|
||||
if (qm > 0.0f) frexpf(qm, &qe);
|
||||
const float qup = ldexpf(1.0f, 14 - qe), qdown = ldexpf(scale_log2, qe - 14);
|
||||
// q A fragments of this wave's 64 dims: row = col (heads 12..15 zero), k = dim0 + kk*16 + half*8 + i
|
||||
wh8 qh[4], ql[4];
|
||||
#pragma unroll
|
||||
for (int kk = 0; kk < 4; ++kk) {
|
||||
float x[8];
|
||||
if (col < G) {
|
||||
const float4* src = reinterpret_cast<const float4*>(q + (size_t) col * HD + dim0 + kk * 16 + half * 8);
|
||||
const float4 a = src[0], b = src[1];
|
||||
x[0] = a.x; x[1] = a.y; x[2] = a.z; x[3] = a.w; x[4] = b.x; x[5] = b.y; x[6] = b.z; x[7] = b.w;
|
||||
} else {
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 8; ++i) x[i] = 0.0f;
|
||||
}
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const float v = x[i] * qup;
|
||||
const _Float16 hi = (_Float16) v;
|
||||
qh[kk][i] = hi;
|
||||
ql[kk][i] = (_Float16) (v - (float) hi);
|
||||
}
|
||||
}
|
||||
|
||||
wf8 acc[4];
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 4; ++j) acc[j] = wf8{0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f};
|
||||
|
||||
const int n_chunks = (n + WCH - 1) / WCH;
|
||||
for (int ci = 0; ci < n_chunks; ++ci) {
|
||||
const int c0 = ci * WCH;
|
||||
// this lane's cell of the chunk (lane = cell index): its pool row, or -1 (past the selection, or a block the
|
||||
// KV streaming left non-resident: masked as in the decode kernel)
|
||||
long long myrow = -1;
|
||||
if (c0 + lane < n) {
|
||||
const int cell = __ldg(ids + c0 + lane);
|
||||
const long long page = (long long) __ldg(p.page_table + cell / page_size);
|
||||
if (page >= 0) myrow = (page * n_kv_heads + kvh) * page_size + (cell % page_size);
|
||||
}
|
||||
// stage this wave's V slice of the chunk (64 bytes of each row) and the V scale; the K fragments come straight
|
||||
// from global memory (8 consecutive codes per lane)
|
||||
{
|
||||
uint4 v0 = make_uint4(0, 0, 0, 0), v1 = v0, v2 = v0, v3 = v0;
|
||||
float vsc = 0.0f;
|
||||
if (myrow >= 0) {
|
||||
const uint4* src = reinterpret_cast<const uint4*>(p.v_q + myrow * HD + dim0);
|
||||
v0 = __ldg(src); v1 = __ldg(src + 1); v2 = __ldg(src + 2); v3 = __ldg(src + 3);
|
||||
vsc = __half2float(__ushort_as_half(__ldg(p.v_scale + myrow * (HD / KV_Q8_GROUP) + warp)));
|
||||
}
|
||||
uint4* dst = reinterpret_cast<uint4*>(&S.v[warp][lane][0]);
|
||||
dst[0] = v0; dst[1] = v1; dst[2] = v2; dst[3] = v3;
|
||||
S.vs[warp][lane] = vsc;
|
||||
if (warp == 0) S.valid[lane] = myrow >= 0;
|
||||
}
|
||||
// q.k over this wave's 64 dims for two 16-cell tiles, times the cell's K scale for this group
|
||||
#pragma unroll
|
||||
for (int nt = 0; nt < 2; ++nt) {
|
||||
const long long rr = __shfl(myrow, nt * 16 + col);
|
||||
wf8 s = wf8{0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f};
|
||||
float ksc = 0.0f;
|
||||
if (rr >= 0) ksc = __half2float(__ushort_as_half(__ldg(p.k_scale + rr * (HD / KV_Q8_GROUP) + warp)));
|
||||
#pragma unroll
|
||||
for (int kk = 0; kk < 4; ++kk) {
|
||||
uint2 kx = make_uint2(0, 0);
|
||||
if (rr >= 0) kx = __ldg(reinterpret_cast<const uint2*>(p.k_q + rr * HD + dim0 + kk * 16 + half * 8));
|
||||
const wh8 b = i8x8_to_h8(kx);
|
||||
s = wmma_f16(qh[kk], b, s);
|
||||
s = wmma_f16(ql[kk], b, s);
|
||||
}
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 8; ++i) S.part[warp][half * 8 + i][nt * 16 + col] = s[i] * ksc;
|
||||
}
|
||||
__syncthreads();
|
||||
// online softmax over the four groups' sum (fixed order): row t/8, 4 cells per thread
|
||||
{
|
||||
const int r = t >> 3, sub = t & 7;
|
||||
float x[4], mx = -INFINITY;
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
const int c = sub * 4 + j;
|
||||
// past the selection, or a masked cell (non-resident page, as in the decode kernel): no weight
|
||||
x[j] = S.valid[c] ? (((S.part[0][r][c] + S.part[1][r][c]) + S.part[2][r][c]) + S.part[3][r][c]) * qdown
|
||||
: -INFINITY;
|
||||
mx = fmaxf(mx, x[j]);
|
||||
}
|
||||
#pragma unroll
|
||||
for (int o = 1; o < 8; o <<= 1) mx = fmaxf(mx, __shfl_xor_sync(0xffffffffu, mx, o));
|
||||
const float m_old = S.mrow[r];
|
||||
const float m_new = fmaxf(m_old, mx);
|
||||
float sum = 0.0f;
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
const float e = x[j] == -INFINITY ? 0.0f : exp2f(x[j] - m_new);
|
||||
S.p[r][sub * 4 + j] = e;
|
||||
sum += e;
|
||||
}
|
||||
#pragma unroll
|
||||
for (int o = 1; o < 8; o <<= 1) sum += __shfl_xor_sync(0xffffffffu, sum, o);
|
||||
if (sub == 0) {
|
||||
const float a = m_old == -INFINITY ? 0.0f : exp2f(m_old - m_new);
|
||||
S.alpha[r] = a;
|
||||
S.lsum[r] = fmaf(S.lsum[r], a, sum);
|
||||
S.mrow[r] = m_new;
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
// p.v over this wave's 64 dims: 4 tiles of 16 dims, 2 k-steps of 16 cells
|
||||
{
|
||||
float vmax = S.vs[warp][lane];
|
||||
#pragma unroll
|
||||
for (int o = 16; o > 0; o >>= 1) vmax = fmaxf(vmax, __shfl_xor_sync(0xffffffffu, vmax, o));
|
||||
const float vup = vmax > 0.0f ? 16384.0f / vmax : 0.0f, vdown = vmax * (1.0f / 16384.0f);
|
||||
wf8 tmp[4];
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 4; ++j) tmp[j] = wf8{0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f};
|
||||
#pragma unroll
|
||||
for (int ks = 0; ks < 2; ++ks) {
|
||||
const int cb = ks * 16 + half * 8; // this lane's 8 cells
|
||||
wh8 ah, al;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const float pv = S.p[col][cb + i] * (S.vs[warp][cb + i] * vup);
|
||||
const _Float16 hi = (_Float16) pv;
|
||||
ah[i] = hi;
|
||||
al[i] = (_Float16) (pv - (float) hi);
|
||||
}
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
const int d = j * 16 + col;
|
||||
wh8 b;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 8; ++i) b[i] = (_Float16) (int) (int8_t) S.v[warp][cb + i][d];
|
||||
tmp[j] = wmma_f16(ah, b, tmp[j]);
|
||||
tmp[j] = wmma_f16(al, b, tmp[j]);
|
||||
}
|
||||
}
|
||||
float a[8];
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 8; ++i) a[i] = S.alpha[half * 8 + i];
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 4; ++j)
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 8; ++i) acc[j][i] = fmaf(acc[j][i], a[i], tmp[j][i] * vdown);
|
||||
}
|
||||
// no barrier here: `v`/`vs` are this wave's own, and `p`/`alpha`/`valid` are rewritten only after the next
|
||||
// chunk's first barrier (which every wave reaches after its p.v)
|
||||
}
|
||||
float inv[8];
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const float l = S.lsum[half * 8 + i];
|
||||
inv[i] = l > 0.0f ? 1.0f / l : 0.0f;
|
||||
}
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 4; ++j)
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const int row = half * 8 + i;
|
||||
if (row < G) attn[(size_t) row * HD + dim0 + j * 16 + col] = acc[j][i] * inv[i];
|
||||
}
|
||||
#else
|
||||
__builtin_trap();
|
||||
#endif
|
||||
}
|
||||
|
||||
// gfx12 (RDNA4) only, and only on request: the output differs from the default kernel's in its last bits
|
||||
bool hip_wmma_usable() {
|
||||
static const bool want = [] {
|
||||
const char* e = std::getenv("STRATA_HIP_WMMA");
|
||||
return e != nullptr && e[0] == '1';
|
||||
}();
|
||||
if (!want) return false;
|
||||
static int arch[64] = {}; // 0 unknown, 1 gfx12, 2 other
|
||||
int dev = 0;
|
||||
if (hipGetDevice(&dev) != hipSuccess || dev < 0 || dev >= 64) { (void) hipGetLastError(); return false; }
|
||||
if (arch[dev] == 0) {
|
||||
hipDeviceProp_t prop{};
|
||||
if (hipGetDeviceProperties(&prop, dev) != hipSuccess) { (void) hipGetLastError(); return false; }
|
||||
arch[dev] = std::strncmp(prop.gcnArchName, "gfx12", 5) == 0 ? 1 : 2;
|
||||
static bool told = false;
|
||||
if (!told) {
|
||||
told = true;
|
||||
std::fprintf(stderr, "strata: STRATA_HIP_WMMA: the prompt attention on matrix cores %s (%s)\n",
|
||||
arch[dev] == 1 ? "on" : "unavailable", prop.gcnArchName);
|
||||
}
|
||||
}
|
||||
return arch[dev] == 1;
|
||||
}
|
||||
|
||||
bool launch_wmma(const float* q, const QsaAttnPools& pools, const int32_t* ids, const int32_t* steps, int64_t cap,
|
||||
const QsaShapes& s, float* attn, int64_t n_q, cudaStream_t st) {
|
||||
const float scale_log2 = 1.4426950408889634f / sqrtf((float) HD);
|
||||
for (int64_t q0 = 0; q0 < n_q; q0 += 65535) {
|
||||
const int64_t nb = n_q - q0 < 65535 ? n_q - q0 : 65535;
|
||||
prompt_attn_wmma_kernel<<<dim3((unsigned) nb, (unsigned) s.n_head_kv), THREADS, 0, st>>>(
|
||||
q + q0 * s.n_head * HD, pools, ids + q0 * cap, steps + q0 * kStepCount, (int) s.n_head_kv,
|
||||
(int) s.page_size, scale_log2, attn + q0 * s.n_head * HD, (int) cap);
|
||||
}
|
||||
const cudaError_t e = cudaGetLastError();
|
||||
if (e != cudaSuccess) {
|
||||
std::fprintf(stderr, "qsa_prompt_attn_batch (wmma): %s\n", cudaGetErrorString(e));
|
||||
std::exit(1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
bool qsa_prompt_attn_batch(const float* q, const QsaAttnPools& pools, const int32_t* ids, const int32_t* steps,
|
||||
@@ -694,22 +979,34 @@ bool qsa_prompt_attn_batch(const float* q, const QsaAttnPools& pools, const int3
|
||||
{ // sm_75 or newer: the MMA above compiles for both. sm_80+ runs the cp.async kernel (launch_i8); Turing has
|
||||
// no cp.async, so it runs the v1 kernel (launch<1>, same accuracy, another summation order). An older card
|
||||
// keeps the old kernel.
|
||||
static int cc_major[64] = {};
|
||||
// #371: the compute capability with its minor - sm_70 (V100) has no m16n8k8 (the kernels trap below sm_75)
|
||||
static int cc[64] = {};
|
||||
int dev = 0;
|
||||
if (cudaGetDevice(&dev) != cudaSuccess || dev < 0 || dev >= 64) { cudaGetLastError(); return false; }
|
||||
if (cc_major[dev] == 0) {
|
||||
int major = 0;
|
||||
if (cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev) != cudaSuccess) {
|
||||
if (cc[dev] == 0) {
|
||||
int major = 0, minor = 0;
|
||||
if (cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev) != cudaSuccess ||
|
||||
cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, dev) != cudaSuccess) {
|
||||
cudaGetLastError();
|
||||
return false;
|
||||
}
|
||||
cc_major[dev] = major;
|
||||
// STRATA_QSA_WARP=1|attn (an A/B arm): the pre-sm_80 kernels on any card, as RTX 20 runs them
|
||||
const char* w = std::getenv("STRATA_QSA_WARP");
|
||||
cc[dev] = w && (!std::strcmp(w, "1") || !std::strcmp(w, "attn")) ? 75
|
||||
: 10 * strata::cc_major_of(major) + strata::cc_minor_of(minor);
|
||||
}
|
||||
if (cc_major[dev] < 7) return false;
|
||||
turing = cc_major[dev] < 8;
|
||||
if (cc[dev] < 75) return false;
|
||||
turing = cc[dev] < 80;
|
||||
}
|
||||
#if defined(__HIPCC__)
|
||||
return false; // the tensor-core kernel is compiled out on AMD (its major version is not a CUDA sm)
|
||||
// the tensor-core kernels are compiled out on AMD (its major version is not a CUDA sm); RDNA4 has its own int8-KV
|
||||
// matrix-core kernel, opt-in (STRATA_HIP_WMMA=1); everything else keeps the old kernel
|
||||
(void) turing;
|
||||
if (pools.k_q != nullptr && pools.v_q != nullptr && pools.k_scale != nullptr && pools.v_scale != nullptr &&
|
||||
pools.k_q4 == nullptr && pools.v_q4 == nullptr && s.head_dim == HD && s.n_head == (int64_t) G * s.n_head_kv &&
|
||||
cap > 0 && ids && steps && pools.page_table && hip_wmma_usable())
|
||||
return launch_wmma(q, pools, ids, steps, cap, s, attn, n_q, (cudaStream_t) stream);
|
||||
return false;
|
||||
#endif
|
||||
if (pools.k_q4 != nullptr || s.head_dim != HD || s.n_head != (int64_t) G * s.n_head_kv || cap <= 0 || !ids ||
|
||||
!steps || !pools.page_table)
|
||||
|
||||
+519
-13
@@ -1,4 +1,7 @@
|
||||
// src/kernels/cuda/qsa_select.cu - see include/strata/kernels/qsa_select.hpp.
|
||||
#include "strata/core/emulate.hpp"
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include "strata/kernels/qsa_select.hpp"
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
@@ -6,6 +9,7 @@
|
||||
#include <cfloat>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace strata::kernels {
|
||||
namespace {
|
||||
@@ -276,6 +280,160 @@ __global__ void __launch_bounds__(128) block_scores_tc_kernel(const float* __res
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__HIPCC__)
|
||||
// ---- gfx12 (RDNA4: gfx1200 / gfx1201) scorer on v_wmma_f32_16x16x16_bf16 (wave32). Same GEMM as above: for a tile of 16
|
||||
// queries, every block's score is sum over the 4 indexer heads of relu(q_h . k), K = 128. FP32-level accuracy from a
|
||||
// three-way bf16 split of both operands (x = hi + mid + lo EXACTLY - the top 8, next 8 and last 8 bits of the fp32
|
||||
// mantissa, so bf16's fp32-sized exponent range needs no scaling) and the six products of order <= 2, smallest first;
|
||||
// like the TF32 kernel it sums in another order than the warp kernel (not bitwise). The tail block n_bid is the warp
|
||||
// kernel's arithmetic (block_scores_tail_kernel).
|
||||
// D = A x B with A = 16 key blocks (row l%16 of a lane, k = 8*(l/16)+i), B = 16 queries (column l%16, same k):
|
||||
// a lane ends up with 8 consecutive blocks of ONE query, written as one run. No LDS for keys: a lane reads its
|
||||
// 32-byte slice of the key row from global (the 16 query tiles of a launch re-read them from L2); the queries are
|
||||
// split once per CTA into LDS.
|
||||
#if defined(__gfx1200__) || defined(__gfx1201__)
|
||||
#define STRATA_SEL_GFX12 1
|
||||
#else
|
||||
#define STRATA_SEL_GFX12 0
|
||||
#endif
|
||||
typedef short sel_s8 __attribute__((ext_vector_type(8)));
|
||||
typedef float sel_f8 __attribute__((ext_vector_type(8)));
|
||||
typedef uint32_t sel_u4 __attribute__((ext_vector_type(4)));
|
||||
constexpr int WQT = 16; // queries per CTA (the N of one WMMA)
|
||||
constexpr int WITER = 4; // key tiles per warp (the CTA covers 4 warps * WITER * 16 blocks)
|
||||
constexpr int WQS = IDX_DIM + 8; // bf16 elements per LDS row: 272 bytes, conflict-free 16-byte reads
|
||||
|
||||
__device__ __forceinline__ sel_f8 wmma_bf16(const sel_s8& a, const sel_s8& b, const sel_f8& c) {
|
||||
#if STRATA_SEL_GFX12
|
||||
return __builtin_amdgcn_wmma_f32_16x16x16_bf16_w32_gfx12(a, b, c);
|
||||
#else
|
||||
__trap();
|
||||
return c;
|
||||
#endif
|
||||
}
|
||||
// the bf16 pieces of x as fp32 bit patterns whose upper 16 bits are the bf16 (lower 16 are zero)
|
||||
__device__ __forceinline__ void split3(float x, uint32_t& hi, uint32_t& mid, uint32_t& lo) {
|
||||
hi = __float_as_uint(x) & 0xffff0000u;
|
||||
const float r1 = x - __uint_as_float(hi); // exact
|
||||
mid = __float_as_uint(r1) & 0xffff0000u;
|
||||
lo = __float_as_uint(r1 - __uint_as_float(mid)) & 0xffff0000u; // exact: at most 8 significant bits left
|
||||
}
|
||||
__device__ __forceinline__ uint32_t pack_bf16x2(uint32_t a, uint32_t b) { // low half = a's bf16, high half = b's
|
||||
#if STRATA_SEL_GFX12
|
||||
return __builtin_amdgcn_perm(b, a, 0x07060302u);
|
||||
#else
|
||||
return (a >> 16) | (b & 0xffff0000u);
|
||||
#endif
|
||||
}
|
||||
|
||||
__global__ void __launch_bounds__(128) block_scores_wmma_kernel(const float* __restrict__ pooled,
|
||||
const float* __restrict__ q_idx,
|
||||
const int32_t* __restrict__ steps, int64_t nq,
|
||||
int64_t max_blocks, int64_t reach,
|
||||
float* __restrict__ out) {
|
||||
__shared__ __align__(16) uint16_t sq[3][IDX_HEADS][WQT][WQS]; // hi / mid / lo, [head][query][dim]
|
||||
__shared__ int s_nbid[WQT];
|
||||
const int t = threadIdx.x, lane = t & 31, warp = t >> 5, l16 = lane & 15, g = lane >> 4;
|
||||
const int64_t q0 = (int64_t) blockIdx.y * WQT;
|
||||
for (int i = t; i < WQT * IDX_HEADS * IDX_DIM / 4; i += 128) {
|
||||
const int row = i / (IDX_DIM / 4), c = i % (IDX_DIM / 4);
|
||||
const int qr = row / IDX_HEADS, h = row % IDX_HEADS;
|
||||
float4 v = make_float4(0.f, 0.f, 0.f, 0.f);
|
||||
if (q0 + qr < nq) v = reinterpret_cast<const float4*>(q_idx + (q0 + qr) * IDX_HEADS * IDX_DIM)[h * (IDX_DIM / 4) + c];
|
||||
const float x[4] = {v.x, v.y, v.z, v.w};
|
||||
uint32_t hb[4], mb[4], lb[4];
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 4; ++j) split3(x[j], hb[j], mb[j], lb[j]);
|
||||
*reinterpret_cast<uint2*>(&sq[0][h][qr][c * 4]) = make_uint2(pack_bf16x2(hb[0], hb[1]), pack_bf16x2(hb[2], hb[3]));
|
||||
*reinterpret_cast<uint2*>(&sq[1][h][qr][c * 4]) = make_uint2(pack_bf16x2(mb[0], mb[1]), pack_bf16x2(mb[2], mb[3]));
|
||||
*reinterpret_cast<uint2*>(&sq[2][h][qr][c * 4]) = make_uint2(pack_bf16x2(lb[0], lb[1]), pack_bf16x2(lb[2], lb[3]));
|
||||
}
|
||||
if (t < WQT) s_nbid[t] = q0 + t < nq ? steps[(q0 + t) * kStepCount + kStepNBid] : 0;
|
||||
__syncthreads();
|
||||
int hi_nbid = 0;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < WQT; ++i) hi_nbid = max(hi_nbid, s_nbid[i]);
|
||||
const int64_t qi = q0 + l16;
|
||||
const int nb_q = s_nbid[l16];
|
||||
for (int it = 0; it < WITER; ++it) {
|
||||
const int64_t b0 = (((int64_t) blockIdx.x * WITER + it) * 4 + warp) * 16;
|
||||
if (b0 >= reach || b0 >= hi_nbid) break; // warp-uniform; later tiles start higher
|
||||
const int64_t row = b0 + l16;
|
||||
const bool rv = row < hi_nbid && row < max_blocks;
|
||||
const float* kp = pooled + (rv ? row : 0) * IDX_DIM + g * 8;
|
||||
// acc: the hi*hi products; cor: the five smaller ones (<= 2^-8 of it). Kept apart and added once at the end: a
|
||||
// correction summed into the big accumulator is rounded to ITS ulp at every one of the 48 steps (measured: 8e-7
|
||||
// of the score scale; apart, near the warp kernel's)
|
||||
sel_f8 acc[IDX_HEADS], cor[IDX_HEADS];
|
||||
#pragma unroll
|
||||
for (int h = 0; h < IDX_HEADS; ++h) acc[h] = cor[h] = sel_f8{0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f};
|
||||
#pragma unroll 2
|
||||
for (int kk = 0; kk < IDX_DIM / 16; ++kk) {
|
||||
float4 k0 = make_float4(0.f, 0.f, 0.f, 0.f), k1 = k0;
|
||||
if (rv) {
|
||||
k0 = *reinterpret_cast<const float4*>(kp + kk * 16);
|
||||
k1 = *reinterpret_cast<const float4*>(kp + kk * 16 + 4);
|
||||
}
|
||||
const float x[8] = {k0.x, k0.y, k0.z, k0.w, k1.x, k1.y, k1.z, k1.w};
|
||||
uint32_t hb[8], mb[8], lb[8];
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 8; ++j) split3(x[j], hb[j], mb[j], lb[j]);
|
||||
sel_u4 ah, am, al;
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
ah[j] = pack_bf16x2(hb[2 * j], hb[2 * j + 1]);
|
||||
am[j] = pack_bf16x2(mb[2 * j], mb[2 * j + 1]);
|
||||
al[j] = pack_bf16x2(lb[2 * j], lb[2 * j + 1]);
|
||||
}
|
||||
const sel_s8 Ah = __builtin_bit_cast(sel_s8, ah), Am = __builtin_bit_cast(sel_s8, am),
|
||||
Al = __builtin_bit_cast(sel_s8, al);
|
||||
#pragma unroll
|
||||
for (int h = 0; h < IDX_HEADS; ++h) {
|
||||
const sel_s8 Bh = *reinterpret_cast<const sel_s8*>(&sq[0][h][l16][kk * 16 + g * 8]);
|
||||
const sel_s8 Bm = *reinterpret_cast<const sel_s8*>(&sq[1][h][l16][kk * 16 + g * 8]);
|
||||
const sel_s8 Bl = *reinterpret_cast<const sel_s8*>(&sq[2][h][l16][kk * 16 + g * 8]);
|
||||
cor[h] = wmma_bf16(Al, Bh, cor[h]);
|
||||
cor[h] = wmma_bf16(Ah, Bl, cor[h]);
|
||||
cor[h] = wmma_bf16(Am, Bm, cor[h]);
|
||||
cor[h] = wmma_bf16(Ah, Bm, cor[h]);
|
||||
cor[h] = wmma_bf16(Am, Bh, cor[h]);
|
||||
acc[h] = wmma_bf16(Ah, Bh, acc[h]);
|
||||
}
|
||||
}
|
||||
// relu per head, heads added in order (as the warp kernel); this lane: query qi, blocks b0 + 8g .. 8g+7
|
||||
if (qi < nq) {
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const int64_t b = b0 + g * 8 + i;
|
||||
if (b >= nb_q || b >= max_blocks) continue;
|
||||
float score = 0.0f;
|
||||
#pragma unroll
|
||||
for (int h = 0; h < IDX_HEADS; ++h) {
|
||||
const float d = acc[h][i] + cor[h][i];
|
||||
score += d > 0.0f ? d : 0.0f;
|
||||
}
|
||||
out[qi * max_blocks + b] = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the gfx12 kernel needs gfx1200/gfx1201 code objects and a gfx12 device (gfx1100 has WMMA too, with another layout)
|
||||
bool sel_gfx12_device() {
|
||||
static int ok[64] = {}; // per device: 0 unknown, 1 yes, 2 no
|
||||
int dev = 0;
|
||||
if (cudaGetDevice(&dev) != cudaSuccess || dev < 0 || dev >= 64) { cudaGetLastError(); return false; }
|
||||
if (ok[dev] == 0) {
|
||||
cudaDeviceProp prop;
|
||||
ok[dev] = (cudaGetDeviceProperties(&prop, dev) == cudaSuccess &&
|
||||
(std::strncmp(prop.gcnArchName, "gfx1200", 7) == 0 || std::strncmp(prop.gcnArchName, "gfx1201", 7) == 0))
|
||||
? 1 : 2;
|
||||
cudaGetLastError();
|
||||
}
|
||||
return ok[dev] == 1;
|
||||
}
|
||||
#endif // __HIPCC__
|
||||
|
||||
// the tail block n_bid of each query: exactly block_scores_kernel's arithmetic for that block
|
||||
__global__ void __launch_bounds__(32) block_scores_tail_kernel(const float* __restrict__ dead,
|
||||
const float* __restrict__ q_idx,
|
||||
@@ -308,6 +466,14 @@ __global__ void __launch_bounds__(32) block_scores_tail_kernel(const float* __re
|
||||
// block_topk_kernel's (radix threshold, ties to the lowest index, cells ascending): identical ids.
|
||||
constexpr int TK_T = 1024;
|
||||
constexpr int TK_PER = 33;
|
||||
#if defined(__HIPCC__)
|
||||
// AMD (RDNA, wave32): a 1,024-thread block may use 192 VGPRs per lane, so the keys of 66 blocks per thread fit - contexts
|
||||
// up to 4 * 1024 * 66 = 270,336 cells (the 262,144 --max-context) keep the register kernel. NVIDIA's 64 registers per
|
||||
// thread at 1,024 threads allow only TK_PER.
|
||||
constexpr int TK_PER_MAX = 66;
|
||||
#else
|
||||
constexpr int TK_PER_MAX = TK_PER;
|
||||
#endif
|
||||
|
||||
__device__ __forceinline__ int block_excl_scan(int v, int* s_warp, int& total) {
|
||||
const int lane = threadIdx.x & 31, warp = threadIdx.x >> 5;
|
||||
@@ -337,6 +503,7 @@ __device__ __forceinline__ int block_excl_scan(int v, int* s_warp, int& total) {
|
||||
return r;
|
||||
}
|
||||
|
||||
template <int PER>
|
||||
__global__ void __launch_bounds__(TK_T) block_topk_reg_kernel(const float* __restrict__ scores,
|
||||
const int32_t* __restrict__ steps, int64_t max_blocks,
|
||||
int64_t cap, int32_t* __restrict__ ids) {
|
||||
@@ -354,11 +521,11 @@ __global__ void __launch_bounds__(TK_T) block_topk_reg_kernel(const float* __res
|
||||
}
|
||||
const float* sc = scores + qi * max_blocks;
|
||||
const int64_t nb = n_bid + 1;
|
||||
const int64_t per = (nb + TK_T - 1) / TK_T; // <= TK_PER (the caller checks)
|
||||
const int64_t per = (nb + TK_T - 1) / TK_T; // <= PER (the caller checks)
|
||||
const int64_t b0 = (int64_t) t * per, b1 = (b0 + per < nb) ? b0 + per : nb;
|
||||
uint32_t key[TK_PER];
|
||||
uint32_t key[PER];
|
||||
#pragma unroll
|
||||
for (int j = 0; j < TK_PER; ++j) key[j] = (b0 + j < b1) ? order_key(sc[b0 + j]) : 0u;
|
||||
for (int j = 0; j < PER; ++j) key[j] = (b0 + j < b1) ? order_key(sc[b0 + j]) : 0u;
|
||||
auto weight = [&](int64_t b) -> int { return b < n_bid ? R : (int) (n_kv - n_bid * R); };
|
||||
uint32_t prefix = 0;
|
||||
int above = 0;
|
||||
@@ -367,7 +534,7 @@ __global__ void __launch_bounds__(TK_T) block_topk_reg_kernel(const float* __res
|
||||
__syncwarp();
|
||||
const uint32_t hi_mask = shift == 24 ? 0u : (0xffffffffu << (shift + 8));
|
||||
#pragma unroll
|
||||
for (int j = 0; j < TK_PER; ++j) {
|
||||
for (int j = 0; j < PER; ++j) {
|
||||
const int64_t b = b0 + j;
|
||||
if (b >= b1) break;
|
||||
const int w = weight(b);
|
||||
@@ -399,7 +566,7 @@ __global__ void __launch_bounds__(TK_T) block_topk_reg_kernel(const float* __res
|
||||
const int64_t eq_budget = width - above;
|
||||
int gt = 0, eq = 0;
|
||||
#pragma unroll
|
||||
for (int j = 0; j < TK_PER; ++j) {
|
||||
for (int j = 0; j < PER; ++j) {
|
||||
const int64_t b = b0 + j;
|
||||
if (b >= b1) break;
|
||||
const int w = weight(b);
|
||||
@@ -416,7 +583,7 @@ __global__ void __launch_bounds__(TK_T) block_topk_reg_kernel(const float* __res
|
||||
int64_t wpos = block_excl_scan(sel, s_warp, tot);
|
||||
int64_t eq_left = my_eq;
|
||||
#pragma unroll
|
||||
for (int j = 0; j < TK_PER; ++j) {
|
||||
for (int j = 0; j < PER; ++j) {
|
||||
const int64_t b = b0 + j;
|
||||
if (b >= b1) break;
|
||||
const int w = weight(b);
|
||||
@@ -477,6 +644,229 @@ __global__ void __launch_bounds__(SCORE_WARPS * 32) block_scores_multi_kernel(co
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(__HIPCC__)
|
||||
// ---- the decode top-k on a thread-block cluster (sm_90+; S19). One CTA per query (block_topk_reg_kernel, or
|
||||
// block_topk_kernel above 33,792 blocks: a --max-context over ~135K) makes four radix passes and two scans over up to
|
||||
// 65,538 blocks on ONE SM while the rest of the GPU idles - a decode window has 1-5 queries. Here a cluster of CL_N
|
||||
// CTAs shares a query: CTA r holds the keys of blocks [r * per, (r + 1) * per) in shared memory, builds the digit
|
||||
// histogram of its keys, and PUSHES it into slot r of every CTA's `hin` (distributed shared memory); after one cluster
|
||||
// barrier each CTA sums the CL_N slots itself, so all of them take the same digit. The emit pass needs each CTA's
|
||||
// cells above / at the threshold: pushed the same way, then each CTA offsets its own by the ranks before it.
|
||||
// RTX 5070, per call at capacity = context (decode_cluster_parity --bench): 21.9 -> 15.6 us at 32K, 58 -> 18 at 128K,
|
||||
// 200 -> 22 at 262K (the one-CTA kernels' digit search was serial, too: one thread over 256 bins per pass).
|
||||
//
|
||||
// IDENTICAL IDS, by construction rather than by luck: the threshold thr (the width-th largest key, cells counted with
|
||||
// their weights) and `above` (cells with a larger key) are pure functions of the multiset of (key, weight) - integer
|
||||
// histograms, summed in any order - and the digit rule is block_topk_kernel's, written as a scan (the largest digit d
|
||||
// whose cells at or above it reach `need`; 0 when none). The cells emitted are then fixed: every cell of a block with
|
||||
// key > thr, and the first eq_budget = width - above cells at thr in ascending order (a block at thr may be cut),
|
||||
// written in ascending cell order. Position of a thread's first cell = (cells above thr before it) + min(cells at thr
|
||||
// before it, eq_budget): the telescoped sum of the reference's per-thread clamp. NaN keys are 0 (order_key) as there.
|
||||
//
|
||||
// Barriers: barrier.cluster arrive / wait (each CTA's threads all take part; it also orders the CTA's own shared
|
||||
// memory, so it doubles as __syncthreads). Phase 0 is a relaxed arrive at entry, waited before the first remote store
|
||||
// (a CTA's shared memory exists once it runs). Pushes go out before the arrive that releases them, and nothing reads
|
||||
// another CTA's memory after the last wait - so a CTA may exit without a closing barrier (a wait counts the threads
|
||||
// that have not exited). `hin` alternates by pass parity: CTA x reads slot buffer p&1 in pass p before arriving at
|
||||
// pass p+1's barrier, and nobody writes buffer p&1 again (pass p+2) before waiting on that barrier.
|
||||
constexpr int CL_N = 8; // CTAs per query (the portable cluster size)
|
||||
constexpr int CL_T = 1024; // threads per CTA
|
||||
constexpr int CL_MAXQ = 16; // larger calls (prefill sub-batches) fill the GPU with one CTA per query already
|
||||
#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 900
|
||||
#define STRATA_SEL_CLUSTER 1
|
||||
#else
|
||||
#define STRATA_SEL_CLUSTER 0 // an older target's code is a trap; qsa_block_topk_cluster never launches it there
|
||||
#endif
|
||||
|
||||
#if STRATA_SEL_CLUSTER
|
||||
__device__ __forceinline__ void cl_arrive_relaxed() {
|
||||
asm volatile("barrier.cluster.arrive.relaxed.aligned;\n" ::: "memory");
|
||||
}
|
||||
__device__ __forceinline__ void cl_arrive() { asm volatile("barrier.cluster.arrive.release.aligned;\n" ::: "memory"); }
|
||||
__device__ __forceinline__ void cl_wait() { asm volatile("barrier.cluster.wait.acquire.aligned;\n" ::: "memory"); }
|
||||
__device__ __forceinline__ unsigned cl_rank() {
|
||||
unsigned r;
|
||||
asm volatile("mov.u32 %0, %%cluster_ctarank;\n" : "=r"(r));
|
||||
return r;
|
||||
}
|
||||
template <typename T> __device__ __forceinline__ T* cl_map(T* p, unsigned rank) { // p in CTA `rank`'s shared memory
|
||||
uint64_t o;
|
||||
asm volatile("mapa.u64 %0, %1, %2;\n" : "=l"(o) : "l"((uint64_t) p), "r"(rank));
|
||||
return reinterpret_cast<T*>(o);
|
||||
}
|
||||
// exclusive block scan of a 64-bit value over CL_T threads; `total` = the CTA's sum
|
||||
__device__ __forceinline__ unsigned long long cl_excl_scan64(unsigned long long v, unsigned long long* s_warp,
|
||||
unsigned long long& total) {
|
||||
const int lane = threadIdx.x & 31, warp = threadIdx.x >> 5;
|
||||
unsigned long long x = v;
|
||||
#pragma unroll
|
||||
for (int o = 1; o < 32; o <<= 1) {
|
||||
const unsigned long long y = __shfl_up_sync(0xffffffffu, x, o);
|
||||
if (lane >= o) x += y;
|
||||
}
|
||||
if (lane == 31) s_warp[warp] = x;
|
||||
__syncthreads();
|
||||
if (warp == 0) {
|
||||
const unsigned long long w = s_warp[lane];
|
||||
unsigned long long z = w;
|
||||
#pragma unroll
|
||||
for (int o = 1; o < 32; o <<= 1) {
|
||||
const unsigned long long y = __shfl_up_sync(0xffffffffu, z, o);
|
||||
if (lane >= o) z += y;
|
||||
}
|
||||
s_warp[lane] = z - w;
|
||||
if (lane == 31) s_warp[32] = z;
|
||||
}
|
||||
__syncthreads();
|
||||
total = s_warp[32];
|
||||
return s_warp[warp] + x - v;
|
||||
}
|
||||
#endif
|
||||
|
||||
// grid (CL_N, nq), cluster (CL_N, 1, 1), CL_T threads, dynamic shared memory: ceil(max_blocks / CL_N) keys
|
||||
__global__ void __launch_bounds__(CL_T) block_topk_cluster_kernel(const float* __restrict__ scores,
|
||||
const int32_t* __restrict__ steps, int64_t max_blocks,
|
||||
int64_t cap, int32_t* __restrict__ ids) {
|
||||
#if STRATA_SEL_CLUSTER
|
||||
extern __shared__ uint32_t keys[]; // this CTA's blocks' keys
|
||||
__shared__ __align__(16) int hin[2][CL_N][256]; // the cluster's histograms, slot = the pushing rank
|
||||
__shared__ __align__(16) int hloc[2][256]; // this CTA's histogram
|
||||
__shared__ unsigned long long cnt[CL_N]; // per rank: its cells above thr (low 32 bits) and at it (high)
|
||||
__shared__ unsigned long long s_warp[33];
|
||||
__shared__ uint32_t s_prefix;
|
||||
__shared__ int s_above;
|
||||
const unsigned rank = cl_rank();
|
||||
const int t = threadIdx.x, lane = t & 31, warp = t >> 5;
|
||||
cl_arrive_relaxed(); // phase 0: this CTA runs
|
||||
const int64_t qi = blockIdx.y;
|
||||
const int32_t* st = steps + qi * kStepCount;
|
||||
const int64_t n_kv = st[kStepNKv], n_bid = st[kStepNBid], width = st[kStepWidth];
|
||||
int32_t* out = ids + qi * cap;
|
||||
if (n_kv <= width) { // the identity, split over the cluster; no remote access
|
||||
for (int64_t j = (int64_t) rank * CL_T + t; j < n_kv; j += (int64_t) CL_N * CL_T) out[j] = (int32_t) j;
|
||||
return;
|
||||
}
|
||||
// the tail block n_bid weighs its cells (0..3); with none it is not a candidate (the reference skips w == 0)
|
||||
const int wtail = (int) (n_kv - n_bid * R);
|
||||
const int64_t nbe = n_bid + (wtail > 0 ? 1 : 0);
|
||||
const int64_t per = (nbe + CL_N - 1) / CL_N; // <= ceil(max_blocks / CL_N), the keys the host sized
|
||||
const int64_t lo = (int64_t) rank * per < nbe ? (int64_t) rank * per : nbe;
|
||||
const int64_t hi = lo + per < nbe ? lo + per : nbe;
|
||||
const int n = (int) (hi - lo);
|
||||
const int tail_i = (wtail > 0 && n_bid >= lo && n_bid < hi) ? (int) (n_bid - lo) : -1;
|
||||
const float* sc = scores + qi * max_blocks + lo;
|
||||
for (int i = t; i < n; i += CL_T) keys[i] = order_key(sc[i]);
|
||||
for (int i = t; i < 2 * 256; i += CL_T) (&hloc[0][0])[i] = 0;
|
||||
__syncthreads();
|
||||
uint32_t prefix = 0;
|
||||
int above = 0; // cells strictly above the digits fixed so far
|
||||
for (int pass = 0; pass < 4; ++pass) {
|
||||
const int shift = 24 - 8 * pass;
|
||||
const uint32_t hmask = pass == 0 ? 0u : (0xffffffffu << (shift + 8));
|
||||
int* hl = hloc[pass & 1];
|
||||
// warp-aggregated: the scores share their top bits, so plain atomics would queue on a few bins
|
||||
for (int i0 = 0; i0 < n; i0 += CL_T) {
|
||||
const int i = i0 + t;
|
||||
int bin = -1;
|
||||
if (i < n) {
|
||||
const uint32_t k = keys[i];
|
||||
if ((k & hmask) == prefix) bin = (int) ((k >> shift) & 255);
|
||||
}
|
||||
const unsigned same = __match_any_sync(0xffffffffu, bin);
|
||||
if (bin >= 0 && lane == __ffs(same) - 1) atomicAdd(&hl[bin], __popc(same) * R);
|
||||
if (bin >= 0 && i == tail_i) atomicAdd(&hl[bin], wtail - R);
|
||||
}
|
||||
__syncthreads();
|
||||
if (pass == 0) cl_wait(); // phase 0 done: every CTA's shared memory is there
|
||||
for (int i = t; i < CL_N * 64; i += CL_T) { // the histogram into slot `rank` of every CTA, as int4
|
||||
const int dst = i >> 6, c = i & 63;
|
||||
*cl_map(reinterpret_cast<int4*>(&hin[pass & 1][rank][0]) + c, (unsigned) dst) =
|
||||
reinterpret_cast<const int4*>(hl)[c];
|
||||
}
|
||||
for (int i = t; i < 256; i += CL_T) hloc[(pass + 1) & 1][i] = 0;
|
||||
cl_arrive();
|
||||
cl_wait();
|
||||
if (warp == 0) {
|
||||
// lane L owns digits 255 - 8L down to 248 - 8L; the digit holding the need-th cell from the top
|
||||
const int need = (int) width - above;
|
||||
int tot[8], part = 0;
|
||||
#pragma unroll
|
||||
for (int k = 0; k < 8; ++k) {
|
||||
const int d = 255 - 8 * lane - k;
|
||||
int v = 0;
|
||||
#pragma unroll
|
||||
for (int r = 0; r < CL_N; ++r) v += hin[pass & 1][r][d];
|
||||
tot[k] = v;
|
||||
part += v;
|
||||
}
|
||||
int incl = part;
|
||||
#pragma unroll
|
||||
for (int o = 1; o < 32; o <<= 1) {
|
||||
const int y = __shfl_up_sync(0xffffffffu, incl, o);
|
||||
if (lane >= o) incl += y;
|
||||
}
|
||||
const int excl = incl - part;
|
||||
const unsigned hit = __ballot_sync(0xffffffffu, excl < need && incl >= need);
|
||||
// no hit cannot happen (the cells total n_kv > width); the reference would then take digit 0
|
||||
const int who = hit ? __ffs(hit) - 1 : 31;
|
||||
if (lane == who) {
|
||||
int acc = excl, k = 0;
|
||||
for (; k < 7; ++k) {
|
||||
if (acc + tot[k] >= need) break;
|
||||
acc += tot[k];
|
||||
}
|
||||
s_prefix = prefix | ((uint32_t) (255 - 8 * lane - k) << shift);
|
||||
s_above = above + acc;
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
prefix = s_prefix;
|
||||
above = s_above;
|
||||
}
|
||||
const uint32_t thr = prefix;
|
||||
const int64_t eq_budget = width - above; // cells at thr that fit, lowest index first
|
||||
// each thread a contiguous run of the CTA's keys; an odd run length keeps the shared reads conflict-free
|
||||
int seg = (n + CL_T - 1) / CL_T;
|
||||
if ((seg & 1) == 0 && seg > 0) ++seg;
|
||||
const int s0 = t * seg < n ? t * seg : n, s1 = s0 + seg < n ? s0 + seg : n;
|
||||
uint32_t gt = 0, eq = 0;
|
||||
for (int i = s0; i < s1; ++i) {
|
||||
const uint32_t k = keys[i], w = i == tail_i ? (uint32_t) wtail : (uint32_t) R;
|
||||
if (k > thr) gt += w;
|
||||
else if (k == thr) eq += w;
|
||||
}
|
||||
unsigned long long cta;
|
||||
const unsigned long long before =
|
||||
cl_excl_scan64((unsigned long long) gt | ((unsigned long long) eq << 32), s_warp, cta);
|
||||
if (t < CL_N) *cl_map(&cnt[rank], (unsigned) t) = cta;
|
||||
cl_arrive();
|
||||
cl_wait();
|
||||
int64_t gt_off = 0, eq_off = 0;
|
||||
for (unsigned r = 0; r < rank; ++r) {
|
||||
gt_off += (int64_t) (cnt[r] & 0xffffffffu);
|
||||
eq_off += (int64_t) (cnt[r] >> 32);
|
||||
}
|
||||
const int64_t eq_before = eq_off + (int64_t) (before >> 32);
|
||||
int64_t wpos = gt_off + (int64_t) (before & 0xffffffffu) + (eq_before < eq_budget ? eq_before : eq_budget);
|
||||
int64_t eq_left = eq_budget - eq_before;
|
||||
if (eq_left < 0) eq_left = 0;
|
||||
for (int i = s0; i < s1; ++i) {
|
||||
const uint32_t k = keys[i];
|
||||
const int w = i == tail_i ? wtail : R;
|
||||
const int64_t b = lo + i;
|
||||
if (k > thr) {
|
||||
for (int c = 0; c < w; ++c) out[wpos++] = (int32_t) (b * R + c);
|
||||
} else if (k == thr) {
|
||||
for (int c = 0; c < w && eq_left > 0; ++c, --eq_left) out[wpos++] = (int32_t) (b * R + c);
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void) scores; (void) steps; (void) max_blocks; (void) cap; (void) ids;
|
||||
__trap();
|
||||
#endif
|
||||
}
|
||||
#endif // !__HIPCC__
|
||||
} // namespace
|
||||
|
||||
void qsa_block_scores(const float* pooled, const float* dead, const float* q_idx, const int32_t* steps, int64_t nq,
|
||||
@@ -507,6 +897,25 @@ bool qsa_block_scores_tc(const float* pooled, const float* dead, const float* q_
|
||||
int64_t max_blocks, const QsaShapes& s, float* scores, void* stream, int64_t active_blocks) {
|
||||
if (nq <= 0) return true;
|
||||
if (s.idx_dim != IDX_DIM || s.idx_n_head != IDX_HEADS || s.idx_block != R || nq > 65535 * TC_QT) return false;
|
||||
#if defined(__HIPCC__)
|
||||
// AMD: the gfx12 (RDNA4) WMMA scorer, opt-in (STRATA_SELECT_WMMA=1): it selects slightly differently from the warp
|
||||
// kernel (254/256 queries the same), so the default keeps the warp kernel; every other target keeps it too (false)
|
||||
static const bool wmma_on = [] {
|
||||
const char* v = std::getenv("STRATA_SELECT_WMMA");
|
||||
return v != nullptr && v[0] != '\0' && v[0] != '0';
|
||||
}();
|
||||
if (!wmma_on || !sel_gfx12_device()) return false;
|
||||
{
|
||||
const int64_t reach = active_blocks > 0 && active_blocks < max_blocks ? active_blocks : max_blocks;
|
||||
const int64_t per = (int64_t) 4 * WITER * 16;
|
||||
const dim3 grid((unsigned) ((reach + per - 1) / per), (unsigned) ((nq + WQT - 1) / WQT));
|
||||
block_scores_wmma_kernel<<<grid, 128, 0, (cudaStream_t) stream>>>(pooled, q_idx, steps, nq, max_blocks, reach, scores);
|
||||
block_scores_tail_kernel<<<(unsigned) nq, 32, 0, (cudaStream_t) stream>>>(dead, q_idx, steps, max_blocks, scores);
|
||||
const cudaError_t e = cudaGetLastError();
|
||||
if (e != cudaSuccess) { std::fprintf(stderr, "qsa_block_scores_tc: %s\n", cudaGetErrorString(e)); std::exit(1); }
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
{ // sm_80 or newer (TF32 MMA); an older card keeps the warp kernel
|
||||
static int cc_major[64] = {};
|
||||
int dev = 0;
|
||||
@@ -517,13 +926,12 @@ bool qsa_block_scores_tc(const float* pooled, const float* dead, const float* q_
|
||||
cudaGetLastError();
|
||||
return false;
|
||||
}
|
||||
cc_major[dev] = major;
|
||||
// STRATA_QSA_WARP=1|select (an A/B arm): the pre-sm_80 kernels on any card, as RTX 20 runs them
|
||||
const char* w = std::getenv("STRATA_QSA_WARP");
|
||||
cc_major[dev] = w && (!std::strcmp(w, "1") || !std::strcmp(w, "select")) ? 7 : strata::cc_major_of(major);
|
||||
}
|
||||
if (cc_major[dev] < 8) return false;
|
||||
}
|
||||
#if defined(__HIPCC__)
|
||||
return false; // the tensor-core kernel is compiled out on AMD (its major version is not a CUDA sm)
|
||||
#endif
|
||||
static bool attr[64] = {}; // the shared-memory opt-in is per device (a layer split runs it on several)
|
||||
int adev = 0;
|
||||
cudaGetDevice(&adev);
|
||||
@@ -545,6 +953,7 @@ bool qsa_block_scores_tc(const float* pooled, const float* dead, const float* q_
|
||||
const cudaError_t e = cudaGetLastError();
|
||||
if (e != cudaSuccess) { std::fprintf(stderr, "qsa_block_scores_tc: %s\n", cudaGetErrorString(e)); std::exit(1); }
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void qsa_block_topk_ref(const float* scores, const int32_t* steps, int64_t nq, int64_t max_blocks, int64_t cap,
|
||||
@@ -559,13 +968,107 @@ void qsa_block_topk_ref(const float* scores, const int32_t* steps, int64_t nq, i
|
||||
if (e != cudaSuccess) { std::fprintf(stderr, "qsa_block_topk: %s\n", cudaGetErrorString(e)); std::exit(1); }
|
||||
}
|
||||
|
||||
bool qsa_block_topk_cluster(const float* scores, const int32_t* steps, int64_t nq, int64_t max_blocks, int64_t cap,
|
||||
const QsaShapes& s, int32_t* ids, void* stream) {
|
||||
#if defined(__HIPCC__)
|
||||
(void) scores; (void) steps; (void) nq; (void) max_blocks; (void) cap; (void) s; (void) ids; (void) stream;
|
||||
return false;
|
||||
#else
|
||||
if (nq <= 0) return true;
|
||||
if (s.idx_block != R || cap < qsa_selection_width(kTopkMaxCells, s) || nq > 65535 || max_blocks <= 0) return false;
|
||||
const size_t smem = (size_t) ((max_blocks + CL_N - 1) / CL_N) * sizeof(uint32_t);
|
||||
// Per device (a layer split runs on several): 1 the cluster kernel runs here, 2 it does not. It needs sm_90+ (the
|
||||
// card's, or STRATA_EMULATE_CC's) AND code built for it: a build with only older code JIT-compiles their PTX, whose
|
||||
// copy of this kernel is a trap - the function's PTX version says which. `opt`: the dynamic shared memory opted in.
|
||||
static int ok[64] = {};
|
||||
static size_t opt[64] = {};
|
||||
int dev = 0;
|
||||
if (cudaGetDevice(&dev) != cudaSuccess || dev < 0 || dev >= 64) { cudaGetLastError(); return false; }
|
||||
if (ok[dev] == 0) {
|
||||
int major = 0;
|
||||
cudaFuncAttributes fa{};
|
||||
const bool code = cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev) == cudaSuccess &&
|
||||
strata::cc_major_of(major) >= 9 &&
|
||||
cudaFuncGetAttributes(&fa, block_topk_cluster_kernel) == cudaSuccess && fa.ptxVersion >= 90 &&
|
||||
fa.binaryVersion >= 90;
|
||||
cudaGetLastError();
|
||||
ok[dev] = code ? 1 : 2;
|
||||
}
|
||||
if (ok[dev] != 1) return false;
|
||||
if (smem > opt[dev]) { // a larger capacity: opt in, and check that a cluster of CL_N such CTAs can be resident
|
||||
int clusters = 0;
|
||||
cudaLaunchConfig_t q{};
|
||||
cudaLaunchAttribute qa[1];
|
||||
qa[0].id = cudaLaunchAttributeClusterDimension;
|
||||
qa[0].val.clusterDim.x = CL_N;
|
||||
qa[0].val.clusterDim.y = 1;
|
||||
qa[0].val.clusterDim.z = 1;
|
||||
q.gridDim = dim3(CL_N, 1, 1);
|
||||
q.blockDim = dim3(CL_T, 1, 1);
|
||||
q.dynamicSmemBytes = smem;
|
||||
q.attrs = qa;
|
||||
q.numAttrs = 1;
|
||||
if (cudaFuncSetAttribute(block_topk_cluster_kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, (int) smem) !=
|
||||
cudaSuccess ||
|
||||
cudaOccupancyMaxActiveClusters(&clusters, block_topk_cluster_kernel, &q) != cudaSuccess || clusters < 1) {
|
||||
cudaGetLastError();
|
||||
return false; // this capacity takes the one-CTA kernels; a smaller one may still fit
|
||||
}
|
||||
opt[dev] = smem;
|
||||
}
|
||||
cudaLaunchConfig_t cfg{};
|
||||
cudaLaunchAttribute at[1];
|
||||
at[0].id = cudaLaunchAttributeClusterDimension;
|
||||
at[0].val.clusterDim.x = CL_N;
|
||||
at[0].val.clusterDim.y = 1;
|
||||
at[0].val.clusterDim.z = 1;
|
||||
cfg.gridDim = dim3(CL_N, (unsigned) nq, 1);
|
||||
cfg.blockDim = dim3(CL_T, 1, 1);
|
||||
cfg.dynamicSmemBytes = smem;
|
||||
cfg.stream = (cudaStream_t) stream;
|
||||
cfg.attrs = at;
|
||||
cfg.numAttrs = 1;
|
||||
const cudaError_t e = cudaLaunchKernelEx(&cfg, block_topk_cluster_kernel, scores, steps, max_blocks, cap, ids);
|
||||
if (e != cudaSuccess) { std::fprintf(stderr, "qsa_block_topk cluster: %s\n", cudaGetErrorString(e)); std::exit(1); }
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void qsa_block_topk(const float* scores, const int32_t* steps, int64_t nq, int64_t max_blocks, int64_t cap,
|
||||
const QsaShapes& s, int32_t* ids, void* stream) {
|
||||
const QsaShapes& s, int32_t* ids, void* stream, int64_t active_blocks) {
|
||||
// keys in registers when every query's blocks fit (contexts up to ~135K cells); the same ids. STRATA_TOPK_OLD=1:
|
||||
// the kernel that reads them from memory on every pass
|
||||
static const bool old = std::getenv("STRATA_TOPK_OLD") != nullptr;
|
||||
if (nq <= 0) return;
|
||||
if (old || max_blocks > (int64_t) TK_T * TK_PER) {
|
||||
#if !defined(__HIPCC__)
|
||||
// sm_90+: a cluster of CL_N CTAs per query for the calls of a few queries (decode windows); the same ids.
|
||||
// STRATA_QSA_CLUSTER=0: the one-CTA kernels below
|
||||
static const bool cluster = [] {
|
||||
const char* v = std::getenv("STRATA_QSA_CLUSTER");
|
||||
return !v || std::atoi(v) != 0;
|
||||
}();
|
||||
if (cluster && !old && nq <= CL_MAXQ && qsa_block_topk_cluster(scores, steps, nq, max_blocks, cap, s, ids, stream))
|
||||
return;
|
||||
#endif
|
||||
// the blocks a query can have: the call's active count when the caller knows it (the prompt path), else the capacity.
|
||||
// Decode (no count) keeps the capacity rule and the original register width: nothing changes there.
|
||||
#if defined(__HIPCC__)
|
||||
const bool counted = active_blocks > 0;
|
||||
#else
|
||||
// CUDA keeps 0.1.32's capacity rule: #337's dispatch was measured on RDNA4 only, and on the RTX 5070 the 64K
|
||||
// prompts read 1-3% slower with it
|
||||
const bool counted = false;
|
||||
(void) active_blocks;
|
||||
#endif
|
||||
const int64_t reach = counted && active_blocks < max_blocks ? active_blocks : max_blocks;
|
||||
const int64_t fit = (int64_t) TK_T * (counted ? TK_PER_MAX : TK_PER);
|
||||
#if defined(__HIPCC__)
|
||||
constexpr int64_t kRegMinBlocks = 7168; // gfx1201: below ~28K cells the 1,024-thread kernel's fixed cost loses to the ref
|
||||
const bool too_small = counted && reach < kRegMinBlocks;
|
||||
#else
|
||||
const bool too_small = false;
|
||||
#endif
|
||||
if (old || too_small || reach > fit) {
|
||||
qsa_block_topk_ref(scores, steps, nq, max_blocks, cap, s, ids, stream);
|
||||
return;
|
||||
}
|
||||
@@ -573,7 +1076,10 @@ void qsa_block_topk(const float* scores, const int32_t* steps, int64_t nq, int64
|
||||
std::fprintf(stderr, "qsa_block_topk: unsupported geometry or cap\n");
|
||||
std::exit(1);
|
||||
}
|
||||
block_topk_reg_kernel<<<(unsigned) nq, TK_T, 0, (cudaStream_t) stream>>>(scores, steps, max_blocks, cap, ids);
|
||||
if (reach <= (int64_t) TK_T * TK_PER)
|
||||
block_topk_reg_kernel<TK_PER><<<(unsigned) nq, TK_T, 0, (cudaStream_t) stream>>>(scores, steps, max_blocks, cap, ids);
|
||||
else
|
||||
block_topk_reg_kernel<TK_PER_MAX><<<(unsigned) nq, TK_T, 0, (cudaStream_t) stream>>>(scores, steps, max_blocks, cap, ids);
|
||||
const cudaError_t e = cudaGetLastError();
|
||||
if (e != cudaSuccess) { std::fprintf(stderr, "qsa_block_topk: %s\n", cudaGetErrorString(e)); std::exit(1); }
|
||||
}
|
||||
|
||||
@@ -186,10 +186,185 @@ __global__ void router_top10_kernel(const float* __restrict__ logits, int n_toke
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__HIPCC__)
|
||||
// ---- S6, AMD: the same router, BIT-IDENTICAL, without its serial parts. Measured on RDNA4 (gfx1201) the kernel
|
||||
// above took 39 us per call in decode (16% of the GPU's decode time): the ascending double sum is 512 dependent FP64
|
||||
// adds (~50 cycles each there), the ten selection passes are 20 block barriers, and the renormalisation re-reads its
|
||||
// ten weights from global memory one by one. Here:
|
||||
// * the sum's ONLY use is inv = (float) (1.0 / sum). Every term is positive and the largest is exactly 1
|
||||
// (exp(0)), so the serial sum and any other summation order both lie within 2^-43 (relative) of the exact sum
|
||||
// (|serial - exact| <= 511u, |tree - exact| <= 21u, u = 2^-53). 1.0 / x and the float cast are monotone: when
|
||||
// the bracket [tree (1 - 2^-40), tree (1 + 2^-40)] maps to ONE float, that float is the serial sum's. Otherwise
|
||||
// (about 1 row in 1000 in the parity test, or a non-finite sum) thread 0 runs the serial sum as before;
|
||||
// * the selection runs on wave 0 alone with the probabilities in registers (16 per lane): the largest p with the
|
||||
// lowest index on ties is unique, so the butterfly finds the same expert as the block reduction did;
|
||||
// * the renormalisation sums the ten weights in rank order from registers and divides them in parallel.
|
||||
// Same expressions, same operands: the ids and weights are bitwise those of router_top10_kernel (hip_router_fast
|
||||
// checks it on 65,536 rows with ties, near-ties, NaN rows and the forced serial path). STRATA_HIP_ROUTER_OLD=1: the
|
||||
// kernel above.
|
||||
template <bool FORCE_SERIAL>
|
||||
__global__ void router_top10_fast_kernel(const float* __restrict__ logits, int n_tokens, int n_expert, int k,
|
||||
int* __restrict__ ids, float* __restrict__ weights) {
|
||||
extern __shared__ unsigned char s_raw[];
|
||||
const size_t taken_bytes = ((size_t) n_expert + 15u) & ~(size_t) 15u;
|
||||
double* s_ex = (double*) (s_raw + taken_bytes);
|
||||
__shared__ float s_red[RT_MAX_THREADS / 32];
|
||||
__shared__ double s_dred[RT_MAX_THREADS / 32];
|
||||
__shared__ float s_inv;
|
||||
const int t = blockIdx.x;
|
||||
if (t >= n_tokens) return;
|
||||
const int tid = threadIdx.x, lane = tid & 31, warp = tid >> 5;
|
||||
const int nt = blockDim.x, nw = (nt + 31) >> 5;
|
||||
const float* l = logits + (size_t) t * n_expert;
|
||||
// the max: as router_top10_kernel (exact, order-independent)
|
||||
float mx = -INFINITY;
|
||||
for (int e = tid; e < n_expert; e += nt) mx = fmaxf(mx, l[e]);
|
||||
for (int off = 16; off > 0; off >>= 1) mx = fmaxf(mx, __shfl_down_sync(0xffffffffu, mx, off));
|
||||
if (lane == 0) s_red[warp] = mx;
|
||||
__syncthreads();
|
||||
if (tid < 32) {
|
||||
float v = (tid < nw) ? s_red[tid] : -INFINITY;
|
||||
for (int off = 16; off > 0; off >>= 1) v = fmaxf(v, __shfl_down_sync(0xffffffffu, v, off));
|
||||
if (tid == 0) s_red[0] = v;
|
||||
}
|
||||
__syncthreads();
|
||||
mx = s_red[0];
|
||||
// the exponentials (the same expression), and a tree sum of them for the bracket
|
||||
double part = 0.0;
|
||||
for (int e = tid; e < n_expert; e += nt) {
|
||||
const double x = exp((double) l[e] - (double) mx);
|
||||
s_ex[e] = x;
|
||||
part += x;
|
||||
}
|
||||
for (int off = 16; off > 0; off >>= 1) part += __shfl_xor_sync(0xffffffffu, part, off);
|
||||
if (lane == 0) s_dred[warp] = part;
|
||||
__syncthreads();
|
||||
double tot = 0.0;
|
||||
for (int w = 0; w < nw; ++w) tot += s_dred[w];
|
||||
const double lo = tot * (1.0 - 0x1p-40), hi = tot * (1.0 + 0x1p-40);
|
||||
const float f_lo = (float) (1.0 / hi), f_hi = (float) (1.0 / lo);
|
||||
float inv;
|
||||
if (!FORCE_SERIAL && tot >= 1.0 && hi < 1e300 && f_lo == f_hi) {
|
||||
inv = f_lo;
|
||||
} else { // block-uniform (every thread computed the same tot): the serial sum, exactly as router_top10_kernel
|
||||
if (tid == 0) {
|
||||
double sum = 0.0;
|
||||
for (int e = 0; e < n_expert; ++e) sum += s_ex[e];
|
||||
s_inv = (float) (1.0 / sum);
|
||||
}
|
||||
__syncthreads();
|
||||
inv = s_inv;
|
||||
}
|
||||
if (warp != 0) return;
|
||||
// wave 0: lane holds experts lane + 32 j; p is router_top10_kernel's expression
|
||||
constexpr int PER = 16;
|
||||
float pv[PER];
|
||||
unsigned live = 0;
|
||||
#pragma unroll
|
||||
for (int j = 0; j < PER; ++j) {
|
||||
const int e = lane + 32 * j;
|
||||
pv[j] = -INFINITY;
|
||||
if (e < n_expert) { pv[j] = (float) (s_ex[e] * inv); live |= 1u << j; }
|
||||
}
|
||||
float my_w = 0.0f;
|
||||
int my_id = 0, nsel = 0;
|
||||
for (int i = 0; i < k; ++i) {
|
||||
float bv = -INFINITY;
|
||||
int bi = n_expert; // a sentinel that loses to every real index
|
||||
#pragma unroll
|
||||
for (int j = 0; j < PER; ++j)
|
||||
if (((live >> j) & 1u) && pv[j] > bv) { bv = pv[j]; bi = lane + 32 * j; }
|
||||
for (int off = 16; off > 0; off >>= 1) {
|
||||
const float ov = __shfl_xor_sync(0xffffffffu, bv, off);
|
||||
const int oi = __shfl_xor_sync(0xffffffffu, bi, off);
|
||||
if (ov > bv || (ov == bv && oi < bi)) { bv = ov; bi = oi; }
|
||||
}
|
||||
if (bi >= n_expert) break; // as router_top10_kernel: no rank i or later is written (every p left is NaN)
|
||||
if ((bi & 31) == lane) live &= ~(1u << (bi >> 5));
|
||||
if (lane == i) { my_w = bv; my_id = bi; }
|
||||
nsel = i + 1;
|
||||
}
|
||||
if (nsel == k) {
|
||||
double s = 0.0;
|
||||
for (int i = 0; i < k; ++i) s += (double) __shfl_sync(0xffffffffu, my_w, i);
|
||||
const double sc = fmax(s, 6.103515625e-05); // 2**-14
|
||||
if (lane < k) {
|
||||
ids[(size_t) t * k + lane] = my_id;
|
||||
weights[(size_t) t * k + lane] = (float) ((double) my_w / sc);
|
||||
}
|
||||
} else { // router_top10_kernel's tail exactly: unwritten ranks keep what the buffer held and are renormalised too
|
||||
if (lane < nsel) { ids[(size_t) t * k + lane] = my_id; weights[(size_t) t * k + lane] = my_w; }
|
||||
__threadfence();
|
||||
if (lane == 0) {
|
||||
double s = 0.0;
|
||||
for (int i = 0; i < k; ++i) s += (double) weights[(size_t) t * k + i];
|
||||
const double sc = fmax(s, 6.103515625e-05);
|
||||
for (int i = 0; i < k; ++i)
|
||||
weights[(size_t) t * k + i] = (float) ((double) weights[(size_t) t * k + i] / sc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void launch_generic(const float* logits, int n_tokens, int n_expert, int k, int* ids, float* weights, void* stream,
|
||||
int mode) { // mode 0: router_top10_kernel, 1: the HIP fast kernel, 2: the fast kernel's serial path
|
||||
int threads = n_expert < RT_MAX_THREADS ? n_expert : RT_MAX_THREADS;
|
||||
threads = (threads + 31) & ~31;
|
||||
const size_t taken_bytes = ((size_t) n_expert + 15u) & ~(size_t) 15u;
|
||||
const size_t smem = taken_bytes + (size_t) n_expert * sizeof(double) + (size_t) n_expert * sizeof(float);
|
||||
if (mode == 1) {
|
||||
router_top10_fast_kernel<false><<<(unsigned) n_tokens, threads, smem, (cudaStream_t) stream>>>(
|
||||
logits, n_tokens, n_expert, k, ids, weights);
|
||||
return;
|
||||
}
|
||||
if (mode == 2) {
|
||||
router_top10_fast_kernel<true><<<(unsigned) n_tokens, threads, smem, (cudaStream_t) stream>>>(
|
||||
logits, n_tokens, n_expert, k, ids, weights);
|
||||
return;
|
||||
}
|
||||
router_top10_kernel<<<(unsigned) n_tokens, threads, smem, (cudaStream_t) stream>>>(
|
||||
logits, n_tokens, n_expert, k, ids, weights);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
bool router_top10_variant(const float* logits, int n_tokens, int n_expert, int k, int* ids, float* weights,
|
||||
void* stream, int variant) {
|
||||
#if defined(__HIPCC__)
|
||||
if (n_tokens <= 0 || n_expert <= 64 || n_expert > 512 || k <= 0 || k > 32) return false;
|
||||
if (variant < 0 || variant > 2) return false;
|
||||
launch_generic(logits, n_tokens, n_expert, k, ids, weights, stream, variant);
|
||||
return cudaGetLastError() == cudaSuccess;
|
||||
#else
|
||||
(void) logits; (void) n_tokens; (void) n_expert; (void) k; (void) ids; (void) weights; (void) stream;
|
||||
(void) variant;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void router_top10(const float* logits, int n_tokens, int n_expert, int k, int* ids, float* weights,
|
||||
void* stream) {
|
||||
#if defined(__HIPCC__)
|
||||
{
|
||||
static const bool old = std::getenv("STRATA_HIP_ROUTER_OLD") != nullptr;
|
||||
if (!old && n_tokens > 0 && n_expert > 64 && n_expert <= 512 && k > 0 && k <= 32) {
|
||||
launch_generic(logits, n_tokens, n_expert, k, ids, weights, stream, 1);
|
||||
const cudaError_t e = cudaGetLastError();
|
||||
if (e != cudaSuccess) {
|
||||
std::fprintf(stderr, "router_top10 launch: %s\n", cudaGetErrorString(e));
|
||||
std::exit(1);
|
||||
}
|
||||
if (stream == nullptr) {
|
||||
const cudaError_t s = cudaDeviceSynchronize();
|
||||
if (s != cudaSuccess) {
|
||||
std::fprintf(stderr, "router_top10: %s\n", cudaGetErrorString(s));
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (n_tokens <= 0 || n_expert <= 0 || k <= 0) return;
|
||||
if (k > 64) {
|
||||
std::fprintf(stderr, "router_top10: k %d exceeds the kernel's 64\n", k);
|
||||
|
||||
+151
-3
@@ -10,7 +10,8 @@
|
||||
// produces a valid token, so only a comparison at the distribution level can tell them apart; the parity test
|
||||
// does that against an independently computed distribution.
|
||||
//
|
||||
// `sampler_greedy_kernel` is the plain argmax, one block per token over the vocabulary. The sampled chain has
|
||||
// `sampler_greedy_kernel` is the plain argmax, one block per token over the vocabulary (on sm_90+ without penalties,
|
||||
// `sampler_greedy_cluster_kernel`: the same token from a cluster of 8 CTAs per row). The sampled chain has
|
||||
// three implementations that pick the same token, bit for bit:
|
||||
// - the SPLIT top_k (default): `sampler_split_part_kernel` cuts each row into 4,096-logit blocks over the whole
|
||||
// GPU, each keeps its own top_k, and `sampler_split_merge_kernel` merges those lists and runs the tail;
|
||||
@@ -20,6 +21,7 @@
|
||||
// The two new ones share `sampled_tail_warp` (top_p / min_p / temperature / draw on one warp).
|
||||
#include "strata/kernels/sampler.hpp"
|
||||
#include "strata/core/coupled_draft.hpp"
|
||||
#include "strata/core/emulate.hpp"
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
@@ -166,6 +168,96 @@ __global__ void sampler_greedy_kernel(const float* __restrict__ logits, int n_vo
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(__HIPCC__)
|
||||
/// **THE SAME ARGMAX ON A THREAD-BLOCK CLUSTER (sm_90+, S19), FOR THE CALLS WITHOUT PENALTIES.** One block per
|
||||
/// token reads its 1 MB row (248,320 logits) on one SM with one load in flight per thread: latency-bound, 40 us per
|
||||
/// call on an RTX 5070, for every verify window and every draft step. Here a cluster of `kAmCtas` CTAs shares the
|
||||
/// row, each thread keeps four loads in flight, and the CTAs' results meet in CTA 0's shared memory (distributed
|
||||
/// shared memory): 6 us (decode_cluster_parity --bench).
|
||||
///
|
||||
/// The answer is a function of the row alone, so it is the one-block kernel's bit for bit: the LOWEST index whose value
|
||||
/// is the largest non-NaN value above -inf (each thread walks its elements in ascending order with a strict `>`, and
|
||||
/// every merge takes the larger value or, on equality, the smaller index - an order-free rule), and 0 when there is
|
||||
/// none (all -inf / NaN), as there. NaN never wins a `>`.
|
||||
///
|
||||
/// Barriers: a relaxed cluster arrive at entry, waited before the remote store (CTA 0 must be running); each CTA's
|
||||
/// result goes to slot `rank` of CTA 0, released by the next arrive; CTAs 1.. then exit (a cluster wait counts the
|
||||
/// threads that have not exited) and CTA 0 waits, reads its own slots, and writes the token.
|
||||
constexpr int kAmCtas = 8; // CTAs per token (the portable cluster size)
|
||||
constexpr int kAmThreads = 1024;
|
||||
#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 900
|
||||
#define STRATA_AM_CLUSTER 1
|
||||
#else
|
||||
#define STRATA_AM_CLUSTER 0 // older targets: a trap, never launched (sample_greedy_cluster checks)
|
||||
#endif
|
||||
#if STRATA_AM_CLUSTER
|
||||
__device__ __forceinline__ void am_take(float s, int v, float& bv, int& best) {
|
||||
if (s > bv) { bv = s; best = v; }
|
||||
}
|
||||
__device__ __forceinline__ void am_merge(float ov, int oi, float& bv, int& best) {
|
||||
if (ov > bv || (ov == bv && oi < best)) { bv = ov; best = oi; }
|
||||
}
|
||||
#endif
|
||||
// grid (kAmCtas, n_tokens), cluster (kAmCtas, 1, 1), kAmThreads threads
|
||||
__global__ void __launch_bounds__(kAmThreads) sampler_greedy_cluster_kernel(const float* __restrict__ logits,
|
||||
int n_vocab, int* __restrict__ out) {
|
||||
#if STRATA_AM_CLUSTER
|
||||
__shared__ float sv[32];
|
||||
__shared__ int si[32];
|
||||
__shared__ float cv[kAmCtas]; // on CTA 0: each CTA's result
|
||||
__shared__ int ci[kAmCtas];
|
||||
unsigned rank;
|
||||
asm volatile("mov.u32 %0, %%cluster_ctarank;\n" : "=r"(rank));
|
||||
asm volatile("barrier.cluster.arrive.relaxed.aligned;\n" ::: "memory");
|
||||
const float* l = logits + (size_t) blockIdx.y * n_vocab;
|
||||
float bv = __int_as_float(0xff800000); // -inf; `n_vocab` is "no candidate", as in sampler_greedy_kernel
|
||||
int best = n_vocab;
|
||||
constexpr int S = kAmCtas * kAmThreads;
|
||||
int v = (int) rank * kAmThreads + (int) threadIdx.x;
|
||||
for (; v + 3 * S < n_vocab; v += 4 * S) { // four independent loads, then taken in ascending order
|
||||
const float x0 = l[v], x1 = l[v + S], x2 = l[v + 2 * S], x3 = l[v + 3 * S];
|
||||
am_take(x0, v, bv, best);
|
||||
am_take(x1, v + S, bv, best);
|
||||
am_take(x2, v + 2 * S, bv, best);
|
||||
am_take(x3, v + 3 * S, bv, best);
|
||||
}
|
||||
for (; v < n_vocab; v += S) am_take(l[v], v, bv, best);
|
||||
for (int off = 16; off > 0; off >>= 1)
|
||||
am_merge(__shfl_down_sync(0xFFFFFFFFu, bv, off), __shfl_down_sync(0xFFFFFFFFu, best, off), bv, best);
|
||||
const int warp = (int) (threadIdx.x >> 5), lane = (int) (threadIdx.x & 31);
|
||||
if (lane == 0) { sv[warp] = bv; si[warp] = best; }
|
||||
__syncthreads();
|
||||
if (warp == 0) {
|
||||
bv = sv[lane];
|
||||
best = si[lane];
|
||||
for (int off = 16; off > 0; off >>= 1)
|
||||
am_merge(__shfl_down_sync(0xFFFFFFFFu, bv, off), __shfl_down_sync(0xFFFFFFFFu, best, off), bv, best);
|
||||
}
|
||||
asm volatile("barrier.cluster.wait.acquire.aligned;\n" ::: "memory"); // CTA 0 runs
|
||||
if (threadIdx.x == 0) {
|
||||
uint64_t a;
|
||||
asm volatile("mapa.u64 %0, %1, %2;\n" : "=l"(a) : "l"((uint64_t) &cv[rank]), "r"(0u));
|
||||
*reinterpret_cast<float*>(a) = bv;
|
||||
asm volatile("mapa.u64 %0, %1, %2;\n" : "=l"(a) : "l"((uint64_t) &ci[rank]), "r"(0u));
|
||||
*reinterpret_cast<int*>(a) = best;
|
||||
}
|
||||
asm volatile("barrier.cluster.arrive.release.aligned;\n" ::: "memory");
|
||||
if (rank != 0) return;
|
||||
asm volatile("barrier.cluster.wait.acquire.aligned;\n" ::: "memory");
|
||||
if (warp == 0) {
|
||||
bv = lane < kAmCtas ? cv[lane] : __int_as_float(0xff800000);
|
||||
best = lane < kAmCtas ? ci[lane] : n_vocab;
|
||||
for (int off = 16; off > 0; off >>= 1)
|
||||
am_merge(__shfl_down_sync(0xFFFFFFFFu, bv, off), __shfl_down_sync(0xFFFFFFFFu, best, off), bv, best);
|
||||
if (lane == 0) out[blockIdx.y] = (best < n_vocab) ? best : 0;
|
||||
}
|
||||
#else
|
||||
(void) logits; (void) n_vocab; (void) out;
|
||||
__trap();
|
||||
#endif
|
||||
}
|
||||
#endif // !__HIPCC__
|
||||
|
||||
/// **THE SAMPLED PATH, ONE BLOCK PER TOKEN.** The kernel below replaced a version that ran the whole chain
|
||||
/// in ONE THREAD per token (`<<<ceil(T/64), 64>>>`, so a 4-token window fielded four threads): `top_k` alone
|
||||
/// was `k` sequential scans of the vocabulary with an inner sweep over the already-taken list - 20 x 248,320
|
||||
@@ -833,6 +925,55 @@ int2* split_scratch(void* stream, size_t entries) {
|
||||
|
||||
} // namespace
|
||||
|
||||
bool sample_greedy_cluster(const float* logits, int n_tokens, int n_vocab, int* out, void* stream) {
|
||||
#if defined(__HIPCC__)
|
||||
(void) logits; (void) n_tokens; (void) n_vocab; (void) out; (void) stream;
|
||||
return false;
|
||||
#else
|
||||
if (n_tokens <= 0 || n_vocab <= 0) return true;
|
||||
if (n_tokens > 65535) return false;
|
||||
// Per device (a layer split runs on several): 1 the cluster kernel runs here, 2 it does not - sm_90+ (the card's,
|
||||
// or STRATA_EMULATE_CC's), code built for it (an older build's PTX holds a trap: the PTX version says which), and
|
||||
// room for one cluster of kAmCtas CTAs.
|
||||
static int ok[64] = {};
|
||||
int dev = 0;
|
||||
if (cudaGetDevice(&dev) != cudaSuccess || dev < 0 || dev >= 64) { cudaGetLastError(); return false; }
|
||||
cudaLaunchConfig_t cfg{};
|
||||
cudaLaunchAttribute at[1];
|
||||
at[0].id = cudaLaunchAttributeClusterDimension;
|
||||
at[0].val.clusterDim.x = kAmCtas;
|
||||
at[0].val.clusterDim.y = 1;
|
||||
at[0].val.clusterDim.z = 1;
|
||||
cfg.gridDim = dim3(kAmCtas, 1, 1);
|
||||
cfg.blockDim = dim3(kAmThreads, 1, 1);
|
||||
cfg.dynamicSmemBytes = 0;
|
||||
cfg.stream = (cudaStream_t) stream;
|
||||
cfg.attrs = at;
|
||||
cfg.numAttrs = 1;
|
||||
if (ok[dev] == 0) {
|
||||
int major = 0, clusters = 0;
|
||||
cudaFuncAttributes fa{};
|
||||
const bool runs = cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev) == cudaSuccess &&
|
||||
strata::cc_major_of(major) >= 9 &&
|
||||
cudaFuncGetAttributes(&fa, sampler_greedy_cluster_kernel) == cudaSuccess &&
|
||||
fa.ptxVersion >= 90 && fa.binaryVersion >= 90 &&
|
||||
cudaOccupancyMaxActiveClusters(&clusters, sampler_greedy_cluster_kernel, &cfg) ==
|
||||
cudaSuccess &&
|
||||
clusters >= 1;
|
||||
cudaGetLastError();
|
||||
ok[dev] = runs ? 1 : 2;
|
||||
}
|
||||
if (ok[dev] != 1) return false;
|
||||
cfg.gridDim = dim3(kAmCtas, (unsigned) n_tokens, 1);
|
||||
const cudaError_t e = cudaLaunchKernelEx(&cfg, sampler_greedy_cluster_kernel, logits, n_vocab, out);
|
||||
if (e != cudaSuccess) {
|
||||
std::fprintf(stderr, "sample_tokens cluster launch: %s\n", cudaGetErrorString(e));
|
||||
std::exit(1);
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void sample_tokens(const float* logits, int n_tokens, int n_vocab, const int* history, int history_len,
|
||||
const SamplerParams& p, int* out, void* stream) {
|
||||
if (n_tokens <= 0 || n_vocab <= 0) return;
|
||||
@@ -845,10 +986,17 @@ void sample_tokens(const float* logits, int n_tokens, int n_vocab, const int* hi
|
||||
? (unsigned) ((n_vocab + 31) / 32) * sizeof(unsigned) // the penalty bitmap
|
||||
: 0;
|
||||
if (p.greedy || p.temperature <= 0.0f) {
|
||||
// Without penalties (shmem == 0: no window) on sm_90+, a cluster of CTAs per token - the same token; see
|
||||
// `sampler_greedy_cluster_kernel`. STRATA_ARGMAX_MULTI=0: always the one-block kernel.
|
||||
static const bool multi = [] {
|
||||
const char* v = std::getenv("STRATA_ARGMAX_MULTI");
|
||||
return !v || std::atoi(v) != 0;
|
||||
}();
|
||||
// One block per token, 1,024 threads over the vocabulary. See `sampler_greedy_kernel`.
|
||||
const int gthreads = 1024;
|
||||
sampler_greedy_kernel<<<(unsigned) n_tokens, gthreads, shmem, (cudaStream_t) stream>>>(
|
||||
logits, n_vocab, history, history_len, p, p.penalty_last_n, p.penalty_last_n, out);
|
||||
if (!(multi && shmem == 0 && sample_greedy_cluster(logits, n_tokens, n_vocab, out, stream)))
|
||||
sampler_greedy_kernel<<<(unsigned) n_tokens, gthreads, shmem, (cudaStream_t) stream>>>(
|
||||
logits, n_vocab, history, history_len, p, p.penalty_last_n, p.penalty_last_n, out);
|
||||
} else if (sampled_path() == SampledPath::Old) {
|
||||
// The same block-per-token shape: the selection's k argmax rounds reduce inside the block. See
|
||||
// `sampler_kernel`'s header for what the old one-thread-per-token launch cost.
|
||||
|
||||
@@ -546,7 +546,7 @@ void copy_indexed(float* dst, const float* src, int64_t stride, const int32_t* i
|
||||
namespace { __global__ void gpu_stamp_kernel(unsigned long long* buf, int i) {
|
||||
unsigned long long t;
|
||||
#if defined(__HIPCC__)
|
||||
t = wall_clock64() * 10ull; // gfx11 / gfx12: a constant 100 MHz counter, in ns
|
||||
t = wall_clock64() * 10ull; // gfx10.3 / gfx11 / gfx12: a constant 100 MHz counter, in ns
|
||||
#else
|
||||
asm volatile("mov.u64 %0, %%globaltimer;" : "=l"(t));
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
// src/kernels/decode_cluster_parity.cpp - S19: the decode kernels on thread-block clusters (sm_90+) against the ones
|
||||
// they replace, bit for bit (GPU, synthetic, no model).
|
||||
//
|
||||
// QSA top-k : qsa_block_topk_cluster vs qsa_block_topk's one-CTA dispatch (the register kernel up to 33,792 blocks,
|
||||
// block_topk_kernel above) and vs qsa_block_topk_ref - every id of every row of the `cap`-wide buffer,
|
||||
// including the entries past the width that neither may write.
|
||||
// argmax : sample_greedy_cluster vs sample_tokens' one-block greedy kernel.
|
||||
//
|
||||
// Inputs: random scores like a real indexer's, and adversarial ones - all equal, a few distinct values (ties at the
|
||||
// threshold across the CTAs of a cluster), NaN, +-inf, -0.0 against +0.0, denormals, monotone rows, all NaN; contexts
|
||||
// from below the selection width (the identity) to 262,144 cells, every tail weight (n_kv mod 4), 1-16 queries; and
|
||||
// one CUDA graph that captures both cluster launches, replayed with new inputs.
|
||||
//
|
||||
// decode_cluster_parity --selftest parity (exit 0 = identical everywhere); skipped (exit 0) below sm_90
|
||||
// decode_cluster_parity --bench per-call times, old vs new, at 32K / 128K / 262K
|
||||
//
|
||||
// The old paths are reached through the public dispatchers with STRATA_QSA_CLUSTER=0 / STRATA_ARGMAX_MULTI=0, set
|
||||
// here before their first call (they read the variables once).
|
||||
#include "strata/kernels/qsa.hpp"
|
||||
#include "strata/kernels/qsa_select.hpp"
|
||||
#include "strata/kernels/sampler.hpp"
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace k = strata::kernels;
|
||||
|
||||
namespace {
|
||||
|
||||
void ck(cudaError_t e, const char* what) {
|
||||
if (e != cudaSuccess) {
|
||||
std::fprintf(stderr, "%s: %s\n", what, cudaGetErrorString(e));
|
||||
std::exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
void set_env(const char* name, const char* value) {
|
||||
#if defined(_WIN32)
|
||||
_putenv_s(name, value);
|
||||
#else
|
||||
setenv(name, value, 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
template <typename T> struct Dev {
|
||||
T* p = nullptr;
|
||||
size_t n = 0;
|
||||
explicit Dev(size_t count) : n(count) { ck(cudaMalloc(&p, count * sizeof(T) + 64), "cudaMalloc"); }
|
||||
~Dev() { cudaFree(p); }
|
||||
void put(const std::vector<T>& h) {
|
||||
ck(cudaMemcpy(p, h.data(), h.size() * sizeof(T), cudaMemcpyHostToDevice), "up");
|
||||
}
|
||||
std::vector<T> get() const {
|
||||
std::vector<T> h(n);
|
||||
ck(cudaMemcpy(h.data(), p, n * sizeof(T), cudaMemcpyDeviceToHost), "down");
|
||||
return h;
|
||||
}
|
||||
};
|
||||
|
||||
const float kNaN = std::numeric_limits<float>::quiet_NaN(), kInf = std::numeric_limits<float>::infinity();
|
||||
|
||||
// ---- QSA top-k fixtures
|
||||
enum Pattern { kRandom, kAllEqual, kFewValues, kNaNs, kInfs, kSignedZero, kDenormal, kAscending, kDescending, kAllNaN,
|
||||
kQuantized, kPatterns };
|
||||
const char* pattern_name(int p) {
|
||||
static const char* n[] = {"random", "all-equal", "few-values", "nan", "inf", "signed-zero", "denormal", "ascending",
|
||||
"descending", "all-nan", "quantized"};
|
||||
return n[p];
|
||||
}
|
||||
|
||||
// one query's row of block scores (blocks 0..n_bid); the tail block gets the +1e9 the score kernel gives it
|
||||
void fill_row(float* row, int64_t n_kv, int64_t n_bid, int pattern, std::mt19937& rng) {
|
||||
std::normal_distribution<float> nd(0.f, 1.f);
|
||||
std::uniform_int_distribution<int> pick(0, 99);
|
||||
for (int64_t b = 0; b <= n_bid; ++b) {
|
||||
float v = 0.f;
|
||||
const float g = nd(rng);
|
||||
switch (pattern) {
|
||||
case kRandom: v = std::fmax(0.f, g) * 3.f + std::fmax(0.f, nd(rng)); break; // relu sums: exact zeros too
|
||||
case kAllEqual: v = 1.f; break;
|
||||
case kFewValues: v = (float) (pick(rng) % 4) * 0.5f; break;
|
||||
case kNaNs: v = pick(rng) < 15 ? kNaN : std::fabs(g); break;
|
||||
case kInfs: {
|
||||
const int r = pick(rng);
|
||||
v = r < 5 ? kInf : r < 15 ? -kInf : r < 30 ? -std::fabs(g) : g;
|
||||
break;
|
||||
}
|
||||
case kSignedZero: v = pick(rng) < 50 ? -0.0f : (pick(rng) < 50 ? 0.0f : -std::fabs(g) * 1e-3f); break;
|
||||
case kDenormal: v = (float) (pick(rng) % 7) * 1e-41f; break;
|
||||
case kAscending: v = (float) b; break;
|
||||
case kDescending: v = (float) (n_bid - b); break;
|
||||
case kAllNaN: v = kNaN; break;
|
||||
case kQuantized: v = std::round(std::fabs(g) * 8.f) / 8.f; break;
|
||||
}
|
||||
row[b] = v;
|
||||
}
|
||||
if (n_kv % 4 != 0 && pattern != kAllNaN && pattern != kAllEqual) row[n_bid] += 1e9f;
|
||||
}
|
||||
|
||||
struct TopkCase {
|
||||
int64_t ctx; // the last query's n_kv
|
||||
int64_t capacity; // --max-context: max_blocks = capacity / 4 + 2 (the engine's rule)
|
||||
int nq;
|
||||
int pattern;
|
||||
};
|
||||
|
||||
int run_topk_case(const TopkCase& c, std::mt19937& rng, bool verbose) {
|
||||
const k::QsaShapes s = k::qsa_real_shapes();
|
||||
const int64_t max_blocks = c.capacity / 4 + 2, cap = k::qsa_selection_width(k::kTopkMaxCells, s);
|
||||
std::vector<float> sc((size_t) (c.nq * max_blocks), 0.f);
|
||||
std::vector<int32_t> steps((size_t) c.nq * k::kStepCount);
|
||||
for (int i = 0; i < c.nq; ++i) {
|
||||
const int64_t n_kv = c.ctx - (c.nq - 1 - i); // the window's queries, one cell apart, the last at ctx
|
||||
int32_t* st = steps.data() + (size_t) i * k::kStepCount;
|
||||
st[k::kStepPos] = (int32_t) (n_kv - 1);
|
||||
st[k::kStepNKv] = (int32_t) n_kv;
|
||||
st[k::kStepNBid] = (int32_t) (n_kv / s.idx_block);
|
||||
st[k::kStepWidth] = (int32_t) k::qsa_selection_width(n_kv, s);
|
||||
fill_row(sc.data() + (size_t) i * max_blocks, n_kv, n_kv / s.idx_block, c.pattern, rng);
|
||||
}
|
||||
Dev<float> d_sc(sc.size());
|
||||
const size_t n_ids = (size_t) (c.nq * cap);
|
||||
Dev<int32_t> d_st(steps.size()), d_new(n_ids), d_old(n_ids), d_ref(n_ids);
|
||||
d_sc.put(sc);
|
||||
d_st.put(steps);
|
||||
ck(cudaMemset(d_new.p, 0xA5, d_new.n * 4), "memset");
|
||||
ck(cudaMemset(d_old.p, 0xA5, d_old.n * 4), "memset");
|
||||
ck(cudaMemset(d_ref.p, 0xA5, d_ref.n * 4), "memset");
|
||||
if (!k::qsa_block_topk_cluster(d_sc.p, d_st.p, c.nq, max_blocks, cap, s, d_new.p, nullptr)) {
|
||||
std::printf("FAIL topk ctx %lld cap %lld nq %d: the cluster kernel did not run\n", (long long) c.ctx,
|
||||
(long long) c.capacity, c.nq);
|
||||
return 1;
|
||||
}
|
||||
k::qsa_block_topk(d_sc.p, d_st.p, c.nq, max_blocks, cap, s, d_old.p, nullptr);
|
||||
k::qsa_block_topk_ref(d_sc.p, d_st.p, c.nq, max_blocks, cap, s, d_ref.p, nullptr);
|
||||
ck(cudaDeviceSynchronize(), "topk sync");
|
||||
const std::vector<int32_t> a = d_new.get(), b = d_old.get(), r = d_ref.get();
|
||||
int64_t diff_old = 0, diff_ref = 0, first = -1;
|
||||
for (size_t i = 0; i < a.size(); ++i) {
|
||||
if (a[i] != b[i]) { ++diff_old; if (first < 0) first = (int64_t) i; }
|
||||
if (a[i] != r[i]) ++diff_ref;
|
||||
}
|
||||
const bool ok = diff_old == 0 && diff_ref == 0;
|
||||
if (!ok || verbose)
|
||||
std::printf("%s topk ctx %7lld cap %7lld nq %2d %-11s: %lld ids differ from the one-CTA dispatch, %lld from "
|
||||
"the reference%s\n", ok ? "ok " : "FAIL", (long long) c.ctx, (long long) c.capacity, c.nq,
|
||||
pattern_name(c.pattern), (long long) diff_old, (long long) diff_ref,
|
||||
first >= 0 ? (" (first at " + std::to_string(first) + ")").c_str() : "");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
// ---- argmax fixtures
|
||||
enum AmPattern { aRandom, aTies, aAllEqual, aAllNegInf, aAllNaN, aNaNMax, aPosInf, aSignedZero, aLast, aFirst,
|
||||
aDenormal, aTiesAcross, aPatterns };
|
||||
const char* am_name(int p) {
|
||||
static const char* n[] = {"random", "ties", "all-equal", "all--inf", "all-nan", "nan+max", "+inf", "signed-zero",
|
||||
"max-last", "max-first", "denormal", "ties-across"};
|
||||
return n[p];
|
||||
}
|
||||
|
||||
void fill_logits(float* l, int nv, int pattern, std::mt19937& rng) {
|
||||
std::normal_distribution<float> nd(0.f, 3.f);
|
||||
std::uniform_int_distribution<int> at(0, nv - 1);
|
||||
for (int v = 0; v < nv; ++v) l[v] = nd(rng);
|
||||
switch (pattern) {
|
||||
case aRandom: break;
|
||||
case aTies: { const int n = 1 + at(rng) % 9; for (int i = 0; i < n; ++i) l[at(rng)] = 100.f; break; }
|
||||
case aAllEqual: for (int v = 0; v < nv; ++v) l[v] = 0.25f; break;
|
||||
case aAllNegInf: for (int v = 0; v < nv; ++v) l[v] = -kInf; break;
|
||||
case aAllNaN: for (int v = 0; v < nv; ++v) l[v] = kNaN; break;
|
||||
case aNaNMax:
|
||||
for (int v = 0; v < nv; ++v) if (at(rng) % 3 == 0) l[v] = kNaN;
|
||||
l[at(rng)] = 50.f;
|
||||
l[at(rng)] = 50.f;
|
||||
break;
|
||||
case aPosInf: for (int i = 0; i < 4; ++i) l[at(rng)] = kInf; break;
|
||||
case aSignedZero:
|
||||
for (int v = 0; v < nv; ++v) l[v] = -std::fabs(l[v]) - 1.f;
|
||||
for (int i = 0; i < 6; ++i) l[at(rng)] = (i & 1) ? 0.0f : -0.0f;
|
||||
break;
|
||||
case aLast: l[nv - 1] = 1e30f; break;
|
||||
case aFirst: l[0] = 1e30f; if (nv > 1) l[nv - 1] = 1e30f; break;
|
||||
case aDenormal: for (int v = 0; v < nv; ++v) l[v] = (float) (at(rng) % 5) * 1e-42f - 1e-30f * (v & 1); break;
|
||||
case aTiesAcross: // the same maximum in several CTAs' and threads' ranges (8 x 1024 stride), plus -inf, NaN
|
||||
for (int v = 0; v < nv; v += 1 + at(rng) % 9000) l[v] = 7.f;
|
||||
for (int v = 3; v < nv; v += 11) l[v] = kNaN;
|
||||
for (int v = 5; v < nv; v += 13) l[v] = -kInf;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int run_argmax_case(int nv, int T, int pattern, std::mt19937& rng, bool verbose) {
|
||||
std::vector<float> l((size_t) nv * T);
|
||||
for (int t = 0; t < T; ++t) fill_logits(l.data() + (size_t) t * nv, nv, t % 2 == 0 ? pattern : aRandom, rng);
|
||||
Dev<float> d_l(l.size());
|
||||
Dev<int> d_new((size_t) T), d_old((size_t) T);
|
||||
d_l.put(l);
|
||||
ck(cudaMemset(d_new.p, 0xA5, (size_t) T * 4), "memset");
|
||||
ck(cudaMemset(d_old.p, 0x5A, (size_t) T * 4), "memset");
|
||||
k::SamplerParams sp;
|
||||
sp.greedy = true;
|
||||
sp.temperature = 0.f;
|
||||
if (!k::sample_greedy_cluster(d_l.p, T, nv, d_new.p, nullptr)) {
|
||||
std::printf("FAIL argmax nv %d T %d: the cluster kernel did not run\n", nv, T);
|
||||
return 1;
|
||||
}
|
||||
k::sample_tokens(d_l.p, T, nv, nullptr, 0, sp, d_old.p, nullptr);
|
||||
ck(cudaDeviceSynchronize(), "argmax sync");
|
||||
const std::vector<int> a = d_new.get(), b = d_old.get();
|
||||
// and an independent host answer: the lowest index of the largest non-NaN value above -inf, else 0
|
||||
int bad = 0;
|
||||
for (int t = 0; t < T; ++t) {
|
||||
const float* r = l.data() + (size_t) t * nv;
|
||||
int hb = -1;
|
||||
for (int v = 0; v < nv; ++v)
|
||||
if (r[v] > -kInf && (hb < 0 || r[v] > r[hb])) hb = v;
|
||||
if (hb < 0) hb = 0;
|
||||
if (a[(size_t) t] != b[(size_t) t] || a[(size_t) t] != hb) ++bad;
|
||||
}
|
||||
if (bad || verbose)
|
||||
std::printf("%s argmax nv %6d T %d %-11s: %d rows differ (new %d, old %d)\n", bad ? "FAIL" : "ok ", nv, T,
|
||||
am_name(pattern), bad, a[0], b[0]);
|
||||
return bad ? 1 : 0;
|
||||
}
|
||||
|
||||
// both cluster launches captured in one graph, replayed over new inputs written into the same buffers
|
||||
int run_graph_case(std::mt19937& rng) {
|
||||
const k::QsaShapes s = k::qsa_real_shapes();
|
||||
const int nq = 5, nv = 248320;
|
||||
const int64_t capacity = 131072 + 272, max_blocks = capacity / 4 + 2;
|
||||
const int64_t cap = k::qsa_selection_width(k::kTopkMaxCells, s);
|
||||
Dev<float> d_sc((size_t) (nq * max_blocks)), d_l((size_t) nv * nq);
|
||||
Dev<int32_t> d_st((size_t) nq * k::kStepCount), d_new((size_t) (nq * cap)), d_old((size_t) (nq * cap));
|
||||
Dev<int> a_new((size_t) nq), a_old((size_t) nq);
|
||||
ck(cudaMemset(d_new.p, 0, d_new.n * 4), "memset"); // the entries past a row's width: written by neither
|
||||
ck(cudaMemset(d_old.p, 0, d_old.n * 4), "memset");
|
||||
cudaStream_t cs;
|
||||
ck(cudaStreamCreateWithFlags(&cs, cudaStreamNonBlocking), "stream");
|
||||
cudaGraph_t g = nullptr;
|
||||
cudaGraphExec_t ge = nullptr;
|
||||
ck(cudaStreamBeginCapture(cs, cudaStreamCaptureModeThreadLocal), "begin capture");
|
||||
const bool l1 = k::qsa_block_topk_cluster(d_sc.p, d_st.p, nq, max_blocks, cap, s, d_new.p, cs);
|
||||
const bool l2 = k::sample_greedy_cluster(d_l.p, nq, nv, a_new.p, cs);
|
||||
ck(cudaStreamEndCapture(cs, &g), "end capture");
|
||||
if (!l1 && !l2) { // no cluster code here (a card or a build below sm_90): nothing to compare
|
||||
if (g) cudaGraphDestroy(g);
|
||||
cudaStreamDestroy(cs);
|
||||
return -1;
|
||||
}
|
||||
if (!l1 || !l2) { std::printf("FAIL graph: one cluster kernel did not run\n"); return 1; }
|
||||
ck(cudaGraphInstantiate(&ge, g, 0), "instantiate");
|
||||
int fails = 0;
|
||||
for (int rep = 0; rep < 6; ++rep) {
|
||||
const int64_t ctx = 100000 + rep * 4001 + rep % 4;
|
||||
std::vector<float> sc((size_t) (nq * max_blocks), 0.f), l((size_t) nv * nq);
|
||||
std::vector<int32_t> steps((size_t) nq * k::kStepCount);
|
||||
for (int i = 0; i < nq; ++i) {
|
||||
const int64_t n_kv = ctx - (nq - 1 - i);
|
||||
int32_t* st = steps.data() + (size_t) i * k::kStepCount;
|
||||
st[k::kStepPos] = (int32_t) (n_kv - 1);
|
||||
st[k::kStepNKv] = (int32_t) n_kv;
|
||||
st[k::kStepNBid] = (int32_t) (n_kv / 4);
|
||||
st[k::kStepWidth] = (int32_t) k::qsa_selection_width(n_kv, s);
|
||||
fill_row(sc.data() + (size_t) i * max_blocks, n_kv, n_kv / 4, rep % 2 ? kFewValues : kRandom, rng);
|
||||
fill_logits(l.data() + (size_t) i * nv, nv, rep % 2 ? aTiesAcross : aRandom, rng);
|
||||
}
|
||||
d_sc.put(sc);
|
||||
d_st.put(steps);
|
||||
d_l.put(l);
|
||||
ck(cudaGraphLaunch(ge, cs), "graph launch");
|
||||
k::qsa_block_topk(d_sc.p, d_st.p, nq, max_blocks, cap, s, d_old.p, cs);
|
||||
k::SamplerParams sp;
|
||||
sp.greedy = true;
|
||||
k::sample_tokens(d_l.p, nq, nv, nullptr, 0, sp, a_old.p, cs);
|
||||
ck(cudaStreamSynchronize(cs), "graph sync");
|
||||
const auto x = d_new.get(), y = d_old.get();
|
||||
const auto u = a_new.get(), v = a_old.get();
|
||||
int64_t d1 = 0, d2 = 0;
|
||||
for (size_t i = 0; i < x.size(); ++i) d1 += x[i] != y[i];
|
||||
for (size_t i = 0; i < u.size(); ++i) d2 += u[i] != v[i];
|
||||
if (d1 || d2) ++fails;
|
||||
std::printf("%s graph replay %d (ctx %lld): %lld ids, %lld tokens differ\n", d1 || d2 ? "FAIL" : "ok ", rep,
|
||||
(long long) ctx, (long long) d1, (long long) d2);
|
||||
}
|
||||
cudaGraphExecDestroy(ge);
|
||||
cudaGraphDestroy(g);
|
||||
cudaStreamDestroy(cs);
|
||||
return fails;
|
||||
}
|
||||
|
||||
// back-to-back launches on `cs` (the engine's are too: one stream, inside a graph)
|
||||
template <typename F> float time_us(cudaStream_t cs, F&& f, int reps = 300) {
|
||||
cudaEvent_t a, b;
|
||||
cudaEventCreate(&a);
|
||||
cudaEventCreate(&b);
|
||||
for (int i = 0; i < 5; ++i) f();
|
||||
cudaEventRecord(a, cs);
|
||||
for (int i = 0; i < reps; ++i) f();
|
||||
cudaEventRecord(b, cs);
|
||||
ck(cudaEventSynchronize(b), "time");
|
||||
float ms = 0.f;
|
||||
cudaEventElapsedTime(&ms, a, b);
|
||||
cudaEventDestroy(a);
|
||||
cudaEventDestroy(b);
|
||||
return ms * 1000.f / (float) reps;
|
||||
}
|
||||
|
||||
int bench() {
|
||||
std::mt19937 rng(11);
|
||||
const k::QsaShapes s = k::qsa_real_shapes();
|
||||
const int64_t cap = k::qsa_selection_width(k::kTopkMaxCells, s);
|
||||
cudaStream_t cs;
|
||||
ck(cudaStreamCreateWithFlags(&cs, cudaStreamNonBlocking), "stream");
|
||||
std::printf("QSA top-k, per call (us); capacity = context + 272 as the speed runs set --max-context\n");
|
||||
for (const int64_t ctx : {32768LL, 131072LL, 262144LL - 272}) {
|
||||
for (const int nq : {1, 5}) {
|
||||
const int64_t capacity = ctx + 272, max_blocks = capacity / 4 + 2;
|
||||
std::vector<float> sc((size_t) (nq * max_blocks), 0.f);
|
||||
std::vector<int32_t> steps((size_t) nq * k::kStepCount);
|
||||
for (int i = 0; i < nq; ++i) {
|
||||
const int64_t n_kv = ctx - (nq - 1 - i);
|
||||
int32_t* st = steps.data() + (size_t) i * k::kStepCount;
|
||||
st[k::kStepPos] = (int32_t) (n_kv - 1);
|
||||
st[k::kStepNKv] = (int32_t) n_kv;
|
||||
st[k::kStepNBid] = (int32_t) (n_kv / 4);
|
||||
st[k::kStepWidth] = (int32_t) k::qsa_selection_width(n_kv, s);
|
||||
fill_row(sc.data() + (size_t) i * max_blocks, n_kv, n_kv / 4, kRandom, rng);
|
||||
}
|
||||
Dev<float> d_sc(sc.size());
|
||||
Dev<int32_t> d_st(steps.size()), d_ids((size_t) (nq * cap));
|
||||
d_sc.put(sc);
|
||||
d_st.put(steps);
|
||||
const float* sp = d_sc.p;
|
||||
const int32_t* st = d_st.p;
|
||||
int32_t* ids = d_ids.p;
|
||||
const float t_old = time_us(cs, [&] { k::qsa_block_topk(sp, st, nq, max_blocks, cap, s, ids, cs); });
|
||||
const float t_ref = time_us(cs, [&] { k::qsa_block_topk_ref(sp, st, nq, max_blocks, cap, s, ids, cs); });
|
||||
const float t_new = time_us(cs, [&] { k::qsa_block_topk_cluster(sp, st, nq, max_blocks, cap, s, ids, cs); });
|
||||
std::printf(" ctx %6lld nq %d: one-CTA dispatch %7.1f (ref %7.1f) cluster %6.1f -> %.1fx\n",
|
||||
(long long) ctx, nq, t_old, t_ref, t_new, t_old / t_new);
|
||||
}
|
||||
}
|
||||
std::printf("argmax, per call (us)\n");
|
||||
for (const int nv : {248320, 32768}) {
|
||||
for (const int T : {1, 5}) {
|
||||
std::vector<float> l((size_t) nv * T);
|
||||
for (int t = 0; t < T; ++t) fill_logits(l.data() + (size_t) t * nv, nv, aRandom, rng);
|
||||
Dev<float> d_l(l.size());
|
||||
Dev<int> d_o((size_t) T);
|
||||
d_l.put(l);
|
||||
k::SamplerParams sp;
|
||||
sp.greedy = true;
|
||||
const float t_old = time_us(cs, [&] { k::sample_tokens(d_l.p, T, nv, nullptr, 0, sp, d_o.p, cs); });
|
||||
const float t_new = time_us(cs, [&] { k::sample_greedy_cluster(d_l.p, T, nv, d_o.p, cs); });
|
||||
std::printf(" n_vocab %6d T %d: one block %6.1f cluster %5.1f -> %.1fx\n", nv, T, t_old, t_new,
|
||||
t_old / t_new);
|
||||
}
|
||||
}
|
||||
cudaStreamDestroy(cs);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
set_env("STRATA_QSA_CLUSTER", "0"); // the dispatchers: the one-CTA kernels (the cluster ones called directly)
|
||||
set_env("STRATA_ARGMAX_MULTI", "0");
|
||||
const bool do_bench = argc > 1 && std::strcmp(argv[1], "--bench") == 0;
|
||||
const bool verbose = argc > 2 && std::strcmp(argv[2], "-v") == 0;
|
||||
int dev = 0, major = 0;
|
||||
ck(cudaGetDevice(&dev), "device");
|
||||
ck(cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev), "cc");
|
||||
std::mt19937 rng(20261002);
|
||||
// first, so that both kernels' first calls (their per-device checks and opt-ins) happen inside a stream capture,
|
||||
// as in the engine's decode graphs
|
||||
const int gfails = run_graph_case(rng);
|
||||
if (gfails < 0) {
|
||||
std::printf("SKIP: no thread-block clusters on this card / build (compute capability %d)\n", major);
|
||||
return 0;
|
||||
}
|
||||
if (do_bench) return bench();
|
||||
int fails = 0, cases = 0;
|
||||
// contexts: below the width (identity), at it, just past it, odd tails, the register kernel's range, past it
|
||||
// (block_topk_kernel), up to 262,144; capacities: tight (the speed runs) and 262,144 (a long --max-context)
|
||||
const int64_t ctxs[] = {7, 2050, 2051, 2052, 2053, 2054, 2055, 3001, 9000, 32768, 32769, 65538, 100003,
|
||||
131072, 135170, 135171, 200001, 262142, 262143, 262144};
|
||||
for (const int64_t ctx : ctxs) {
|
||||
for (const int64_t capacity : {ctx + 272, (int64_t) 262144 + 272}) {
|
||||
if (capacity < ctx) continue;
|
||||
for (int p = 0; p < kPatterns; ++p) {
|
||||
const int nq = (p % 3 == 0) ? 1 : (p % 3 == 1 ? 5 : 16);
|
||||
if (nq > ctx) continue;
|
||||
fails += run_topk_case({ctx, capacity, nq, p}, rng, verbose);
|
||||
++cases;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::printf("QSA top-k: %d cases, %d failed\n", cases, fails);
|
||||
int afails = 0, acases = 0;
|
||||
for (const int nv : {248320, 248321, 248319, 32771, 24576, 8193, 1000, 33, 1}) {
|
||||
for (int p = 0; p < aPatterns; ++p) {
|
||||
for (const int T : {1, 5}) {
|
||||
afails += run_argmax_case(nv, T, p, rng, verbose);
|
||||
++acases;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::printf("argmax: %d cases, %d failed\n", acases, afails);
|
||||
const int total = fails + afails + gfails;
|
||||
std::printf("%s\n", total == 0 ? "PASS: the cluster kernels are bitwise identical" : "FAIL");
|
||||
return total == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
@@ -331,7 +332,22 @@ int fused_multi_lds_parity(const float* d_norm, const uint16_t* d_down, const ui
|
||||
check(cudaMemcpy(s.mixed.data(), d_mixed, s.mixed.size() * sizeof(float), cudaMemcpyDeviceToHost), "multi read mixed");
|
||||
return s;
|
||||
};
|
||||
auto same = [](const Snapshot& a, const Snapshot& b) {
|
||||
// the split read (STRATA_GR_V3=1) sums in another order than the single-token kernel: equal within float
|
||||
// rounding, not to the bit, so it is compared with a relative tolerance; the default kernels bit for bit
|
||||
static const bool v3 = [] { const char* v = std::getenv("STRATA_GR_V3"); return v != nullptr && std::atoi(v) != 0; }();
|
||||
auto close = [](const std::vector<float>& x, const std::vector<float>& y) {
|
||||
double worst = 0.0, mag = 1e-30;
|
||||
for (size_t i = 0; i < x.size(); ++i) {
|
||||
worst = std::max(worst, (double) std::fabs(x[i] - y[i]));
|
||||
mag = std::max(mag, (double) std::fabs(y[i]));
|
||||
}
|
||||
if (worst > 2e-6 * mag)
|
||||
std::printf(" tolerance: worst %.3e of max |ref| %.3e (rel %.3e), n %zu\n", worst, mag, worst / mag, x.size());
|
||||
return worst <= 2e-6 * mag;
|
||||
};
|
||||
auto same = [&](const Snapshot& a, const Snapshot& b) {
|
||||
if (v3) // `lo` is the default kernels' workspace between down and up; the split read keeps it in shared memory
|
||||
return close(a.r_out, b.r_out) && close(a.rs, b.rs) && close(a.inject, b.inject) && close(a.mixed, b.mixed);
|
||||
return std::memcmp(a.r_out.data(), b.r_out.data(), a.r_out.size() * sizeof(float)) == 0 &&
|
||||
std::memcmp(a.lo.data(), b.lo.data(), a.lo.size() * sizeof(float)) == 0 &&
|
||||
std::memcmp(a.rs.data(), b.rs.data(), a.rs.size() * sizeof(float)) == 0 &&
|
||||
|
||||
@@ -467,6 +467,59 @@ int check_q5_1_min(cudaStream_t s) {
|
||||
"(largest per-block sum shift %.3g) %s\n", e_q, e_s, shift, ok ? "ok" : "FAIL");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
// #290: the BF16 token embedding (--embd-gguf) - iq_embed_rows and iq_dequant_f32 on a random BF16 table against
|
||||
// the exact widening (bits << 16), every bit; rows gathered out of order, with repeats
|
||||
int check_bf16_embd(cudaStream_t s) {
|
||||
constexpr int kBf16 = 30;
|
||||
const int64_t H = 2560, V = 61, NT = 97;
|
||||
int failures = 0;
|
||||
if (!strata::kernels::embed_type_supported(kBf16) || strata::kernels::iq_supported(kBf16) ||
|
||||
strata::kernels::iq_row_bytes(kBf16, H) != (size_t) H * 2) {
|
||||
std::printf("bf16 embedding: type support / row bytes wrong\n");
|
||||
return 1;
|
||||
}
|
||||
std::mt19937 rng(290);
|
||||
std::vector<uint16_t> table((size_t) (V * H));
|
||||
for (auto& v : table) { // any bit pattern but NaN (a NaN's payload is not what the test is about)
|
||||
do v = (uint16_t) (rng() & 0xffff); while ((v & 0x7f80) == 0x7f80 && (v & 0x7f));
|
||||
}
|
||||
std::vector<int32_t> tok((size_t) NT);
|
||||
for (auto& t : tok) t = (int32_t) (rng() % V);
|
||||
void* dt = nullptr;
|
||||
int32_t* dtok = nullptr;
|
||||
float* dout = nullptr;
|
||||
cudaMalloc(&dt, table.size() * 2);
|
||||
cudaMalloc((void**) &dtok, tok.size() * 4);
|
||||
const int64_t out_rows = NT > V ? NT : V; // the gathered rows (NT) and the whole table (V) share the buffer
|
||||
cudaMalloc((void**) &dout, (size_t) (out_rows * H) * 4);
|
||||
cudaMemcpy(dt, table.data(), table.size() * 2, cudaMemcpyHostToDevice);
|
||||
cudaMemcpy(dtok, tok.data(), tok.size() * 4, cudaMemcpyHostToDevice);
|
||||
auto widen = [](uint16_t b) { const uint32_t u = (uint32_t) b << 16; float f; std::memcpy(&f, &u, 4); return f; };
|
||||
std::vector<float> got((size_t) (out_rows * H));
|
||||
strata::kernels::iq_embed_rows(kBf16, dt, (size_t) H * 2, dtok, NT, H, dout, s);
|
||||
cudaStreamSynchronize(s);
|
||||
cudaMemcpy(got.data(), dout, (size_t) (NT * H) * 4, cudaMemcpyDeviceToHost);
|
||||
size_t rows_differ = 0;
|
||||
for (int64_t r = 0; r < NT; ++r)
|
||||
for (int64_t d = 0; d < H; ++d) {
|
||||
const float want = widen(table[(size_t) (tok[(size_t) r] * H + d)]);
|
||||
if (std::memcmp(&got[(size_t) (r * H + d)], &want, 4) != 0) { ++rows_differ; break; }
|
||||
}
|
||||
strata::kernels::iq_dequant_f32(kBf16, dt, V * H, dout, s);
|
||||
cudaStreamSynchronize(s);
|
||||
cudaMemcpy(got.data(), dout, table.size() * 4, cudaMemcpyDeviceToHost);
|
||||
size_t values_differ = 0;
|
||||
for (size_t i = 0; i < table.size(); ++i) {
|
||||
const float want = widen(table[i]);
|
||||
values_differ += std::memcmp(&got[i], &want, 4) != 0;
|
||||
}
|
||||
cudaFree(dt); cudaFree(dtok); cudaFree(dout);
|
||||
std::printf("bf16 embedding: %lld gathered rows, %zu differ; dequant of %lld values, %zu differ in any bit\n",
|
||||
(long long) NT, rows_differ, (long long) (V * H), values_differ);
|
||||
if (rows_differ || values_differ) ++failures;
|
||||
return failures;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
@@ -474,7 +527,8 @@ int main(int argc, char** argv) {
|
||||
if (argc < 2) {
|
||||
std::fprintf(stderr, "usage: native_expert_parity <shard.gguf> [layer ...]\n"
|
||||
" native_expert_parity --synthetic GU/DOWN ... (ggml type names, e.g. q4_K/q5_1)\n"
|
||||
" native_expert_parity --q5_1-min\n");
|
||||
" native_expert_parity --q5_1-min\n"
|
||||
" native_expert_parity --bf16-embd\n");
|
||||
return 2;
|
||||
}
|
||||
// #152's width check tests the opt-in rule (the multi-token kernels from one token on)
|
||||
@@ -491,6 +545,8 @@ int main(int argc, char** argv) {
|
||||
const std::string mode = argv[1];
|
||||
if (mode == "--q5_1-min") {
|
||||
failures += check_q5_1_min(s);
|
||||
} else if (mode == "--bf16-embd") {
|
||||
failures += check_bf16_embd(s);
|
||||
} else if (mode == "--synthetic") {
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
const std::string arg = argv[i];
|
||||
|
||||
+88
-23
@@ -3,10 +3,12 @@
|
||||
// See include/strata/kernels/ngram.hpp for the semantics, the rival readings, and the note on MADV_RANDOM.
|
||||
#include "strata/kernels/ngram.hpp"
|
||||
#include "strata/artifact/gguf_reader.hpp"
|
||||
#include "strata/artifact/dequant.hpp"
|
||||
#include "strata/kernels/f16_bits.hpp"
|
||||
#include "strata/ngram/ple_reader.hpp"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#if !defined(_WIN32)
|
||||
@@ -111,11 +113,33 @@ void iq4nl_dequant_row(const uint8_t* row, float* out160) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct Fp8Table {
|
||||
float v[256];
|
||||
Fp8Table() {
|
||||
for (int x = 0; x < 256; ++x) {
|
||||
const int s = x >> 7, e = (x >> 3) & 15, m = x & 7;
|
||||
float f;
|
||||
if (e == 15 && m == 7) f = 0.0f; // NaN in e4m3fn; never in a real table, read as 0
|
||||
else if (e == 0) f = std::ldexp((float) m / 8.0f, -6);
|
||||
else f = std::ldexp(1.0f + (float) m / 8.0f, e - 7);
|
||||
v[x] = s ? -f : f;
|
||||
}
|
||||
}
|
||||
};
|
||||
const Fp8Table kFp8;
|
||||
} // namespace
|
||||
|
||||
void fp8_e4m3_dequant_row(const uint8_t* row, float scale, float* out160) {
|
||||
for (int j = 0; j < PLE_HEAD_DIM; ++j) out160[j] = kFp8.v[row[j]] * scale;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------
|
||||
struct PleTable::Impl {
|
||||
GgufFile* file = nullptr;
|
||||
const uint8_t* data = nullptr;
|
||||
uint64_t n_rows = 0;
|
||||
bool q5_0 = false; // #296: Q5_0 rows (110 B), the mapped reader only
|
||||
mutable uint64_t bytes_read = 0;
|
||||
// Direct mode (plan v0.3 P2): the mapping above is released after the header parse and every row comes
|
||||
// from an unbuffered SSD read into `raw`.
|
||||
@@ -125,7 +149,16 @@ struct PleTable::Impl {
|
||||
bool pending = false;
|
||||
bool locked = false;
|
||||
uint32_t rows[PLE_N_HEADS] = {};
|
||||
uint8_t raw[PLE_N_HEADS * PLE_ROW_BYTES] = {};
|
||||
uint8_t raw[PLE_N_HEADS * PLE_ROW_BYTES_MAX] = {};
|
||||
bool fp8 = false; // F8_E4M3 rows (tools/ple_fp8_pack.py); else IQ4_NL
|
||||
float scale = 1.0f; // the FP8 table's one scale
|
||||
uint32_t rb = PLE_ROW_BYTES; // bytes per row
|
||||
void decode(const uint8_t* row, float* out160) const {
|
||||
if (fp8) fp8_e4m3_dequant_row(row, scale, out160);
|
||||
else if (q5_0)
|
||||
for (int b = 0; b < PLE_HEAD_DIM / 32; ++b) strata::dequantize_q5_0(row + (size_t) b * 22, out160 + b * 32);
|
||||
else iq4nl_dequant_row(row, out160);
|
||||
}
|
||||
};
|
||||
|
||||
PleTable::PleTable() : impl_(new Impl) {}
|
||||
@@ -155,8 +188,31 @@ bool PleTable::open(const std::string& gguf_path, std::string& err, const PleIoO
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
if (std::strcmp(t->type_name(), "IQ4_NL") != 0) {
|
||||
err = std::string("per_layer_token_embd.weight is ") + t->type_name() + ", not IQ4_NL";
|
||||
// IQ4_NL (ISTA-DASLab's shard 2, the original's own GGUF), Q5_0 (#296: OrcaRouter's GGUF), or the FP8 table as
|
||||
// shipped: I8 bytes marked strata.ple.format = f8_e4m3 with strata.ple.scale (tools/ple_fp8_pack.py)
|
||||
impl_->fp8 = false;
|
||||
impl_->q5_0 = std::strcmp(t->type_name(), "Q5_0") == 0;
|
||||
impl_->rb = impl_->q5_0 ? (PLE_HEAD_DIM / 32) * 22 : PLE_ROW_BYTES;
|
||||
if (std::strcmp(t->type_name(), "I8") == 0) {
|
||||
const MetaValue* f = impl_->file->get("strata.ple.format");
|
||||
const MetaValue* s = impl_->file->get("strata.ple.scale");
|
||||
if (f == nullptr || f->s != "f8_e4m3" || s == nullptr || !(s->num() > 0.0)) {
|
||||
err = "per_layer_token_embd.weight is I8 without strata.ple.format = f8_e4m3 and a positive strata.ple.scale";
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
impl_->fp8 = true;
|
||||
impl_->scale = (float) s->num();
|
||||
impl_->rb = PLE_ROW_BYTES_FP8;
|
||||
} else if (!impl_->q5_0 && std::strcmp(t->type_name(), "IQ4_NL") != 0) {
|
||||
err = std::string("per_layer_token_embd.weight is ") + t->type_name() + ", not IQ4_NL, Q5_0 or FP8 (I8)";
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
// The direct reader has fixed 90-byte rows. Keep it for IQ4_NL; Q5_0's
|
||||
// 110-byte rows use the mapped path until that reader supports variable rows.
|
||||
if (impl_->q5_0 && io.mode == PleIo::Direct) {
|
||||
err = "Q5_0 PLE requires --ple-io mmap";
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
@@ -171,7 +227,7 @@ bool PleTable::open(const std::string& gguf_path, std::string& err, const PleIoO
|
||||
// assumed. A wrong data offset would leave a different remainder.
|
||||
// A shard may hold other tensors too (Swift 1.5's shard 1 holds layers 0-12 and the table): the table must
|
||||
// then fit inside the file at its own offset; alone in its shard (the original's shard 2) it fills it exactly.
|
||||
const uint64_t need = impl_->n_rows * (uint64_t) PLE_ROW_BYTES;
|
||||
const uint64_t need = impl_->n_rows * (uint64_t) impl_->rb;
|
||||
const uint64_t have = impl_->file->file_size() - impl_->file->data_start();
|
||||
const bool alone = impl_->file->tensors().size() == 1;
|
||||
if (alone ? need != have : t->offset + need > have) {
|
||||
@@ -179,7 +235,7 @@ bool PleTable::open(const std::string& gguf_path, std::string& err, const PleIoO
|
||||
std::snprintf(buf, sizeof buf,
|
||||
"PLE table size mismatch: %llu rows x %d B = %llu at offset %llu, but the file holds %llu from "
|
||||
"data_start %llu",
|
||||
(unsigned long long) impl_->n_rows, PLE_ROW_BYTES, (unsigned long long) need,
|
||||
(unsigned long long) impl_->n_rows, (int) impl_->rb, (unsigned long long) need,
|
||||
(unsigned long long) t->offset, (unsigned long long) have,
|
||||
(unsigned long long) impl_->file->data_start());
|
||||
err = buf;
|
||||
@@ -194,10 +250,12 @@ bool PleTable::open(const std::string& gguf_path, std::string& err, const PleIoO
|
||||
delete impl_->file;
|
||||
impl_->file = nullptr;
|
||||
impl_->data = nullptr;
|
||||
if (!impl_->reader.open(gguf_path, table_offset, n_rows, io.max_inflight, io.cache_rows, err, io.io_thread)) {
|
||||
if (!impl_->reader.open(gguf_path, table_offset, n_rows, io.max_inflight, io.cache_rows, err, io.io_thread,
|
||||
impl_->rb)) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
impl_->reader.set_keepalive(io.keepalive_ms, io.keepalive_window_s);
|
||||
impl_->n_rows = n_rows;
|
||||
}
|
||||
if (io.mode == PleIo::Mmap && io.lock && impl_->data != nullptr) {
|
||||
@@ -230,33 +288,37 @@ void PleTable::close() {
|
||||
impl_->file = nullptr;
|
||||
impl_->data = nullptr;
|
||||
impl_->n_rows = 0;
|
||||
impl_->rb = PLE_ROW_BYTES;
|
||||
impl_->q5_0 = false;
|
||||
impl_->fp8 = false;
|
||||
}
|
||||
|
||||
bool PleTable::is_open() const { return impl_->data != nullptr || impl_->reader.is_open(); }
|
||||
bool PleTable::locked() const { return impl_->locked; }
|
||||
const char* PleTable::format() const { return impl_->fp8 ? "F8_E4M3" : impl_->q5_0 ? "Q5_0" : "IQ4_NL"; }
|
||||
PleIo PleTable::mode() const { return impl_->mode; }
|
||||
uint64_t PleTable::rows() const { return impl_->n_rows; }
|
||||
uint64_t PleTable::bytes_read() const { return impl_->bytes_read; }
|
||||
|
||||
void PleTable::read_row(uint32_t row, float* out160) const {
|
||||
if (impl_->mode == PleIo::Direct && impl_->reader.is_open()) {
|
||||
uint8_t raw[PLE_ROW_BYTES];
|
||||
uint8_t raw[PLE_ROW_BYTES_MAX];
|
||||
std::string err;
|
||||
const auto t = impl_->reader.issue(&row, 1, raw);
|
||||
if (!impl_->reader.collect(t, err)) {
|
||||
std::memset(out160, 0, (size_t) PLE_HEAD_DIM * sizeof(float));
|
||||
return;
|
||||
}
|
||||
iq4nl_dequant_row(raw, out160);
|
||||
impl_->bytes_read += PLE_ROW_BYTES;
|
||||
impl_->decode(raw, out160);
|
||||
impl_->bytes_read += impl_->rb;
|
||||
return;
|
||||
}
|
||||
if (impl_->data == nullptr || row >= impl_->n_rows) {
|
||||
std::memset(out160, 0, (size_t) PLE_HEAD_DIM * sizeof(float));
|
||||
return;
|
||||
}
|
||||
iq4nl_dequant_row(impl_->data + (size_t) row * PLE_ROW_BYTES, out160);
|
||||
impl_->bytes_read += PLE_ROW_BYTES;
|
||||
impl_->decode(impl_->data + (size_t) row * impl_->rb, out160);
|
||||
impl_->bytes_read += impl_->rb;
|
||||
}
|
||||
|
||||
bool PleTable::issue(const uint32_t* rows16) {
|
||||
@@ -273,8 +335,8 @@ bool PleTable::issue(const uint32_t* rows16) {
|
||||
ULONG_PTR n = 0;
|
||||
for (int h = 0; h < PLE_N_HEADS; ++h) {
|
||||
if (rows16[h] >= impl_->n_rows) continue;
|
||||
ranges[n].VirtualAddress = (PVOID) (impl_->data + (size_t) rows16[h] * PLE_ROW_BYTES);
|
||||
ranges[n].NumberOfBytes = PLE_ROW_BYTES;
|
||||
ranges[n].VirtualAddress = (PVOID) (impl_->data + (size_t) rows16[h] * impl_->rb);
|
||||
ranges[n].NumberOfBytes = impl_->rb;
|
||||
++n;
|
||||
}
|
||||
if (n > 0) (void) PrefetchVirtualMemory(GetCurrentProcess(), n, ranges, 0);
|
||||
@@ -290,8 +352,8 @@ bool PleTable::collect(float* out2560, std::string& err) {
|
||||
if (impl_->mode == PleIo::Direct) {
|
||||
if (!impl_->reader.collect(impl_->ticket, err)) return false;
|
||||
for (int h = 0; h < PLE_N_HEADS; ++h)
|
||||
iq4nl_dequant_row(impl_->raw + (size_t) h * PLE_ROW_BYTES, out2560 + (size_t) h * PLE_HEAD_DIM);
|
||||
impl_->bytes_read += (uint64_t) PLE_N_HEADS * PLE_ROW_BYTES;
|
||||
impl_->decode(impl_->raw + (size_t) h * impl_->rb, out2560 + (size_t) h * PLE_HEAD_DIM);
|
||||
impl_->bytes_read += (uint64_t) PLE_N_HEADS * impl_->rb;
|
||||
return true;
|
||||
}
|
||||
for (int h = 0; h < PLE_N_HEADS; ++h) read_row(impl_->rows[h], out2560 + (size_t) h * PLE_HEAD_DIM);
|
||||
@@ -302,11 +364,11 @@ bool PleTable::gather_batch(const uint32_t* rows, size_t n_tokens, float* out, s
|
||||
if (impl_->pending) { err = "PleTable::gather_batch while a token is in flight"; return false; }
|
||||
const size_t n = n_tokens * (size_t) PLE_N_HEADS;
|
||||
if (impl_->mode == PleIo::Direct) {
|
||||
std::vector<uint8_t> raw(n * PLE_ROW_BYTES);
|
||||
std::vector<uint8_t> raw(n * impl_->rb);
|
||||
const auto ticket = impl_->reader.issue(rows, n, raw.data());
|
||||
if (!impl_->reader.collect(ticket, err)) return false;
|
||||
for (size_t i = 0; i < n; ++i) iq4nl_dequant_row(raw.data() + i * PLE_ROW_BYTES, out + i * PLE_HEAD_DIM);
|
||||
impl_->bytes_read += (uint64_t) n * PLE_ROW_BYTES;
|
||||
for (size_t i = 0; i < n; ++i) impl_->decode(raw.data() + i * impl_->rb, out + i * PLE_HEAD_DIM);
|
||||
impl_->bytes_read += (uint64_t) n * impl_->rb;
|
||||
return true;
|
||||
}
|
||||
for (size_t i = 0; i < n; ++i) read_row(rows[i], out + i * PLE_HEAD_DIM);
|
||||
@@ -317,15 +379,18 @@ void PleTable::set_injected_delay_us(double us) { impl_->reader.set_injected_del
|
||||
|
||||
std::string PleTable::io_report() const {
|
||||
if (impl_->mode != PleIo::Direct || !impl_->reader.is_open()) return {};
|
||||
const strata::ngram::ReaderStats& s = impl_->reader.stats();
|
||||
char buf[320];
|
||||
std::snprintf(buf, sizeof buf,
|
||||
const strata::ngram::ReaderStats s = impl_->reader.snapshot();
|
||||
char buf[400];
|
||||
int n = std::snprintf(buf, sizeof buf,
|
||||
"ple io: %llu rows, %.1f%% row-cache hits, %llu SSD reads (%.1f MB), read p50 %.0f us p99 %.0f us, "
|
||||
"blocked %.3f ms total (submit %.3f ms), cache %llu/%llu rows",
|
||||
(unsigned long long) s.requests, s.requests ? 100.0 * (double) s.cache_hits / (double) s.requests : 0.0,
|
||||
(unsigned long long) s.reads, (double) s.bytes / 1e6, s.percentile(0.5), s.percentile(0.99),
|
||||
s.wait_us / 1000.0, s.submit_us / 1000.0, (unsigned long long) impl_->reader.cache_size(),
|
||||
(unsigned long long) impl_->reader.cache_capacity());
|
||||
if (s.keepalive_reads > 0 && n > 0 && n < (int) sizeof buf)
|
||||
std::snprintf(buf + n, sizeof buf - (size_t) n, ", SSD kept awake by %llu reads (slowest %.1f ms)",
|
||||
(unsigned long long) s.keepalive_reads, s.keepalive_us_max / 1000.0);
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -360,8 +425,8 @@ void PleTable::gather(const uint32_t* rows16, float* out2560) const {
|
||||
for (int h = 0; h < PLE_N_HEADS; ++h) {
|
||||
// Out-of-range rows are handled by `read_row` as zeros and have no address to prefetch.
|
||||
if (rows16[h] >= impl_->n_rows) continue;
|
||||
ranges[n].VirtualAddress = (PVOID) (impl_->data + (size_t) rows16[h] * PLE_ROW_BYTES);
|
||||
ranges[n].NumberOfBytes = PLE_ROW_BYTES;
|
||||
ranges[n].VirtualAddress = (PVOID) (impl_->data + (size_t) rows16[h] * impl_->rb);
|
||||
ranges[n].NumberOfBytes = impl_->rb;
|
||||
++n;
|
||||
}
|
||||
if (n > 0) (void) PrefetchVirtualMemory(GetCurrentProcess(), n, ranges, 0);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// src/kernels/ple_fp8_parity.cpp - the FP8 PLE table (tools/ple_fp8_pack.py) read through PleTable, against values
|
||||
// decoded from the checkpoint itself, and against the IQ4_NL table it replaces.
|
||||
//
|
||||
// ple_fp8_parity <ple-fp8.gguf> <reference.bin> [<iq4nl shard.gguf>]
|
||||
//
|
||||
// reference.bin: int32 n, uint32 rows[n], float32 values[n][160] - the rows as torch decodes the checkpoint's
|
||||
// F8_E4M3 bytes, times its weight_scale (n a multiple of 16 for the gather_batch check). Every row must match BIT FOR BIT through
|
||||
// both I/O modes (Direct and Mmap) and through the prompt path's gather_batch. With the IQ4_NL shard the same rows
|
||||
// are compared too: same table, so a correlation near 0.997 - a shifted row order would be ~0.
|
||||
#include "strata/kernels/ngram.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace k = strata::kernels;
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 3) {
|
||||
std::fprintf(stderr, "usage: ple_fp8_parity <ple-fp8.gguf> <reference.bin> [<iq4nl shard.gguf>]\n");
|
||||
return 2;
|
||||
}
|
||||
std::ifstream f(argv[2], std::ios::binary);
|
||||
int32_t n = 0;
|
||||
f.read((char*) &n, 4);
|
||||
std::vector<uint32_t> rows((size_t) n);
|
||||
std::vector<float> ref((size_t) n * k::PLE_HEAD_DIM);
|
||||
f.read((char*) rows.data(), (std::streamsize) rows.size() * 4);
|
||||
f.read((char*) ref.data(), (std::streamsize) ref.size() * 4);
|
||||
if (!f || n <= 0) { std::fprintf(stderr, "cannot read %s\n", argv[2]); return 2; }
|
||||
|
||||
int bad = 0;
|
||||
for (const k::PleIo mode : {k::PleIo::Direct, k::PleIo::Mmap}) {
|
||||
k::PleTable t;
|
||||
std::string err;
|
||||
k::PleIoOptions io;
|
||||
io.mode = mode;
|
||||
if (!t.open(argv[1], err, io)) { std::fprintf(stderr, "open: %s\n", err.c_str()); return 1; }
|
||||
std::vector<float> got((size_t) n * k::PLE_HEAD_DIM);
|
||||
for (int i = 0; i < n; ++i) t.read_row(rows[(size_t) i], got.data() + (size_t) i * k::PLE_HEAD_DIM);
|
||||
int diff = 0;
|
||||
for (size_t j = 0; j < got.size(); ++j) diff += std::memcmp(&got[j], &ref[j], 4) != 0;
|
||||
std::vector<float> batch((size_t) n * k::PLE_HEAD_DIM); // the prompt path: all rows as one request
|
||||
const bool ok = t.gather_batch(rows.data(), (size_t) n / k::PLE_N_HEADS, batch.data(), err);
|
||||
int bdiff = 0;
|
||||
const size_t nb = (size_t) n / k::PLE_N_HEADS * k::PLE_N_HEADS * k::PLE_HEAD_DIM;
|
||||
for (size_t j = 0; ok && j < nb; ++j) bdiff += std::memcmp(&batch[j], &ref[j], 4) != 0;
|
||||
std::printf("%-6s %s, %llu rows: read_row %d/%zu values differ, gather_batch %s %d differ\n",
|
||||
mode == k::PleIo::Direct ? "Direct" : "Mmap", t.format(), (unsigned long long) t.rows(), diff,
|
||||
got.size(), ok ? "ok," : "FAILED,", bdiff);
|
||||
bad += diff + bdiff + (ok ? 0 : 1) + (std::strcmp(t.format(), "F8_E4M3") != 0);
|
||||
}
|
||||
if (argc > 3) {
|
||||
k::PleTable q;
|
||||
std::string err;
|
||||
if (!q.open(argv[3], err)) { std::fprintf(stderr, "open %s: %s\n", argv[3], err.c_str()); return 1; }
|
||||
double cmin = 1, rsum = 0;
|
||||
int counted = 0;
|
||||
std::vector<float> v(k::PLE_HEAD_DIM);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
q.read_row(rows[(size_t) i], v.data());
|
||||
const float* r = ref.data() + (size_t) i * k::PLE_HEAD_DIM;
|
||||
double sa = 0, sb = 0, sab = 0, sa2 = 0, sb2 = 0, e2 = 0, r2 = 0;
|
||||
for (int j = 0; j < k::PLE_HEAD_DIM; ++j) {
|
||||
sa += r[j]; sb += v[j]; sab += (double) r[j] * v[j]; sa2 += (double) r[j] * r[j]; sb2 += (double) v[j] * v[j];
|
||||
e2 += ((double) v[j] - r[j]) * ((double) v[j] - r[j]); r2 += (double) r[j] * r[j];
|
||||
}
|
||||
if (r2 == 0) continue; // an all-zero row (the table has some)
|
||||
const double m = k::PLE_HEAD_DIM;
|
||||
const double c = (sab - sa * sb / m) / std::sqrt((sa2 - sa * sa / m) * (sb2 - sb * sb / m));
|
||||
cmin = std::fmin(cmin, c);
|
||||
rsum += std::sqrt(e2 / r2);
|
||||
++counted;
|
||||
}
|
||||
std::printf("IQ4_NL table, same rows: correlation min %.4f, error vs FP8 %.1f%% mean (%d non-zero rows)\n",
|
||||
cmin, 100 * rsum / (counted ? counted : 1), counted);
|
||||
if (cmin < 0.98) ++bad;
|
||||
}
|
||||
std::printf("RESULT: %s\n", bad ? "MISMATCH" : "FP8 rows exact");
|
||||
return bad ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Real-artifact Q5_0 PLE rows against ggml's reference dequantizer.
|
||||
#define NOMINMAX
|
||||
#include "strata/artifact/gguf_reader.hpp"
|
||||
#include "strata/kernels/ngram.hpp"
|
||||
#include "ggml.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 2) {
|
||||
std::fprintf(stderr, "usage: ple_q5_parity <gguf-containing-ple>\n");
|
||||
return 2;
|
||||
}
|
||||
strata::GgufFile gguf(argv[1]);
|
||||
const auto* tensor = gguf.find("per_layer_token_embd.weight");
|
||||
if (!tensor || tensor->type != 6 || tensor->shape.size() != 2 || tensor->shape[0] != 160) {
|
||||
std::fprintf(stderr, "expected Q5_0 PLE [160, N]\n");
|
||||
return 2;
|
||||
}
|
||||
strata::kernels::PleIoOptions options;
|
||||
options.mode = strata::kernels::PleIo::Mmap;
|
||||
strata::kernels::PleTable table;
|
||||
std::string err;
|
||||
if (!table.open(argv[1], err, options)) {
|
||||
std::fprintf(stderr, "%s\n", err.c_str());
|
||||
return 1;
|
||||
}
|
||||
const auto* traits = ggml_get_type_traits(GGML_TYPE_Q5_0);
|
||||
const uint8_t* bytes = gguf.tensor_data(*tensor);
|
||||
const uint32_t probes[] = {0, 1, 12345, 20000003, (uint32_t) (table.rows() - 1)};
|
||||
double max_abs = 0.0;
|
||||
for (uint32_t row : probes) {
|
||||
float got[160], want[160];
|
||||
table.read_row(row, got);
|
||||
traits->to_float(bytes + (size_t) row * 110, want, 160);
|
||||
for (int i = 0; i < 160; ++i)
|
||||
max_abs = std::max(max_abs, (double) std::fabs(got[i] - want[i]));
|
||||
}
|
||||
uint32_t rows[16];
|
||||
for (int i = 0; i < 16; ++i) rows[i] = probes[i % 5];
|
||||
float batch[16 * 160];
|
||||
if (!table.issue(rows) || !table.collect(batch, err)) {
|
||||
std::fprintf(stderr, "collect: %s\n", err.c_str());
|
||||
return 1;
|
||||
}
|
||||
for (int h = 0; h < 16; ++h) {
|
||||
float want[160];
|
||||
traits->to_float(bytes + (size_t) rows[h] * 110, want, 160);
|
||||
for (int i = 0; i < 160; ++i)
|
||||
max_abs = std::max(max_abs, (double) std::fabs(batch[h * 160 + i] - want[i]));
|
||||
}
|
||||
std::printf("Q5_0 PLE: %zu rows, max_abs %.3e %s\n", (size_t) table.rows(), max_abs,
|
||||
max_abs <= 1e-6 ? "PASS" : "FAIL");
|
||||
return max_abs <= 1e-6 ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Q5_1 shared-expert projection: Strata CUDA dequantization vs ggml.
|
||||
#define NOMINMAX
|
||||
#include "strata/artifact/gguf_reader.hpp"
|
||||
#include "strata/kernels/dequant_bf16.hpp"
|
||||
#include "ggml.h"
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 2) {
|
||||
std::fprintf(stderr, "usage: q5_projection_parity <shard1.gguf>\n");
|
||||
return 2;
|
||||
}
|
||||
strata::GgufFile gguf(argv[1]);
|
||||
const auto* t = gguf.find("blk.0.ffn_down_shexp.weight");
|
||||
if (!t || t->type != 7 || t->shape.size() != 2) {
|
||||
std::fprintf(stderr, "expected blk.0.ffn_down_shexp.weight Q5_1\n");
|
||||
return 2;
|
||||
}
|
||||
const int64_t cols = (int64_t) t->shape[0], rows = std::min<int64_t>(4, (int64_t) t->shape[1]);
|
||||
const size_t weight_bytes = (size_t) rows * (size_t) (cols / 32) * 24;
|
||||
const size_t elements = (size_t) rows * (size_t) cols;
|
||||
const uint8_t* source = gguf.tensor_data(*t);
|
||||
void *dweight = nullptr, *dfloat = nullptr, *dbf16 = nullptr;
|
||||
if (cudaMalloc(&dweight, weight_bytes) != cudaSuccess ||
|
||||
cudaMalloc(&dfloat, elements * sizeof(float)) != cudaSuccess ||
|
||||
cudaMalloc(&dbf16, elements * sizeof(uint16_t)) != cudaSuccess) {
|
||||
std::fprintf(stderr, "CUDA allocation failed\n");
|
||||
return 1;
|
||||
}
|
||||
cudaMemcpy(dweight, source, weight_bytes, cudaMemcpyHostToDevice);
|
||||
strata::kernels::dequant_f32(7, dweight, 0, rows, cols, (float*) dfloat, nullptr);
|
||||
strata::kernels::dequant_bf16(7, dweight, 0, rows, cols, (uint16_t*) dbf16, nullptr);
|
||||
std::vector<float> got(elements), ref(elements);
|
||||
std::vector<uint16_t> got_bf16(elements);
|
||||
cudaMemcpy(got.data(), dfloat, elements * sizeof(float), cudaMemcpyDeviceToHost);
|
||||
cudaMemcpy(got_bf16.data(), dbf16, elements * sizeof(uint16_t), cudaMemcpyDeviceToHost);
|
||||
cudaFree(dweight); cudaFree(dfloat); cudaFree(dbf16);
|
||||
const auto* traits = ggml_get_type_traits(GGML_TYPE_Q5_1);
|
||||
for (int64_t r = 0; r < rows; ++r)
|
||||
traits->to_float(source + (size_t) r * (size_t) (cols / 32) * 24,
|
||||
ref.data() + (size_t) r * (size_t) cols, cols);
|
||||
double max_abs = 0;
|
||||
size_t bf16_bad = 0;
|
||||
for (size_t i = 0; i < elements; ++i) {
|
||||
max_abs = std::max(max_abs, (double) std::fabs(got[i] - ref[i]));
|
||||
uint32_t bits;
|
||||
std::memcpy(&bits, &ref[i], sizeof(bits));
|
||||
const uint16_t expected = (uint16_t) ((bits + 0x7fffu + ((bits >> 16) & 1u)) >> 16);
|
||||
if (got_bf16[i] != expected) ++bf16_bad;
|
||||
}
|
||||
const bool ok = max_abs <= 1e-6 && bf16_bad == 0;
|
||||
std::printf("Q5_1 projection: %zu elements, max_abs %.3e, bf16 differences %zu %s\n",
|
||||
elements, max_abs, bf16_bad, ok ? "PASS" : "FAIL");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
// 1. against FP64, the new kernel's error is no larger than a small multiple of the old kernel's (both FP32 math);
|
||||
// 2. the new and old outputs agree to a relative 1e-4 of the output scale;
|
||||
// then times both over a prompt chunk (the old one in batches of 32, as prefill.cpp calls it).
|
||||
// HIP builds (S6): the same checks for the RDNA4 matrix-core kernel (STRATA_HIP_WMMA), skipped (77) off gfx12.
|
||||
// Usage: qsa_prompt_attn_parity [context=32768] [queries=2048] [reps=5]
|
||||
#include "strata/kernels/qsa.hpp"
|
||||
#include "strata/kernels/qsa_decode_attn.hpp"
|
||||
@@ -19,6 +20,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
@@ -196,12 +198,31 @@ int run(int fmt, int64_t ctx, int64_t nq, int reps) { // fmt 1 int8, 0 fp16
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
#if defined(__HIP_PLATFORM_AMD__)
|
||||
// S6: on AMD the kernel under test is the RDNA4 matrix-core one (opt-in in the engine); other cards skip
|
||||
{
|
||||
int dev = 0;
|
||||
hipDeviceProp_t prop{};
|
||||
if (hipGetDevice(&dev) != hipSuccess || hipGetDeviceProperties(&prop, dev) != hipSuccess) return 2;
|
||||
if (std::strncmp(prop.gcnArchName, "gfx12", 5) != 0) {
|
||||
std::printf("SKIP: %s is not gfx12 (the matrix-core prompt attention is RDNA4 only)\n", prop.gcnArchName);
|
||||
return 77;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
_putenv_s("STRATA_HIP_WMMA", "1");
|
||||
#else
|
||||
setenv("STRATA_HIP_WMMA", "1", 1);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
const int64_t ctx = argc > 1 ? std::atoll(argv[1]) : 32768;
|
||||
const int64_t nq = argc > 2 ? std::atoll(argv[2]) : 2048;
|
||||
const int reps = argc > 3 ? std::atoi(argv[3]) : 5;
|
||||
int fails = 0;
|
||||
fails += run(1, ctx, nq, reps);
|
||||
fails += run(0, ctx, nq, reps);
|
||||
#if !defined(__HIP_PLATFORM_AMD__)
|
||||
fails += run(0, ctx, nq, reps); // FP16 KV: the RDNA4 kernel takes int8 KV only
|
||||
#endif
|
||||
fails += run(1, 1500, std::min<int64_t>(nq, 1500), reps); // short context: the selection is every cell
|
||||
fails += run(1, 2100, std::min<int64_t>(nq, 256), reps); // the identity-to-sparse edge
|
||||
std::printf("FAILURES: %d\n", fails);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// src/kernels/qsa_select_bench.cpp - the prompt path's QSA selection (qsa_select.hpp) timed per stage, block scores
|
||||
// (the warp kernel and the tensor-core one) and top-k, for a batch of consecutive queries at a given context, and
|
||||
// the two scorers compared: score difference and how many selections differ (GPU, synthetic, no model).
|
||||
// Usage: qsa_select_bench [context=131072] [queries=256] [reps=10]
|
||||
// Usage: qsa_select_bench [context=131072] [queries=256] [reps=10] [capacity_cells]
|
||||
// capacity_cells (the engine's --max-context): the score buffers and the top-k dispatch follow the CAPACITY
|
||||
// (max_blocks = capacity / 4 + 2), the work follows the context. Default: capacity = context (max_blocks = ctx / 4 + 1).
|
||||
#include "strata/kernels/qsa.hpp"
|
||||
#include "strata/kernels/qsa_select.hpp"
|
||||
|
||||
@@ -33,7 +35,8 @@ int main(int argc, char** argv) {
|
||||
const int64_t nq = argc > 2 ? std::atoll(argv[2]) : 256;
|
||||
const int reps = argc > 3 ? std::atoi(argv[3]) : 10;
|
||||
const k::QsaShapes s = k::qsa_real_shapes();
|
||||
const int64_t max_blocks = ctx / 4 + 1, cap = k::qsa_selection_width(k::kTopkMaxCells, s);
|
||||
const int64_t capacity = argc > 4 ? std::atoll(argv[4]) : 0;
|
||||
const int64_t max_blocks = capacity > 0 ? capacity / 4 + 2 : ctx / 4 + 1, cap = k::qsa_selection_width(k::kTopkMaxCells, s);
|
||||
std::mt19937 rng(7);
|
||||
std::normal_distribution<float> nd(0.f, 1.f);
|
||||
// keys with a shared direction plus noise, so the scores have a spread like a real indexer's
|
||||
@@ -67,6 +70,8 @@ int main(int argc, char** argv) {
|
||||
ck(cudaMalloc(&ids_new, (size_t) (nq * cap) * 4), "malloc");
|
||||
const int64_t active = steps[(size_t) ((nq - 1) * k::kStepCount + k::kStepNBid)] + 1;
|
||||
auto run_old = [&] { k::qsa_block_scores(d_pooled, d_dead, d_q, d_steps, nq, max_blocks, s, sc_old, nullptr, active); };
|
||||
// a device without the tensor-core scorer (HIP other than gfx12): time the warp scorer and the top-k only
|
||||
const bool have_tc = k::qsa_block_scores_tc(d_pooled, d_dead, d_q, d_steps, 1, max_blocks, s, sc_new, nullptr, active);
|
||||
auto run_new = [&] {
|
||||
if (!k::qsa_block_scores_tc(d_pooled, d_dead, d_q, d_steps, nq, max_blocks, s, sc_new, nullptr, active)) {
|
||||
std::fprintf(stderr, "tc scorer refused\n");
|
||||
@@ -74,12 +79,13 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
};
|
||||
run_old();
|
||||
run_new();
|
||||
if (have_tc) run_new();
|
||||
else ck(cudaMemcpy(sc_new, sc_old, (size_t) (nq * max_blocks) * 4, cudaMemcpyDeviceToDevice), "copy");
|
||||
k::qsa_block_topk_ref(sc_old, d_steps, nq, max_blocks, cap, s, ids_old, nullptr);
|
||||
k::qsa_block_topk(sc_new, d_steps, nq, max_blocks, cap, s, ids_new, nullptr);
|
||||
k::qsa_block_topk(sc_new, d_steps, nq, max_blocks, cap, s, ids_new, nullptr, active);
|
||||
int32_t* ids_reg = nullptr; // the register top-k on the OLD scores: must equal the reference exactly
|
||||
ck(cudaMalloc(&ids_reg, (size_t) (nq * cap) * 4), "malloc");
|
||||
k::qsa_block_topk(sc_old, d_steps, nq, max_blocks, cap, s, ids_reg, nullptr);
|
||||
k::qsa_block_topk(sc_old, d_steps, nq, max_blocks, cap, s, ids_reg, nullptr, active);
|
||||
ck(cudaDeviceSynchronize(), "warm");
|
||||
// compare
|
||||
std::vector<float> a((size_t) (nq * max_blocks)), b(a.size());
|
||||
@@ -114,6 +120,32 @@ int main(int argc, char** argv) {
|
||||
cells_diff += (int64_t) d.size() / 2;
|
||||
cells_all += w;
|
||||
}
|
||||
// accuracy against an FP64 host reference on a sample (blocks below n_bid; the tail block is the warp kernel's own
|
||||
// arithmetic in both scorers). Gate, as the prompt-attention harness's: the scorer under test is no worse than 4x
|
||||
// the warp kernel's error, floored at 1e-6 of the score scale.
|
||||
double err_old = 0, err_new = 0, scale = 0;
|
||||
{
|
||||
std::mt19937 srng(11);
|
||||
const int64_t nqs = std::min<int64_t>(nq, 32);
|
||||
for (int64_t qs = 0; qs < nqs; ++qs) {
|
||||
const int64_t i = qs * nq / nqs;
|
||||
const int64_t nbid = steps[(size_t) (i * k::kStepCount + k::kStepNBid)];
|
||||
if (nbid <= 0) continue;
|
||||
for (int sidx = 0; sidx < 1024; ++sidx) {
|
||||
const int64_t j = sidx < 64 ? std::min<int64_t>(nbid - 1, sidx) : (int64_t) (srng() % (uint64_t) nbid);
|
||||
double ref = 0;
|
||||
for (int h = 0; h < 4; ++h) {
|
||||
double d = 0;
|
||||
for (int c = 0; c < 128; ++c) d += (double) q[(size_t) (i * 512 + h * 128 + c)] * (double) pooled[(size_t) (j * 128 + c)];
|
||||
ref += d > 0 ? d : 0;
|
||||
}
|
||||
scale = std::max(scale, std::fabs(ref));
|
||||
err_old = std::max(err_old, std::fabs(ref - (double) a[(size_t) (i * max_blocks + j)]));
|
||||
err_new = std::max(err_new, std::fabs(ref - (double) b[(size_t) (i * max_blocks + j)]));
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool acc_ok = !have_tc || err_new <= std::max(4.0 * err_old, 1e-6 * scale);
|
||||
// time
|
||||
cudaEvent_t e0, e1;
|
||||
cudaEventCreate(&e0); cudaEventCreate(&e1);
|
||||
@@ -126,14 +158,17 @@ int main(int argc, char** argv) {
|
||||
cudaEventElapsedTime(&ms, e0, e1);
|
||||
return ms / reps;
|
||||
};
|
||||
const float t_old = timed(run_old), t_new = timed(run_new);
|
||||
const float t_old = timed(run_old), t_new = have_tc ? timed(run_new) : t_old;
|
||||
const float t_tk = timed([&] { k::qsa_block_topk_ref(sc_old, d_steps, nq, max_blocks, cap, s, ids_old, nullptr); });
|
||||
const float t_tk2 = timed([&] { k::qsa_block_topk(sc_old, d_steps, nq, max_blocks, cap, s, ids_reg, nullptr); });
|
||||
const float t_tk2 = timed([&] { k::qsa_block_topk(sc_old, d_steps, nq, max_blocks, cap, s, ids_reg, nullptr, active); });
|
||||
std::printf("top-k %.3f -> %.3f ms (%.1fx), register top-k identical to the reference %lld/%lld\n", t_tk, t_tk2,
|
||||
t_tk / t_tk2, (long long) reg_same, (long long) nq);
|
||||
std::printf("%s accuracy vs FP64 (score scale %.3g): warp kernel max err %.3g, tensor-core max err %.3g (%.2g of scale)\n",
|
||||
!have_tc ? "SKIP" : acc_ok ? "PASS" : "FAIL", scale, err_old, err_new, scale > 0 ? err_new / scale : 0.0);
|
||||
if (!have_tc) std::printf("tensor-core scorer not available on this device: warp scorer %.3f ms, top-k %.3f ms (%.0f%% of the two)\n", t_old, t_tk2, 100.0 * t_tk2 / (t_old + t_tk2));
|
||||
std::printf("ctx %lld, %lld queries x %lld blocks: scores %.3f -> %.3f ms (%.1fx), top-k %.3f ms; score rel diff "
|
||||
"mean %.2g max %.2g; selections identical %lld/%lld, cells differing %.4f%%\n", (long long) ctx,
|
||||
(long long) nq, (long long) active, t_old, t_new, t_old / t_new, t_tk, sum_rel / std::max(1.0, n_rel),
|
||||
max_rel, (long long) same_sel, (long long) nq, cells_all ? 100.0 * (double) cells_diff / (double) cells_all : 0.0);
|
||||
return 0;
|
||||
return acc_ok && reg_same == nq ? 0 : 1;
|
||||
}
|
||||
+114
-21
@@ -2,6 +2,7 @@
|
||||
#include "strata/ngram/ple_reader.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
@@ -33,15 +34,17 @@ constexpr uint32_t EMPTY = 0xFFFFFFFFu;
|
||||
/// Set-associative row cache: 8 ways per set, round-robin replacement inside a set. Bounded by construction.
|
||||
struct RowCache {
|
||||
uint64_t sets = 0;
|
||||
uint32_t rb = ROW_BYTES; // bytes per row
|
||||
std::vector<uint32_t> keys; // sets * WAYS
|
||||
std::vector<uint8_t> data; // sets * WAYS * ROW_BYTES
|
||||
std::vector<uint8_t> data; // sets * WAYS * rb
|
||||
std::vector<uint8_t> next; // per-set replacement pointer
|
||||
uint64_t used = 0;
|
||||
|
||||
void init(uint64_t rows) {
|
||||
void init(uint64_t rows, uint32_t row_bytes = ROW_BYTES) {
|
||||
sets = rows / WAYS;
|
||||
rb = row_bytes;
|
||||
keys.assign(sets * WAYS, EMPTY);
|
||||
data.assign(sets * WAYS * ROW_BYTES, 0);
|
||||
data.assign(sets * WAYS * rb, 0);
|
||||
next.assign(sets, 0);
|
||||
used = 0;
|
||||
}
|
||||
@@ -53,7 +56,7 @@ struct RowCache {
|
||||
if (sets == 0) return nullptr;
|
||||
const uint64_t s = mix(row) % sets;
|
||||
for (uint32_t w = 0; w < WAYS; ++w)
|
||||
if (keys[s * WAYS + w] == row) return &data[(s * WAYS + w) * ROW_BYTES];
|
||||
if (keys[s * WAYS + w] == row) return &data[(s * WAYS + w) * rb];
|
||||
return nullptr;
|
||||
}
|
||||
void insert(uint32_t row, const uint8_t* bytes) {
|
||||
@@ -63,7 +66,7 @@ struct RowCache {
|
||||
next[s] = (uint8_t) ((w + 1) % WAYS);
|
||||
if (keys[s * WAYS + w] == EMPTY) ++used;
|
||||
keys[s * WAYS + w] = row;
|
||||
std::memcpy(&data[(s * WAYS + w) * ROW_BYTES], bytes, ROW_BYTES);
|
||||
std::memcpy(&data[(s * WAYS + w) * rb], bytes, rb);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -79,6 +82,7 @@ struct Job {
|
||||
uint32_t ticket = 0;
|
||||
std::vector<Use> uses;
|
||||
double issued_us = 0;
|
||||
bool keepalive = false;// no rows and no ticket: it only keeps the SSD awake (`set_keepalive`)
|
||||
};
|
||||
|
||||
struct TicketState {
|
||||
@@ -95,6 +99,7 @@ struct PleReader::Impl {
|
||||
DirectFile file;
|
||||
uint64_t table_offset = 0;
|
||||
uint64_t n_rows = 0;
|
||||
uint32_t row_bytes = ROW_BYTES;
|
||||
uint32_t max_inflight = 0;
|
||||
uint8_t* slab = nullptr; // max_inflight slots of 2 pages
|
||||
std::vector<uint32_t> free_slots;
|
||||
@@ -108,6 +113,12 @@ struct PleReader::Impl {
|
||||
size_t ring_pos = 0;
|
||||
double delay_us = 0;
|
||||
std::string error;
|
||||
// the keep-alive (`set_keepalive`), steady-clock microseconds; `keep_us` 0 = off
|
||||
double keep_us = 0;
|
||||
double keep_window_us = 0;
|
||||
double last_issue_us = 0; // the last `issue`
|
||||
double last_read_us = 0; // the last read that went out, rows or keep-alive
|
||||
uint64_t rng = 0x9E3779B97F4A7C15ull;
|
||||
|
||||
bool threaded = false;
|
||||
bool stop = false;
|
||||
@@ -141,7 +152,14 @@ struct PleReader::Impl {
|
||||
queue.pop_front();
|
||||
Job& j = inflight[s];
|
||||
j.issued_us = now_us();
|
||||
if (!file.submit(j.offset, slot_buf(s), j.length, s, error)) {
|
||||
std::string kerr; // a keep-alive read that cannot go out must not fail the reader
|
||||
if (!file.submit(j.offset, slot_buf(s), j.length, s, j.keepalive ? kerr : error)) {
|
||||
if (j.keepalive) { // no more of them: the SSD may sleep as before
|
||||
keep_us = 0;
|
||||
j.keepalive = false;
|
||||
free_slots.push_back(s);
|
||||
continue;
|
||||
}
|
||||
j.uses.clear();
|
||||
auto it = tickets.find(j.ticket);
|
||||
if (it != tickets.end() && it->second.pending > 0) --it->second.pending;
|
||||
@@ -149,12 +167,35 @@ struct PleReader::Impl {
|
||||
cancel_queued();
|
||||
return false;
|
||||
}
|
||||
stats.submit_us += now_us() - j.issued_us;
|
||||
++stats.reads;
|
||||
last_read_us = j.issued_us;
|
||||
if (!j.keepalive) { // a keep-alive read counts when it completes (`finish`)
|
||||
stats.submit_us += now_us() - j.issued_us;
|
||||
++stats.reads;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// When the next keep-alive read is due, or < 0 when none is: it is off, the reader failed, or no rows
|
||||
/// were asked for within the window (then the SSD may sleep; the next `issue` re-arms it).
|
||||
double keepalive_due(double now) const {
|
||||
if (keep_us <= 0 || !error.empty() || last_issue_us <= 0 || now - last_issue_us > keep_window_us) return -1;
|
||||
return last_read_us + keep_us;
|
||||
}
|
||||
|
||||
/// One page of the table, a different one each time, so the SSD really reads (not its controller's buffer).
|
||||
void queue_keepalive() {
|
||||
rng ^= rng << 13;
|
||||
rng ^= rng >> 7;
|
||||
rng ^= rng << 17;
|
||||
const uint64_t first = table_offset / PAGE, end = (table_offset + n_rows * (uint64_t) row_bytes) / PAGE;
|
||||
Job j;
|
||||
j.offset = (first + (end > first ? rng % (end - first) : 0)) * PAGE;
|
||||
j.length = PAGE;
|
||||
j.keepalive = true;
|
||||
queue.push_back(std::move(j));
|
||||
}
|
||||
|
||||
bool finish(const Completion& c) {
|
||||
const uint32_t s = (uint32_t) c.tag;
|
||||
if (s >= inflight.size()) {
|
||||
@@ -163,6 +204,17 @@ struct PleReader::Impl {
|
||||
return false;
|
||||
}
|
||||
Job& j = inflight[s];
|
||||
if (j.keepalive) { // no rows: it counts once it is done, with how long it took;
|
||||
if (c.ok) { // a failed one turns the keep-alive off, not the reader
|
||||
++stats.keepalive_reads;
|
||||
stats.keepalive_us_max = std::max(stats.keepalive_us_max, now_us() - j.issued_us);
|
||||
} else {
|
||||
keep_us = 0;
|
||||
}
|
||||
j.keepalive = false;
|
||||
free_slots.push_back(s);
|
||||
return error.empty() ? pump() : true;
|
||||
}
|
||||
if (!c.ok) {
|
||||
error = "PleReader: a table read failed";
|
||||
cancel_queued();
|
||||
@@ -176,7 +228,7 @@ struct PleReader::Impl {
|
||||
stats.bytes += c.bytes;
|
||||
const uint8_t* buf = slot_buf(s);
|
||||
for (const Use& u : j.uses) {
|
||||
if (u.in_page + ROW_BYTES > c.bytes) {
|
||||
if (u.in_page + row_bytes > c.bytes) {
|
||||
error = "PleReader: short read inside the table";
|
||||
cancel_queued();
|
||||
j.uses.clear();
|
||||
@@ -187,7 +239,7 @@ struct PleReader::Impl {
|
||||
}
|
||||
}
|
||||
for (const Use& u : j.uses) {
|
||||
std::memcpy(u.dst, buf + u.in_page, ROW_BYTES);
|
||||
std::memcpy(u.dst, buf + u.in_page, row_bytes);
|
||||
cache.insert(u.row, buf + u.in_page);
|
||||
}
|
||||
auto it = tickets.find(j.ticket);
|
||||
@@ -246,7 +298,14 @@ struct PleReader::Impl {
|
||||
void worker_loop() {
|
||||
std::unique_lock<std::mutex> lk(mu);
|
||||
for (;;) {
|
||||
cv_work.wait(lk, [&] { return stop || !queue.empty() || busy(); });
|
||||
// Idle: wait for work. While rows are being asked for, the wait ends in time for a keep-alive read.
|
||||
while (!(stop || !queue.empty() || busy())) {
|
||||
const double now = now_us();
|
||||
const double due = keepalive_due(now);
|
||||
if (due < 0) cv_work.wait(lk);
|
||||
else if (now >= due) queue_keepalive();
|
||||
else cv_work.wait_for(lk, std::chrono::microseconds(std::max<int64_t>(1000, (int64_t) (due - now))));
|
||||
}
|
||||
if (stop && !busy()) break;
|
||||
if (error.empty() && !pump() && error.empty()) error = "PleReader: submit failed";
|
||||
if (!delayed.empty() && !release_delayed() && error.empty()) error = "PleReader: read failed";
|
||||
@@ -274,11 +333,13 @@ PleReader::~PleReader() {
|
||||
}
|
||||
|
||||
bool PleReader::open(const std::string& path, uint64_t table_offset, uint64_t n_rows, uint32_t max_inflight,
|
||||
uint64_t cache_rows, std::string& err, bool io_thread) {
|
||||
uint64_t cache_rows, std::string& err, bool io_thread, uint32_t row_bytes) {
|
||||
close();
|
||||
if (max_inflight == 0 || max_inflight > 1024) { err = "PleReader: max_inflight must be 1..1024"; return false; }
|
||||
if (row_bytes == 0 || row_bytes > PAGE) { err = "PleReader: row_bytes must be 1..4096"; return false; }
|
||||
if (!impl_->file.open(path, err)) return false;
|
||||
if (table_offset + n_rows * (uint64_t) ROW_BYTES > impl_->file.size()) {
|
||||
impl_->row_bytes = row_bytes;
|
||||
if (table_offset + n_rows * (uint64_t) row_bytes > impl_->file.size()) {
|
||||
err = "PleReader: the table extends past the end of " + path;
|
||||
close();
|
||||
return false;
|
||||
@@ -291,8 +352,12 @@ bool PleReader::open(const std::string& path, uint64_t table_offset, uint64_t n_
|
||||
impl_->inflight.assign(max_inflight, Job{});
|
||||
impl_->free_slots.clear();
|
||||
for (uint32_t s = max_inflight; s-- > 0;) impl_->free_slots.push_back(s);
|
||||
impl_->cache.init(cache_rows);
|
||||
impl_->cache.init(cache_rows, row_bytes);
|
||||
impl_->error.clear();
|
||||
impl_->keep_us = 0;
|
||||
impl_->last_issue_us = impl_->last_read_us = 0;
|
||||
impl_->rng = 0x9E3779B97F4A7C15ull ^ (uint64_t) now_us();
|
||||
if (impl_->rng == 0) impl_->rng = 1;
|
||||
reset_stats();
|
||||
impl_->stop = false;
|
||||
impl_->threaded = io_thread;
|
||||
@@ -346,9 +411,12 @@ void PleReader::close() {
|
||||
m.cache.init(0);
|
||||
m.threaded = false;
|
||||
m.stop = false;
|
||||
m.keep_us = 0;
|
||||
m.last_issue_us = m.last_read_us = 0;
|
||||
}
|
||||
|
||||
bool PleReader::is_open() const { return impl_->file.is_open(); }
|
||||
uint32_t PleReader::row_bytes() const { return impl_->row_bytes; }
|
||||
|
||||
PleReader::Ticket PleReader::issue(const uint32_t* rows, size_t n, uint8_t* out_raw) {
|
||||
Impl& m = *impl_;
|
||||
@@ -356,24 +424,28 @@ PleReader::Ticket PleReader::issue(const uint32_t* rows, size_t n, uint8_t* out_
|
||||
if (m.threaded) lk.lock();
|
||||
const uint32_t id = m.next_ticket++;
|
||||
if (m.next_ticket == 0) m.next_ticket = 1;
|
||||
const double now = now_us();
|
||||
const bool rearm = m.keep_us > 0 && (m.last_issue_us <= 0 || now - m.last_issue_us > m.keep_window_us);
|
||||
m.last_issue_us = now;
|
||||
TicketState& ts = m.tickets[id];
|
||||
std::unordered_map<uint64_t, size_t> by_page; // aligned offset -> index in `jobs`
|
||||
std::vector<Job> jobs;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
uint8_t* dst = out_raw + i * ROW_BYTES;
|
||||
const uint32_t rb = m.row_bytes;
|
||||
uint8_t* dst = out_raw + i * rb;
|
||||
++m.stats.requests;
|
||||
if (rows[i] >= m.n_rows) {
|
||||
std::memset(dst, 0, ROW_BYTES);
|
||||
std::memset(dst, 0, rb);
|
||||
continue;
|
||||
}
|
||||
if (const uint8_t* hit = m.cache.find(rows[i])) {
|
||||
std::memcpy(dst, hit, ROW_BYTES);
|
||||
std::memcpy(dst, hit, rb);
|
||||
++m.stats.cache_hits;
|
||||
continue;
|
||||
}
|
||||
const uint64_t at = m.table_offset + (uint64_t) rows[i] * ROW_BYTES;
|
||||
const uint64_t at = m.table_offset + (uint64_t) rows[i] * rb;
|
||||
const uint64_t first = at / PAGE * PAGE;
|
||||
const uint32_t length = (uint32_t) ((at + ROW_BYTES - 1) / PAGE * PAGE - first + PAGE);
|
||||
const uint32_t length = (uint32_t) ((at + rb - 1) / PAGE * PAGE - first + PAGE);
|
||||
auto f = by_page.find(first);
|
||||
if (f != by_page.end()) {
|
||||
Job& j = jobs[f->second];
|
||||
@@ -400,6 +472,8 @@ PleReader::Ticket PleReader::issue(const uint32_t* rows, size_t n, uint8_t* out_
|
||||
if (has_jobs) {
|
||||
m.cv_work.notify_one();
|
||||
m.file.wake(); // in case the worker is blocked in the port
|
||||
} else if (rearm) {
|
||||
m.cv_work.notify_one(); // the keep-alive had lapsed: start it again
|
||||
}
|
||||
} else if (!m.pump() && m.error.empty()) {
|
||||
m.error = "PleReader: submit failed";
|
||||
@@ -439,14 +513,33 @@ bool PleReader::collect(Ticket t, std::string& err) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void PleReader::set_keepalive(double period_ms, double window_s) {
|
||||
Impl& m = *impl_;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m.mu);
|
||||
m.keep_us = m.threaded && period_ms > 0 ? period_ms * 1000.0 : 0.0;
|
||||
m.keep_window_us = window_s > 0 ? window_s * 1e6 : 0.0;
|
||||
}
|
||||
m.cv_work.notify_all();
|
||||
}
|
||||
|
||||
void PleReader::set_injected_delay_us(double delay_us) {
|
||||
std::lock_guard<std::mutex> lk(impl_->mu);
|
||||
impl_->delay_us = delay_us < 0 ? 0 : delay_us;
|
||||
}
|
||||
const ReaderStats& PleReader::stats() const { return impl_->stats; }
|
||||
ReaderStats PleReader::snapshot() const {
|
||||
Impl& m = *impl_;
|
||||
std::unique_lock<std::mutex> lk(m.mu, std::defer_lock);
|
||||
if (m.threaded) lk.lock();
|
||||
return m.stats;
|
||||
}
|
||||
void PleReader::reset_stats() {
|
||||
impl_->stats = ReaderStats{};
|
||||
impl_->ring_pos = 0;
|
||||
Impl& m = *impl_;
|
||||
std::unique_lock<std::mutex> lk(m.mu, std::defer_lock);
|
||||
if (m.threaded) lk.lock(); // the worker may be counting a keep-alive read
|
||||
m.stats = ReaderStats{};
|
||||
m.ring_pos = 0;
|
||||
}
|
||||
uint64_t PleReader::cache_capacity() const { return impl_->cache.sets * WAYS; }
|
||||
uint64_t PleReader::cache_size() const { return impl_->cache.used; }
|
||||
|
||||
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user