diff --git a/.gitignore b/.gitignore index cb6477db40..8bce5378ef 100644 --- a/.gitignore +++ b/.gitignore @@ -37,17 +37,22 @@ td.log /strata-*.json /strata-*.log /build-strata.bat +/build-strata-cuda12.bat +/build-cuda12/ /.strata-mcp/ # image encoder (setup.py --vision): build output and the generated build script build-vision*/ build-vision.bat +build-vision-cuda12.bat # setup.py outputs (engine, downloads) and release builds /engine/ +/engine-cuda12/ /build-release*/ /dist/ *.done /third_party/*.zip +/.ccache/ /.rocm-win/ diff --git a/CMakeLists.txt b/CMakeLists.txt index ff8766b20d..0121bb0097 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,18 @@ # 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.38 LANGUAGES CXX) +project(strata VERSION 0.1.39 LANGUAGES CXX) +# EXPERIMENTAL: the Intel Arc engine, the SYCL port in sycl/ (PR #423; icpx + oneMKL, docs/INTEL_ARC.md). It is its +# own project that takes the unported sources from src/; with this on, nothing below is configured. +option(STRATA_ENABLE_SYCL "EXPERIMENTAL: build the Intel Arc (SYCL) engine from sycl/ - docs/INTEL_ARC.md" OFF) +if(STRATA_ENABLE_SYCL) + if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP) + message(FATAL_ERROR "STRATA_ENABLE_SYCL is a separate backend; leave STRATA_ENABLE_CUDA and STRATA_ENABLE_HIP off") + endif() + message(WARNING "STRATA_ENABLE_SYCL: the Intel Arc engine is experimental (community port, docs/INTEL_ARC.md)") + add_subdirectory(sycl) + return() +endif() 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 @@ -21,6 +32,18 @@ endif() # instead of this machine's (Strata's own AVX-512 kernels are per-file and chosen at run time either way), and # the MSVC runtime linked in, so no Visual C++ redistributable is needed. option(STRATA_PORTABLE "build for other PCs: AVX2 baseline, static MSVC runtime" OFF) +# EXPERIMENTAL, opt-in: an engine for CPUs without AVX2 (#394, #595, #623), compiled on that PC. ggml-cpu, which then +# computes every CPU expert row, is built for this floor instead of AVX2 or the build host: "avx" (Sandy/Ivy Bridge: +# AVX, no FMA/F16C/AVX2) or "none" (SSE4.2 + POPCNT: Nehalem/Westmere). Strata's own AVX2/AVX-512 kernels stay in the +# binary behind their CPU checks; the router lookahead gets an AVX1 kernel. Empty (the default) changes nothing. +set(STRATA_ISA_FLOOR "" CACHE STRING "experimental older-CPU build: empty (default, AVX2 floor), avx, or none (SSE4.2)") +set_property(CACHE STRATA_ISA_FLOOR PROPERTY STRINGS "" avx none) +if(NOT STRATA_ISA_FLOOR STREQUAL "" AND NOT STRATA_ISA_FLOOR STREQUAL "avx" AND NOT STRATA_ISA_FLOOR STREQUAL "none") + message(FATAL_ERROR "STRATA_ISA_FLOOR must be empty, avx or none (got '${STRATA_ISA_FLOOR}')") +endif() +if(STRATA_ISA_FLOOR AND STRATA_PORTABLE) + message(FATAL_ERROR "STRATA_ISA_FLOOR (an older-CPU build) and STRATA_PORTABLE (the AVX2 release build) exclude each other") +endif() if(STRATA_PORTABLE AND MSVC) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded") endif() @@ -33,7 +56,19 @@ 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; gfx1030 unvalidated)" OFF) +# AMD gfx906 (MI50, wave64): the same .cu sources compiled as HIP through the gfx906 compat layer. The CUDA runtime and +# cuBLAS names are mapped by the headers in include/strata/platform/hip_compat/, which go first on the include path, +# and CUDA::cudart / CUDA::cublas become aliases of HIP's targets, so every CUDA target below stays as it is. This is +# separate from STRATA_ENABLE_HIP (the wave32 RDNA backend): with it on, STRATA_ENABLE_CUDA is on and +# STRATA_ENABLE_HIP stays off, so every "CUDA" branch of the tree is the one taken. +option(STRATA_HIP_GFX906 "Build the CUDA targets for AMD gfx906 through HIP (wave64 compat layer)" OFF) +if(STRATA_HIP_GFX906) + if(STRATA_ENABLE_HIP) + message(FATAL_ERROR "STRATA_HIP_GFX906 and STRATA_ENABLE_HIP are separate backends; enable only one") + endif() + set(STRATA_ENABLE_CUDA ON CACHE BOOL "" FORCE) +endif() +option(STRATA_ENABLE_HIP "Build the HIP targets for wave32 AMD GPUs (gfx1100, gfx1201; gfx1012/gfx1030 unvalidated)" OFF) option(STRATA_PREFILL_MMQ "Build the optional GGML MMQ prefill path for HIP" OFF) # 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 @@ -43,7 +78,7 @@ option(STRATA_ORCA_Q4KS_MMQ "Build the GGML Q5_0 MMQ instance for OrcaRouter Q4_ if(STRATA_PREFILL_MMQ AND NOT STRATA_ENABLE_HIP) message(FATAL_ERROR "STRATA_PREFILL_MMQ requires STRATA_ENABLE_HIP=ON") endif() -if(STRATA_ENABLE_CUDA AND STRATA_ENABLE_HIP) +if(STRATA_ENABLE_CUDA AND STRATA_ENABLE_HIP AND NOT STRATA_HIP_GFX906) message(FATAL_ERROR "STRATA_ENABLE_CUDA and STRATA_ENABLE_HIP are separate backends; enable only one") endif() if(STRATA_ENABLE_HIP) @@ -68,7 +103,34 @@ if(STRATA_BUILD_CONVERSATION_TESTS) add_test(NAME conversation_memory_test COMMAND conversation_memory_test) endif() -if(STRATA_ENABLE_CUDA) +if(STRATA_HIP_GFX906) + if(NOT DEFINED CMAKE_HIP_ARCHITECTURES) + set(CMAKE_HIP_ARCHITECTURES gfx906 CACHE STRING "HIP architectures" FORCE) + endif() + enable_language(HIP) + find_package(hip REQUIRED) + find_package(hipblas REQUIRED) + add_library(CUDA::cudart INTERFACE IMPORTED) + target_link_libraries(CUDA::cudart INTERFACE hip::host) + target_compile_definitions(CUDA::cudart INTERFACE STRATA_HIP_GFX906=1 __HIP_PLATFORM_AMD__=1) + target_include_directories(CUDA::cudart BEFORE INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/platform/hip_compat) + add_library(CUDA::cublas INTERFACE IMPORTED) + target_link_libraries(CUDA::cublas INTERFACE roc::hipblas CUDA::cudart) + set(CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/platform/hip_compat) + include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/platform/hip_compat) + add_compile_definitions(STRATA_HIP_GFX906=1) + message(STATUS "Strata: HIP (gfx906 compat) enabled, arch ${CMAKE_HIP_ARCHITECTURES}") + set(_strata_gpu_runtime_target CUDA::cudart) + set(_strata_gpu_blas_target CUDA::cublas) + set(_strata_gpu_include_directories ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES}) +elseif(STRATA_ENABLE_CUDA) + # #585: CMake links CUDA code with the STATIC runtime by default, and the targets below also link CUDA::cudart (the + # DLL's import library). MSVC with a CUDA 12.x toolkit - the experimental Pascal/Volta build - then fails with + # LNK1169 (every cudart symbol defined twice); GNU ld lets it pass. That build links the shared runtime alone. The + # ready-made engine (CUDA 13, no STRATA_EXPERIMENTAL_SM60) is unchanged; -DCMAKE_CUDA_RUNTIME_LIBRARY overrides. + if(MSVC AND STRATA_EXPERIMENTAL_SM60 AND NOT DEFINED CMAKE_CUDA_RUNTIME_LIBRARY) + set(CMAKE_CUDA_RUNTIME_LIBRARY Shared) + endif() enable_language(CUDA) # `enable_language(CUDA)` gives the .cu compile rules but NOT the imported targets, so `CUDA::cudart` has to # be brought in explicitly for the host-side tests that call cudaMalloc themselves. @@ -107,13 +169,18 @@ if(STRATA_ENABLE_CUDA) "CUDA 12.8 crashed on long prompts on these cards (issues #220, #224). Use CUDA 13.0 or newer.") endif() endforeach() + if(STRATA_EXPERIMENTAL_SM60) + # every target sees it: the Pascal / Volta code paths (the Volta prompt attention, PR #600) are compiled into this + # build only, so the ready-made engine (sm_75+) keeps exactly the code it had + add_compile_definitions(STRATA_EXPERIMENTAL_SM60=1) + endif() message(STATUS "Strata: CUDA enabled, arch ${CMAKE_CUDA_ARCHITECTURES}") set(_strata_gpu_runtime_target CUDA::cudart) set(_strata_gpu_blas_target CUDA::cublas) set(_strata_gpu_include_directories ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES}) elseif(STRATA_ENABLE_HIP) set(_strata_gpu_runtime_target strata_hip_runtime) - set(_strata_gpu_blas_target roc::hipblas) + set(_strata_gpu_blas_target ${STRATA_HIP_BLAS_TARGETS}) set(_strata_gpu_include_directories ${STRATA_HIP_COMPAT_INCLUDE_DIR}) endif() @@ -309,7 +376,7 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP) src/kernels/cuda/native_flash_attn.cu src/kernels/cuda/native_qsa_indexer.cu src/kernels/cuda/native_qsa_score.cu - PROPERTIES COMPILE_OPTIONS "--use_fast_math") + PROPERTIES COMPILE_OPTIONS "$,,--use_fast_math>") elseif(STRATA_ENABLE_HIP) # Keep Q8_K's multiply and magic-bias rounding as separate FP32 operations on HIP; contraction to an # FMA changes tie results. The embedding gather also relies on separately rounded operations. @@ -348,6 +415,7 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP) # library is the honest shape of the dependency graph: core knows about devices and weights, kernels know # about arithmetic, and this one knows the ORDER the arithmetic happens in. add_library(strata_engine STATIC src/core/layer.cpp src/core/session.cpp src/core/expert_source.cpp src/core/remote_experts.cpp + src/core/remote_expert_opt.cu src/core/expert_cache.cpp src/core/native_head.cpp src/core/native_dense.cpp src/core/verify.cpp src/core/mtp.cpp src/core/conversation_snapshot.cpp src/core/conversation_state.cpp src/core/conversation_memory.cpp src/core/peer_experts.cpp) target_include_directories(strata_engine PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) @@ -384,6 +452,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP) 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) + add_executable(expert_cache_segmented_test tests/core/expert_cache_segmented_test.cpp) + target_link_libraries(expert_cache_segmented_test PRIVATE strata_engine) + add_test(NAME expert_cache_segmented_test COMMAND expert_cache_segmented_test) + set_tests_properties(expert_cache_segmented_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) @@ -427,6 +499,14 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP) if(STRATA_ENABLE_HIP) enable_testing() set(_strata_hip_tests) + add_executable(hip_prefill_gemm tests/hip/prefill_gemm.cpp) + target_link_libraries(hip_prefill_gemm PRIVATE strata_prefill strata_hip_runtime) + add_test(NAME hip_prefill_gemm COMMAND hip_prefill_gemm) + list(APPEND _strata_hip_tests hip_prefill_gemm) + add_executable(hip_q2_zero tests/hip/q2_zero.cpp) + target_link_libraries(hip_q2_zero PRIVATE strata_kernels strata_hip_runtime) + add_test(NAME hip_q2_zero COMMAND hip_q2_zero) + list(APPEND _strata_hip_tests hip_q2_zero) add_executable(hip_expert_cache_staging tests/hip/expert_cache_staging.cpp) target_link_libraries(hip_expert_cache_staging PRIVATE strata_engine) add_test(NAME hip_expert_cache_staging COMMAND hip_expert_cache_staging) @@ -560,6 +640,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP) target_include_directories(mmvq_multi_parity PRIVATE ${_strata_gpu_include_directories}) target_link_libraries(mmvq_multi_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target}) add_test(NAME mmvq_multi_parity COMMAND mmvq_multi_parity) + if(STRATA_HIP_GFX906) # the CUDA layout, where the negative control has power (the wave64 one is exact at every T) + add_test(NAME mmvq_multi_parity_cuda_layout COMMAND mmvq_multi_parity) + set_tests_properties(mmvq_multi_parity_cuda_layout PROPERTIES ENVIRONMENT "STRATA_MMVQ_WAVE=0") + endif() endif() # the IQ kernels that decode a weight part once for every column/entry, bitwise vs the per-column ones # (GPU, synthetic, no model; --bench adds old/new timings) @@ -568,11 +652,16 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP) target_link_libraries(iq_multi_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target}) add_test(NAME iq_multi_parity COMMAND iq_multi_parity) # the prompt path's DeltaNet recurrence: gdn_rec_kh_kernel against gdn_rec_cols_pipe_kernel, bitwise (GPU, - # synthetic, no model; --bench adds timings) - if(STRATA_ENABLE_CUDA) + # synthetic, no model; --bench adds timings). NVIDIA only: cp.async and the CUDA occupancy API (not on gfx906) + if(STRATA_ENABLE_CUDA AND NOT STRATA_HIP_GFX906) add_executable(gdn_rec_parity src/prefill/gdn_rec_parity.cu) target_link_libraries(gdn_rec_parity PRIVATE ${_strata_gpu_runtime_target}) add_test(NAME gdn_rec_parity COMMAND gdn_rec_parity) + # the prompt path's BF16 products against cuBLAS's own BF16 GEMM, within fp32 rounding (sm_7x runs them on the + # FP16 tensor cores; GPU, synthetic, no model; --bench adds timings) + add_executable(gemm_bf16_parity src/prefill/gemm_bf16_parity.cu) + target_link_libraries(gemm_bf16_parity PRIVATE strata_prefill ) + add_test(NAME gemm_bf16_parity COMMAND gemm_bf16_parity) endif() # native_expert_grouped's group stride and fused SwiGLU + q8_1, bitwise vs the launches before them (GPU, synthetic, # no model; --bench adds v1/new timings of a window's 48 calls in a graph) @@ -585,6 +674,9 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP) target_link_libraries(shared_expert_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target}) add_test(NAME shared_expert_parity COMMAND shared_expert_parity --selftest) + add_executable(fused_gr_bench src/kernels/fused_gr_bench.cpp) + target_include_directories(fused_gr_bench PRIVATE ${_strata_gpu_include_directories}) + target_link_libraries(fused_gr_bench PRIVATE strata_kernels ${_strata_gpu_runtime_target}) add_executable(gr_parity src/kernels/gr_parity.cpp) target_include_directories(gr_parity PRIVATE ${_strata_gpu_include_directories}) target_link_libraries(gr_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target}) @@ -629,10 +721,22 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP) target_link_libraries(quantize_act_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target}) add_test(NAME quantize_act_parity COMMAND quantize_act_parity --selftest) + # the verify window's routing in one multi-column BF16 projection + one top-k, bitwise the per-token calls + add_executable(route_window_parity src/kernels/route_window_parity.cpp) + target_include_directories(route_window_parity PRIVATE ${_strata_gpu_include_directories}) + target_link_libraries(route_window_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target}) + add_test(NAME route_window_parity COMMAND route_window_parity) + add_executable(router_top10_parity src/kernels/router_top10_parity.cpp) target_include_directories(router_top10_parity PRIVATE ${_strata_gpu_include_directories}) target_link_libraries(router_top10_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target}) add_test(NAME router_top10_parity COMMAND router_top10_parity --selftest) + if(STRATA_HIP_GFX906) + # the S6 router (STRATA_HIP_ROUTER_FAST=1 on gfx906) against the portable kernel, bitwise, on wave64 + add_executable(hip_router_fast tests/hip/router_fast.cpp) + target_include_directories(hip_router_fast PRIVATE ${_strata_gpu_include_directories}) + target_link_libraries(hip_router_fast PRIVATE strata_kernels ${_strata_gpu_runtime_target}) + endif() add_executable(s_gemv_parity src/kernels/s_gemv_parity.cpp) target_include_directories(s_gemv_parity PRIVATE ${_strata_gpu_include_directories}) @@ -659,6 +763,13 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP) target_link_libraries(qsa_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target}) add_test(NAME qsa_parity COMMAND qsa_parity --selftest) + # the QSA top-k the engine dispatches against the original kernel: identical ids on both sides of the register + # kernel's reach, with tied and with equal scores (synthetic, no model) + add_executable(qsa_topk_parity src/kernels/qsa_topk_parity.cpp) + target_include_directories(qsa_topk_parity PRIVATE ${_strata_gpu_include_directories}) + target_link_libraries(qsa_topk_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target}) + add_test(NAME qsa_topk_parity COMMAND qsa_topk_parity --selftest) + # ---- P2.S4: the PLE n-gram hash, the IQ4_NL table read, and the PLE block ---- # `ngram.cpp` is host-only but lives in `strata_kernels` because `ple.cu` shares its geometry constants, and # splitting them across two libraries would only add a layer. @@ -857,6 +968,22 @@ if(STRATA_NATIVE_EXPERTS) foreach(_f GGML_AVX GGML_AVX2 GGML_FMA GGML_F16C GGML_BMI2) set(${_f} ON CACHE BOOL "" FORCE) endforeach() + elseif(STRATA_ISA_FLOOR) + # the experimental older-CPU build (STRATA_ISA_FLOOR above): one ggml-cpu for the floor, ggml's "sandybridge" + # feature set (SSE4.2 + AVX) or SSE4.2 alone - not the build host's, which may be newer than the PC it runs on. + # From the Strata_Dirigo fork (rwkeyes). ggml's defaults turn the rest on when GGML_NATIVE is off: all off here. + set(GGML_NATIVE OFF CACHE BOOL "" FORCE) + set(GGML_CPU_ALL_VARIANTS OFF CACHE BOOL "" FORCE) + set(GGML_SSE42 ON CACHE BOOL "" FORCE) + if(STRATA_ISA_FLOOR STREQUAL "avx") + set(GGML_AVX ON CACHE BOOL "" FORCE) + else() + set(GGML_AVX OFF CACHE BOOL "" FORCE) + endif() + foreach(_f GGML_AVX2 GGML_FMA GGML_F16C GGML_BMI2 GGML_AVX_VNNI GGML_AVX512 GGML_AVX512_VBMI GGML_AVX512_VNNI + GGML_AVX512_BF16 GGML_AMX_TILE GGML_AMX_INT8 GGML_AMX_BF16) + set(${_f} OFF CACHE BOOL "" FORCE) + endforeach() else() set(GGML_NATIVE ON CACHE BOOL "" FORCE) endif() @@ -877,20 +1004,36 @@ if(STRATA_NATIVE_EXPERTS) # part of strata_kernels_cpu (the pool calls these and they call its CPU probe: one library, so GNU ld's # one-pass static linking has no cycle to resolve) target_sources(strata_kernels_cpu PRIVATE src/kernels/cpu/native_expert.cpp src/kernels/cpu/iq_avx512.cpp - src/kernels/cpu/iq_avx2.cpp src/kernels/cpu/kq_avx2.cpp) + src/kernels/cpu/iq_avx2.cpp src/kernels/cpu/kq_avx2.cpp + src/kernels/cpu/kq_avx1.cpp) target_include_directories(strata_kernels_cpu PRIVATE third_party/ggml) if(MSVC) set_source_files_properties(src/kernels/cpu/iq_avx512.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX512") set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2") set_source_files_properties(src/kernels/cpu/kq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2") + # the router lookahead's AVX1 dot (older CPUs; called behind cpu_avx1_ok): no FMA, no 256-bit integer ops + set_source_files_properties(src/kernels/cpu/kq_avx1.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX") else() set_source_files_properties(src/kernels/cpu/iq_avx512.cpp PROPERTIES COMPILE_OPTIONS "-mavx512f;-mavx512bw;-mavx512vl;-mavx512dq;-mavx512vnni;-mavx512vbmi;-mf16c;-mfma") set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c") set_source_files_properties(src/kernels/cpu/kq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c") + set_source_files_properties(src/kernels/cpu/kq_avx1.cpp PROPERTIES COMPILE_OPTIONS "-mavx") + endif() + if(STRATA_ISA_FLOOR STREQUAL "avx") + target_compile_definitions(strata_kernels_cpu PRIVATE STRATA_ISA_FLOOR_AVX=1) + elseif(STRATA_ISA_FLOOR STREQUAL "none") + target_compile_definitions(strata_kernels_cpu PRIVATE STRATA_ISA_FLOOR_NONE=1) endif() target_link_libraries(strata_kernels_cpu PUBLIC ggml-cpu ggml-base) target_compile_definitions(strata_kernels_cpu PUBLIC STRATA_NATIVE_EXPERTS=1) + # the router lookahead's dot on each rung this CPU has (AVX2, the older CPUs' AVX1) against a double reference + add_executable(router_dot_parity src/kernels/cpu/router_dot_parity.cpp) + target_link_libraries(router_dot_parity PRIVATE strata_kernels_cpu) + if(STRATA_BUILD_TESTS) + add_test(NAME router_dot_parity COMMAND router_dot_parity) + add_test(NAME router_dot_parity_avx COMMAND ${CMAKE_COMMAND} -E env STRATA_FORCE_ISA=avx $) + endif() 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) @@ -1003,6 +1146,9 @@ if(STRATA_NATIVE_EXPERTS) add_executable(native_expert_parity src/kernels/native_expert_parity.cpp) target_include_directories(native_expert_parity PRIVATE ${_strata_gpu_include_directories}) target_link_libraries(native_expert_parity PRIVATE strata_kernels strata_kernels_cpu strata_artifact ${_strata_gpu_runtime_target}) + add_executable(native_expert_bench src/kernels/native_expert_bench.cpp) + target_include_directories(native_expert_bench PRIVATE ${_strata_gpu_include_directories}) + target_link_libraries(native_expert_bench PRIVATE strata_kernels strata_kernels_cpu strata_artifact ${_strata_gpu_runtime_target}) if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA) # Unsloth UD-Q4_K_XL's three expert pairs (and Q8_0 alone, its embedding) on synthetic ggml-quantized weights # at 2560/640: GPU and CPU experts against the float reference, GPU dequantizers against to_float. @@ -1043,6 +1189,15 @@ if(STRATA_ENABLE_CUDA AND STRATA_BUILD_TESTS AND EXISTS "${CMAKE_CURRENT_SOURCE_ endif() if(STRATA_BUILD_TESTS) + if(WIN32) + add_executable(pool_affinity_test src/kernels/cpu/pool_affinity_test.cpp) + target_link_libraries(pool_affinity_test PRIVATE strata_kernels_cpu) + add_test(NAME pool_affinity_test COMMAND pool_affinity_test) + elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_executable(pool_affinity_test src/kernels/cpu/pool_affinity_linux_test.cpp) + target_link_libraries(pool_affinity_test PRIVATE strata_kernels_cpu) + add_test(NAME pool_affinity_test COMMAND pool_affinity_test) + endif() # Plan v0.3 P6: one expert applied to several tokens, bitwise vs the single-token kernel (synthetic, CPU only). # plan v0.3 P5: the device dequantizers vs the artifact's CPU dequantizers on the real shards (not a ctest: needs # the 38 GB model; run it by hand or from tools/gpu_session.py) @@ -1085,3 +1240,15 @@ if(STRATA_BUILD_TESTS) add_subdirectory(tests) endif() endif() + +# ---- HIP gfx906: every CUDA source of the tree (and the ggml-cuda MMQ sources) is compiled as HIP ---- +if(STRATA_HIP_GFX906) + file(GLOB_RECURSE _strata_cu CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cu ${CMAKE_CURRENT_SOURCE_DIR}/bench/micro/*.cu + ${CMAKE_CURRENT_SOURCE_DIR}/tools/vision/*.cu) + if(_strata_mmq) + file(GLOB _strata_mmq_cu ${_strata_mmq}/*.cu ${_strata_mmq}/template-instances/*.cu) + list(APPEND _strata_cu ${_strata_mmq_cu}) + target_compile_definitions(strata_mmq PRIVATE GGML_USE_HIP) + endif() + set_source_files_properties(${_strata_cu} PROPERTIES LANGUAGE HIP) +endif() diff --git a/README.de.md b/README.de.md new file mode 100644 index 0000000000..e400395e2a --- /dev/null +++ b/README.de.md @@ -0,0 +1,221 @@ +

Strata

+ +[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · **Deutsch** · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md) + +

Ein KI-Modell mit 125 Milliarden Parametern auf deinem eigenen Gaming-PC
+NVIDIA- oder AMD-Grafikkarte (ab 12 GB) · Windows oder Linux · kostenlos und Open Source

+ +

Ein Voxel-Pagodengarten, den das Modell in Strata geschrieben hat, läuft im Browser
+Ein Voxel-Pagodengarten aus einem einzigen Prompt, auf einer RTX 5070 mit Strata (IQ3_S, 128K Kontext) · +ganzes Video (49 s)

+ +Strata lässt **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** auf einem normalen PC laufen. +Das ist ein großes, kluges KI-Modell, das sonst meist einen Server braucht. Es chattet, schreibt Code, versteht +Bilder und arbeitet mit deinen Apps und Coding-Agents zusammen. Nichts verlässt deinen PC. + +## Wie schnell ist es? + +Wir haben es auf zwei ganz normalen Gaming-PCs gemessen. Ein Token ist etwa ¾ eines Wortes. + +- **Schreibt Antworten:** wie schnell die Antwort in einem kurzen Chat erscheint. 60 Tokens pro Sekunde sind + schneller, als du lesen kannst. +- **Liest deinen Prompt:** wie schnell es aufnimmt, was du schickst (hier ein Dokument, Code oder Chatverlauf mit + 32K Tokens). + + + + +
NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAMAMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM
+ +| Größe | Schreibt Antworten | Liest deinen 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 | + + + +| Größe | Schreibt Antworten | Liest deinen 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 | + +
+ +NVIDIA: Q2_0 mit Engine 0.1.36, die anderen Zeilen mit 0.1.26 (4K Antworten, 32K Prompts). Die vollständigen +Tabellen stehen in [DETAILS.md](docs/DETAILS.md#speed-measured). Eine Karte mit mehr VRAM ist schneller: Eine +RTX 3090 (24 GB) sollte etwa 100-140 Tokens pro Sekunde schreiben. Lange Chats und andere Karten: +[Tempo jedes Modells](docs/MODELS.md#how-fast-is-each-size), [Ergebnisse aus der Community](docs/COMMUNITY_BENCHMARKS.md). + +

Buy Me A Coffee
+Strata ist kostenlos. Wenn es auf deinem PC gut läuft, hält ein Kaffee die Arbeit daran am Laufen.

+ +## Was du brauchst + +| | | +| --- | --- | +| **Grafikkarte** | **NVIDIA** GeForce RTX 20, 30, 40 oder 50 Serie, oder **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 oder RX 6800 / 6900 Serie. Sie braucht **12 GB VRAM oder mehr**. | +| **RAM** | 32 GB oder mehr. Dein RAM entscheidet, [welches Modell](#welches-modell-soll-ich-nehmen) passt. Mit 64 GB laufen alle Größen. | +| **Festplatte** | Etwa 80 GB frei. Nimm wenn möglich eine SSD: Der erste Start geht dann viel schneller. | +| **System** | Windows 10 / 11 oder Linux und ein aktueller Grafiktreiber von NVIDIA oder AMD. | + +Alles andere richtet der Installer ein. Zwei oder drei Karten können sich das Modell teilen ([Multi-GPU](docs/MULTI_GPU.md)). + +Experimentell, von Community-Mitgliedern auf ihren eigenen Rechnern geschrieben und getestet: + +- **Ältere Grafikkarten** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Ältere GPUs](docs/OLDER_GPUS.md). +- **Intel Arc**, unter Linux aus dem Quellcode gebaut: [Intel Arc](docs/INTEL_ARC.md). +- **Ältere Prozessoren ohne AVX2**: Sie funktionieren, aber langsam. [Ältere CPUs](docs/INSTALL.md#older-cpus-experimental). + +Die vollständige Liste: [docs/INSTALL.md](docs/INSTALL.md#what-you-need). + +## Installieren + +### Lass deine KI es einrichten + +Nutzt du einen KI-Coding-Assistenten (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Füge dort diesen Text ein: + +```text +Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository. +``` + +Er prüft deine Grafikkarte, deinen RAM und deine Festplatte und wählt das passende Modell. Dann installiert und +startet er es und sagt dir, wie du deine Apps verbindest. KI-Tools können Strata auch über seinen +[MCP-Server](docs/MCP_SERVER.md) installieren, starten und stoppen. + +### Oder mach es selbst + +[Lade Strata herunter](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) und entpacke es (oder nutze +`git clone`). **Windows:** Doppelklick auf **`START-HERE.bat`**. **Linux:** Führe im Strata-Ordner **`./setup.sh`** aus. + +Die Schritte sind für NVIDIA und AMD gleich. Der Installer erkennt deine Karte und richtet die passende Engine +dafür ein. Er stellt dir ein paar Fragen: + +- welches Modell und welche Größe, +- wie viel Kontext (wie viel Text sich das Modell merkt), +- ob es Bilder lesen soll. + +Drück jedes Mal Enter, um die empfohlene Antwort zu nehmen. Dann lädt er das Modell herunter (etwa 70 GB) und +startet es. Bricht der Download ab, starte ihn einfach neu: Er macht dort weiter, wo er aufgehört hat. Dein Browser +öffnet die Strata-App unter `http://127.0.0.1:8080`. + +> **Während das Modell startet, kann dein PC 1-3 Minuten langsam sein oder nicht reagieren** (beim ersten Mal am +> längsten). Strata lädt 35-55 GB in deinen RAM und reserviert einen Teil davon für die Grafikkarte. Das ist normal. +> Warte und schließ das Fenster nicht. Im Fenster siehst du, was Strata gerade tut. + +**Beim nächsten Mal** startest du wieder `START-HERE.bat` (oder `./setup.sh`). Es startet sofort und lädt nichts +doppelt herunter. Schließ das Fenster, um das Modell zu stoppen. `UPDATE.bat` (`./update.sh`) aktualisiert Strata, +ohne es zu starten. Updates, Docker, mehrere Karten, wo die Dateien landen und alle Optionen: +[docs/INSTALL.md](docs/INSTALL.md). + +## Welches Modell soll ich nehmen? + +Der Installer empfiehlt eins passend zu deinem RAM. Dasselbe Modell gibt es in mehreren Größen, mehr oder weniger +stark komprimiert. Kleinere Größen sind schneller. Größere sind etwas klüger. + +| Dein RAM | Nimm | Warum | +| --- | --- | --- | +| **32 GB** | **Coder** | passt in 32 GB und ist für Code gemacht (mit einer 24-GB-Karte laufen auch Q2_0 und IQ2_XS) | +| **48 GB** | **IQ2_XS** (oder Q2_0, das schnellste) | die größeren Größen passen nicht | +| **64 GB** | **IQ2_XS** (empfohlen) oder IQ3_XXS / IQ3_S | alle Größen passen; IQ3_S ist das beste und das langsamste | +| **96 GB oder mehr** | **IQ3_S** oder Unsloths UD-IQ4_XS (~4 Bit) | Platz für die größten Größen, auch wenn alles andere offen ist | + +- **[Coder](docs/MODELS.md#coder):** eine Version für Code, bei der die Hälfte der Experten entfernt wurde. Sie + erreicht 91 % des SWE-bench-Verified-Werts des vollen Modells (von ihren Autoren gemessen) und passt in 32 GB RAM. + Außerhalb von Code ist sie schwächer, auch bei chinesischem und anderem CJK-Text (#438). Nimm dafür Q2_0, IQ2_XS + oder IQ3_S, die alle Experten behalten. +- **[Swift 1.5](docs/MODELS.md#swift-15):** ein Fine-Tune, das viel kürzer nachdenkt, bevor es antwortet. Du + bekommst die Antwort früher, bei etwa gleicher Qualität. +- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** die ~4-Bit-Version von Unsloth, in der Qualität + zwischen IQ3_S und UD-Q4_K_XL. 94 GB Download. Mit weniger als ~80 GB RAM liest Strata einen Teil davon während + der Antwort von der SSD, dort ist es also langsamer (eine NVMe-SSD hilft). +- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimentell): am nächsten am vollen + Modell. Aber Strata liest das meiste davon während der Antwort von der SSD, deshalb schreibt es auf einem PC mit + 64 GB nur 7-8.5 Tokens/s. +- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** Das richtest du von Hand + ein. Es steht nicht im Menü des Installers. + +Größen, Downloads und was wohin passt: [docs/MODELS.md](docs/MODELS.md). Um später ein weiteres Modell +hinzuzufügen, starte `SETUP.bat` (Linux: `./setup.sh --setup`). + +## So benutzt du es + +

Der Monitor-Tab der Strata-App neben einem Coding-Agent
+Der Monitor der Strata-App (links), während ein Coding-Agent den Pagodengarten aus dem Video schreibt (rechts)

+ +- **Im Browser:** Öffne `http://127.0.0.1:8080`. Dort gibt es **Chat**, einen Live-**Monitor** für das Modell und + deine GPU/CPU/RAM und **About** mit den Einstellungen und Adressen. +- **Deine Apps und Coding-Agents:** Füge einen „OpenAI-kompatiblen“ Anbieter mit der Basis-URL + **`http://127.0.0.1:8080/v1`** hinzu. Jeder API-Key und jeder Modellname funktioniert. + - Apps, die die API von Anthropic nutzen: `http://127.0.0.1:8080/v1/messages` (Claude Code: + `ANTHROPIC_BASE_URL=http://127.0.0.1:8080`). + - Codex CLI und andere Apps, die die OpenAI Responses API nutzen: `/v1/responses` + ([Einrichtung](docs/DETAILS.md#the-responses-api-and-codex-cli)). +- **Nachdenken:** Wähle **aus, niedrig, mittel oder hoch** im Chat-Menü oder unter „Reasoning Effort“ in deiner + App. Aus ist am schnellsten. Hoch ist am besten für schwierige Fragen. +- **Bilder:** Antworte im Setup mit Ja auf „Images?“. Klick dann im Chat auf **Picture** oder hänge Bilder in deiner + App an. AMD-Karten lesen Bilder unter Linux über den Prozessor; unter Windows geht das noch nicht. +- **Vom Handy oder einem anderen PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key `. Setz immer einen + Key. +- **Eine Anfrage nach der anderen:** Standardmäßig beantwortet Strata eine Anfrage, die anderen warten. Um mehrere + gleichzeitig zu beantworten, setz `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Auf einer 12-GB-Karte wird + dadurch jede Antwort langsamer. +- **Lange Prompts:** Strata liest die erste Nachricht eines Chats komplett, etwa 1 Minute pro 30,000 Tokens. + Folgenachrichten starten in Sekunden. + +Mehr: [wo deine Chats gespeichert werden](docs/INSTALL.md#where-things-are-stored), [die API](docs/DETAILS.md#using-it). + +## Etwas ist schiefgelaufen? + +- **Mein PC ist beim ersten Start von Strata eingefroren.** Das ist normal, während das Modell lädt. Warte und + schließ das Fenster nicht. Nach 10 Minuten immer noch eingefroren? Starte den PC neu, schließ andere Programme und + versuch es noch einmal, oder nimm eine kleinere Größe. +- **Es hat beim Herunterladen oder Installieren aufgehört.** Starte `START-HERE.bat` (oder `./setup.sh`) noch + einmal. Es macht dort weiter, wo es aufgehört hat. +- **Es ist sehr langsam und die Festplattenlampe blinkt ständig, oder es meldet „the engine stopped + unexpectedly“.** Dein PC hat nicht genug freien RAM. Schließ andere Programme (Browser brauchen viel) oder nimm + eine kleinere Größe (Q2_0 oder IQ2_XS). +- **Es meldet, dass Port 8080 schon belegt ist.** Strata läuft bereits. Such nach seinem Fenster. + +Weitere Probleme und ihre Lösungen: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Kommst du nicht weiter? +Öffne ein [Issue](https://github.com/Niko1221/Strata/issues) und hänge `strata-.log` aus dem Strata-Ordner +an. Du hast ein Sicherheitsproblem gefunden? Melde es vertraulich: [SECURITY.md](SECURITY.md). + +## Wie funktioniert es? + +Modelle wie dieses laufen normalerweise auf Servern mit Hunderten Gigabyte Grafikspeicher. Deine Grafikkarte hat +12-24 GB. Strata bringt das Modell trotzdem unter, indem es **die Arbeit auf deinen ganzen PC verteilt**. Stell dir +eine Küche vor: Was du ständig brauchst, bleibt auf der Arbeitsfläche, der Rest wartet in der Vorratskammer. + +

Die 24,576 Experten des Modells: die meistgenutzten auf der Grafikkarte, alle im RAM, eine Nachschlagetabelle auf der SSD

+ +- **Das Modell ist ein Team aus 24,576 kleinen Spezialisten („Experten“).** Jedes Wort braucht nur 10 davon. +- **Deine Grafikkarte** hält die paar tausend Experten, die am häufigsten gebraucht werden. **Dein RAM** hält alle, + und **dein Prozessor** rechnet gleichzeitig am Rest. **Deine SSD** hält eine große Nachschlagetabelle. + +

Ein kleiner Helfer rät die nächsten Wörter; das große Modell prüft alle auf einmal und behält die richtigen

+ +- **Raten, dann prüfen:** Ein kleiner Helfer rät die nächsten paar Wörter. Das große Modell prüft sie alle auf + einmal. Du bekommst dieselbe Antwort, nur 1.6-1.8x schneller. +- **Lange Texte werden in großen Stücken gelesen** (bis zu 8,192 Tokens auf einmal), mit über 1,000 Tokens pro + Sekunde. + +Die längere Erklärung: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Jeder Teil und seine Zahlen: +[die Details](docs/DETAILS.md#how-it-works) und das [Paper](docs/paper/Strata-Paper.pdf). + +## Danksagung und Lizenz + +Das Modell ist [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) vom Qwen-Team. Komprimiert +wurde es von [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5) +und Unsloth. Strata nutzt Teile von [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Alle Danksagungen: +[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata ist Open Source unter der [MIT-Lizenz](LICENSE). Ein +paar Teile und alle Modelle haben eigene Lizenzen ([welche](docs/HOW_IT_WORKS.md#license)). + +## Strata unterstützen + +Strata ist kostenlos und Open Source. Wenn es dir nützt, kannst du die Entwicklung unterstützen: + +

Buy Me A Coffee

diff --git a/README.es.md b/README.es.md new file mode 100644 index 0000000000..936c13de10 --- /dev/null +++ b/README.es.md @@ -0,0 +1,215 @@ +

Strata

+ +[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · **Español** · [Português](README.pt-BR.md) + +

Ejecuta un modelo de IA de 125 mil millones de parámetros en tu propio PC gaming
+Tarjeta gráfica NVIDIA o AMD (12 GB o más) · Windows o Linux · libre y de código abierto

+ +

Un jardín con una pagoda de vóxeles que escribió el modelo de Strata, funcionando en el navegador
+Un jardín con una pagoda de vóxeles, hecho con un solo prompt en una RTX 5070 con Strata (IQ3_S, contexto de 128K) · +vídeo completo (49 s)

+ +Strata ejecuta **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** en un PC normal. Es un +modelo de IA grande e inteligente que normalmente necesita un servidor. Conversa, escribe código, entiende imágenes +y funciona con tus aplicaciones y agentes de programación. Nada sale de tu PC. + +## ¿Qué velocidad tiene? + +Lo medimos en dos PC gaming normales. Un token equivale más o menos a ¾ de una palabra. + +- **Escribe respuestas:** lo rápido que aparece la respuesta en un chat corto. 60 tokens por segundo es más rápido de lo que puedes leer. +- **Lee tu prompt:** lo rápido que procesa lo que le envías (aquí, un documento, código o historial de chat de 32K tokens). + + + + +
NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAMAMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM
+ +| Tamaño | Escribe respuestas | Lee tu 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 | + + + +| Tamaño | Escribe respuestas | Lee tu 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 | + +
+ +NVIDIA: Q2_0 con el motor 0.1.36, las demás filas con 0.1.26 (respuestas de 4K, prompts de 32K). Las tablas +completas están en [DETAILS.md](docs/DETAILS.md#speed-measured). Una tarjeta con más VRAM es más rápida: una +RTX 3090 (24 GB) debería escribir unos 100-140 tokens por segundo. Chats largos y otras tarjetas: +[velocidad de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados de la comunidad](docs/COMMUNITY_BENCHMARKS.md). + +

Buy Me A Coffee
+Strata es gratis. Si funciona bien en tu PC, un café ayuda a seguir trabajando en él.

+ +## Qué necesitas + +| | | +| --- | --- | +| **Tarjeta gráfica** | **NVIDIA** GeForce serie RTX 20, 30, 40 o 50, o **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 o serie RX 6800 / 6900. Necesita **12 GB de VRAM o más**. | +| **RAM** | 32 GB o más. Tu RAM decide [qué modelo](#qué-modelo-elijo) cabe. Con 64 GB funcionan todos los tamaños. | +| **Disco** | Unos 80 GB libres. Usa un SSD si puedes: el primer arranque es mucho más rápido. | +| **Sistema** | Windows 10 / 11 o Linux, y un controlador gráfico actual de NVIDIA o AMD. | + +El instalador prepara todo lo demás. Dos o tres tarjetas pueden repartirse el modelo ([multi-GPU](docs/MULTI_GPU.md)). + +Experimental, escrito y probado por miembros de la comunidad en sus propios equipos: + +- **Tarjetas gráficas antiguas** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md). +- **Intel Arc**, compilado desde el código fuente en Linux: [Intel Arc](docs/INTEL_ARC.md). +- **Procesadores antiguos sin AVX2**: funcionan, pero despacio. [Older CPUs](docs/INSTALL.md#older-cpus-experimental). + +La lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need). + +## Instalación + +### Deja que tu IA lo instale + +¿Usas un asistente de programación con IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Pega esto en él: + +```text +Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository. +``` + +Revisa tu tarjeta gráfica, tu RAM y tu disco, y elige el modelo que cabe. Luego lo instala, lo arranca y te explica +cómo conectar tus aplicaciones. Las herramientas de IA también pueden instalar, arrancar y detener Strata con su +[servidor MCP](docs/MCP_SERVER.md). + +### O hazlo tú mismo + +[Descarga Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) y descomprímelo (o usa `git clone`). +**Windows:** haz doble clic en **`START-HERE.bat`**. **Linux:** ejecuta **`./setup.sh`** en la carpeta de Strata. + +Los pasos son los mismos para NVIDIA y AMD. El instalador detecta tu tarjeta y prepara el motor adecuado para ella. +Te hace unas pocas preguntas: + +- qué modelo y qué tamaño, +- cuánto contexto (cuánto texto tiene presente el modelo), +- si debe entender imágenes. + +Pulsa Enter cada vez para usar la respuesta recomendada. Después descarga el modelo (unos 70 GB) y lo arranca. Si la +descarga se detiene, vuelve a ejecutarlo: sigue donde lo dejó. Tu navegador abre la aplicación de Strata en +`http://127.0.0.1:8080`. + +> **Mientras el modelo arranca, tu PC puede ir lento o dejar de responder durante 1-3 minutos** (más la primera vez). +> Strata carga 35-55 GB en tu RAM y reserva una parte para la tarjeta gráfica. Es normal. Espera y no cierres la +> ventana. La ventana muestra lo que está haciendo Strata. + +**La próxima vez**, vuelve a ejecutar `START-HERE.bat` (o `./setup.sh`). Arranca enseguida y no descarga nada dos +veces. Cierra su ventana para detener el modelo. `UPDATE.bat` (`./update.sh`) actualiza Strata sin arrancarlo. +Actualizaciones, Docker, varias tarjetas, dónde se guardan los archivos y todas las opciones: [docs/INSTALL.md](docs/INSTALL.md). + +## ¿Qué modelo elijo? + +El instalador te recomienda uno según tu RAM. El mismo modelo viene en varios tamaños, más o menos comprimidos. Los +tamaños pequeños son más rápidos. Los grandes son algo más inteligentes. + +| Tu RAM | Elige | Por qué | +| --- | --- | --- | +| **32 GB** | **Coder** | cabe en 32 GB y está hecho para programar (con una tarjeta de 24 GB también funcionan Q2_0 e IQ2_XS) | +| **48 GB** | **IQ2_XS** (o Q2_0, el más rápido) | los tamaños más grandes no caben | +| **64 GB** | **IQ2_XS** (recomendado), o IQ3_XXS / IQ3_S | caben todos los tamaños; IQ3_S es el mejor y el más lento | +| **96 GB o más** | **IQ3_S**, o el UD-IQ4_XS de Unsloth (~4 bits) | espacio para los tamaños más grandes con todo lo demás abierto | + +- **[Coder](docs/MODELS.md#coder):** una versión para programar a la que se le quitó la mitad de los expertos. Alcanza + el 91% de la puntuación SWE-bench Verified del modelo completo (medido por sus autores) y cabe en 32 GB de RAM. Es + más flojo fuera del código, también con chino y otros textos CJK (#438). Para eso, elige Q2_0, IQ2_XS o IQ3_S, que + conservan todos los expertos. +- **[Swift 1.5](docs/MODELS.md#swift-15):** un ajuste fino que piensa mucho menos tiempo antes de responder. Recibes + la respuesta antes, con más o menos la misma calidad. +- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** la versión de ~4 bits de Unsloth, entre IQ3_S y + UD-Q4_K_XL en calidad. Una descarga de 94 GB. Con menos de ~80 GB de RAM, Strata lee una parte desde el SSD + mientras responde, así que ahí es más lento (un SSD NVMe ayuda). +- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental): el más parecido al modelo + completo. Pero Strata lee la mayor parte desde el SSD mientras responde, así que solo escribe 7-8.5 tokens/s en un + PC con 64 GB. +- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** se instala a mano. No está en + el menú del instalador. + +Tamaños, descargas y qué cabe dónde: [docs/MODELS.md](docs/MODELS.md). Para añadir otro modelo más adelante, ejecuta +`SETUP.bat` (Linux: `./setup.sh --setup`). + +## Cómo usarlo + +

La pestaña Monitor de la aplicación de Strata junto a un agente de programación
+El Monitor de la aplicación de Strata (izquierda) mientras un agente de programación escribe el jardín de la pagoda del vídeo (derecha)

+ +- **En el navegador:** abre `http://127.0.0.1:8080`. Tiene **Chat**, un **Monitor** en vivo del modelo y de tu + GPU/CPU/RAM, y **About** con los ajustes y las direcciones. +- **Tus aplicaciones y agentes de programación:** añade un proveedor "compatible con OpenAI" con la URL base + **`http://127.0.0.1:8080/v1`**. Sirve cualquier clave de API y cualquier nombre de modelo. + - Aplicaciones que usan la API de Anthropic: `http://127.0.0.1:8080/v1/messages` (Claude Code: + `ANTHROPIC_BASE_URL=http://127.0.0.1:8080`). + - Codex CLI y otras aplicaciones que usan la API Responses de OpenAI: `/v1/responses` + ([configuración](docs/DETAILS.md#the-responses-api-and-codex-cli)). +- **Razonamiento:** elige **off, low, medium o high** en el menú del chat o en el "reasoning effort" de tu aplicación. + Off es lo más rápido. High es lo mejor para preguntas difíciles. +- **Imágenes:** responde que sí a "Images?" en la instalación. Luego haz clic en **Picture** en el chat, o adjunta + imágenes en tu aplicación. Las tarjetas AMD leen imágenes en Linux a través del procesador; en Windows todavía no. +- **Desde tu móvil u otro PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key `. Pon siempre una clave. +- **Una petición cada vez:** por defecto Strata responde una petición y las demás esperan. Para responder varias a la + vez, pon `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, cada respuesta va más lenta. +- **Prompts largos:** Strata lee entero el primer mensaje de un chat, más o menos 1 minuto por cada 30,000 tokens. Los + mensajes siguientes empiezan en segundos. + +Más: [dónde se guardan tus chats](docs/INSTALL.md#where-things-are-stored), [la API](docs/DETAILS.md#using-it). + +## ¿Algo salió mal? + +- **Mi PC se congeló la primera vez que arrancó Strata.** Es normal mientras carga el modelo. Espera y no cierres la + ventana. ¿Sigue congelado después de 10 minutos? Reinicia el PC, cierra otros programas y vuelve a intentarlo, o + elige un tamaño más pequeño. +- **Se detuvo durante la descarga o la instalación.** Vuelve a ejecutar `START-HERE.bat` (o `./setup.sh`). Sigue + donde se detuvo. +- **Va muy lento y la luz del disco no deja de parpadear, o dice "the engine stopped unexpectedly".** Tu PC no tiene + suficiente RAM libre. Cierra otros programas (los navegadores usan mucha), o elige un tamaño más pequeño (Q2_0 o + IQ2_XS). +- **Dice que el puerto 8080 ya está en uso.** Strata ya está en marcha. Busca su ventana. + +Más problemas y sus soluciones: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). ¿Sigues atascado? Abre un +[issue](https://github.com/Niko1221/Strata/issues) y adjunta `strata-.log` de la carpeta de Strata. ¿Has +encontrado un problema de seguridad? Infórmalo en privado: [SECURITY.md](SECURITY.md). + +## ¿Cómo funciona? + +Los modelos como este suelen funcionar en servidores con cientos de gigabytes de memoria gráfica. Tu tarjeta gráfica +tiene 12-24 GB. Strata hace que el modelo quepa **repartiendo el trabajo por todo tu PC**. Piensa en una cocina: lo +que usas todo el rato se queda en la encimera, y el resto espera en la despensa. + +

Los 24,576 expertos del modelo: los más usados en la tarjeta gráfica, todos en la RAM y una tabla de consulta en el SSD

+ +- **El modelo es un equipo de 24,576 pequeños especialistas ("expertos").** Cada palabra solo necesita 10 de ellos. +- **Tu tarjeta gráfica** guarda los pocos miles de expertos que más se usan. **Tu RAM** los tiene todos, y **tu + procesador** trabaja con el resto al mismo tiempo. **Tu SSD** guarda una gran tabla de consulta. + +

Un pequeño ayudante adivina las siguientes palabras; el modelo grande las comprueba todas a la vez y se queda con las correctas

+ +- **Adivinar y comprobar:** un pequeño ayudante adivina las siguientes palabras. El modelo grande las comprueba todas + a la vez. Recibes la misma respuesta, 1.6-1.8x antes. +- **Los textos largos se leen en trozos grandes** (hasta 8,192 tokens cada vez), a más de 1,000 tokens por segundo. + +La explicación larga: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte y sus números: +[los detalles](docs/DETAILS.md#how-it-works) y el [artículo](docs/paper/Strata-Paper.pdf). + +## Créditos y licencia + +El modelo es [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), del equipo de Qwen. Lo +comprimieron [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5) +y Unsloth. Strata usa partes de [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos los créditos: +[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata es de código abierto con la [licencia MIT](LICENSE). +Algunas partes y cada modelo tienen sus propias licencias ([cuáles](docs/HOW_IT_WORKS.md#license)). + +## Apoya a Strata + +Strata es libre y de código abierto. Si te resulta útil, puedes apoyar su desarrollo: + +

Buy Me A Coffee

diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 0000000000..22faf7d894 --- /dev/null +++ b/README.fr.md @@ -0,0 +1,220 @@ +

Strata

+ +[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · **Français** · [Español](README.es.md) · [Português](README.pt-BR.md) + +

Faites tourner un modèle d'IA de 125 milliards de paramètres sur votre propre PC de jeu
+Carte graphique NVIDIA ou AMD (12 Go ou plus) · Windows ou Linux · gratuit et open source

+ +

Un jardin de pagode en voxels écrit par le modèle de Strata, qui tourne dans le navigateur
+Un jardin de pagode en voxels, généré en une seule requête sur une RTX 5070 avec Strata (IQ3_S, contexte de 128K) · +vidéo complète (49 s)

+ +Strata fait tourner **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** sur un PC normal. C'est +un grand modèle d'IA, très capable, qui demande d'habitude un serveur. Il discute, écrit du code, lit des images et +fonctionne avec vos applications et vos agents de code. Rien ne quitte votre PC. + +## Quelle est sa vitesse ? + +Nous l'avons mesurée sur deux PC de jeu ordinaires. Un token correspond à environ ¾ d'un mot. + +- **Écrit les réponses :** la vitesse à laquelle la réponse s'affiche dans une courte discussion. 60 tokens par seconde, c'est plus rapide que votre lecture. +- **Lit votre prompt :** la vitesse à laquelle il absorbe ce que vous envoyez (ici un document, du code ou un historique de discussion de 32K tokens). + + + + +
NVIDIA : RTX 5070 (12 Go), Ryzen 5 7600, 64 Go de RAMAMD : RX 9070 XT (16 Go), Ryzen 9 3900X, 47 Go de RAM
+ +| Taille | Écrit les réponses | Lit votre 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 | + + + +| Taille | Écrit les réponses | Lit votre 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 | + +
+ +NVIDIA : Q2_0 avec le moteur 0.1.36, les autres lignes avec 0.1.26 (réponses de 4K, prompts de 32K). Les tableaux +complets sont dans [DETAILS.md](docs/DETAILS.md#speed-measured). Une carte avec plus de VRAM va plus vite : une +RTX 3090 (24 Go) devrait écrire environ 100-140 tokens par seconde. Longues discussions et autres cartes : +[vitesse de chaque modèle](docs/MODELS.md#how-fast-is-each-size), [résultats de la communauté](docs/COMMUNITY_BENCHMARKS.md). + +

Buy Me A Coffee
+Strata est gratuit. S'il tourne bien sur votre PC, un café aide à poursuivre le travail.

+ +## Ce qu'il vous faut + +| | | +| --- | --- | +| **Carte graphique** | **NVIDIA** GeForce RTX série 20, 30, 40 ou 50, ou **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 ou série RX 6800 / 6900. Il faut **12 Go de VRAM ou plus**. | +| **RAM** | 32 Go ou plus. Votre RAM décide [quel modèle](#quel-modèle-choisir-) tient. Avec 64 Go, toutes les tailles tournent. | +| **Disque** | Environ 80 Go libres. Prenez un SSD si possible : le premier démarrage est bien plus rapide. | +| **Système** | Windows 10 / 11 ou Linux, avec un pilote graphique NVIDIA ou AMD à jour. | + +L'installateur s'occupe de tout le reste. Deux ou trois cartes peuvent se partager le modèle ([multi-GPU](docs/MULTI_GPU.md)). + +Expérimental, écrit et testé par des membres de la communauté sur leurs propres machines : + +- **Cartes graphiques plus anciennes** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT) : [Older GPUs](docs/OLDER_GPUS.md). +- **Intel Arc**, compilé depuis les sources sous Linux : [Intel Arc](docs/INTEL_ARC.md). +- **Processeurs plus anciens sans AVX2** : ça marche, mais lentement. [Older CPUs](docs/INSTALL.md#older-cpus-experimental). + +La liste complète : [docs/INSTALL.md](docs/INSTALL.md#what-you-need). + +## Installation + +### Laissez votre IA l'installer + +Vous utilisez un assistant de code IA (Claude Code, Cursor, Codex, GitHub Copilot, ...) ? Collez-lui ceci : + +```text +Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository. +``` + +Il vérifie votre carte graphique, votre RAM et votre disque, et choisit le modèle qui convient. Ensuite il l'installe, +le démarre et vous explique comment connecter vos applications. Les outils d'IA peuvent aussi installer, démarrer et +arrêter Strata grâce à son [serveur MCP](docs/MCP_SERVER.md). + +### Ou faites-le vous-même + +[Téléchargez Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) et décompressez-le (ou faites un `git clone`). +**Windows :** double-cliquez sur **`START-HERE.bat`**. **Linux :** lancez **`./setup.sh`** dans le dossier Strata. + +Les étapes sont les mêmes pour NVIDIA et AMD. L'installateur détecte votre carte et installe le bon moteur pour elle. +Il vous pose quelques questions : + +- quel modèle et quelle taille, +- combien de contexte (la quantité de texte que le modèle garde en tête), +- s'il doit lire les images. + +Appuyez sur Entrée à chaque fois pour garder la réponse recommandée. Ensuite il télécharge le modèle (environ 70 Go) +et le démarre. Si le téléchargement s'arrête, relancez-le : il reprend là où il s'était arrêté. Votre navigateur +ouvre l'application Strata à l'adresse `http://127.0.0.1:8080`. + +> **Pendant le démarrage du modèle, votre PC peut ralentir ou ne plus répondre pendant 1-3 minutes** (plus longtemps la première fois). +> Strata charge 35-55 Go dans votre RAM et en réserve une partie pour la carte graphique. C'est normal. Attendez, et +> ne fermez pas la fenêtre. La fenêtre montre ce que fait Strata. + +**La fois suivante**, relancez `START-HERE.bat` (ou `./setup.sh`). Il démarre tout de suite et ne télécharge rien +deux fois. Fermez sa fenêtre pour arrêter le modèle. `UPDATE.bat` (`./update.sh`) met Strata à jour sans le démarrer. +Mises à jour, Docker, plusieurs cartes, emplacement des fichiers et toutes les options : [docs/INSTALL.md](docs/INSTALL.md). + +## Quel modèle choisir ? + +L'installateur en recommande un selon votre RAM. Le même modèle existe en plusieurs tailles, plus ou moins +compressées. Les petites tailles sont plus rapides. Les grandes sont un peu plus intelligentes. + +| Votre RAM | Prenez | Pourquoi | +| --- | --- | --- | +| **32 Go** | **Coder** | il tient dans 32 Go et il est fait pour le code (avec une carte de 24 Go, Q2_0 et IQ2_XS tournent aussi) | +| **48 Go** | **IQ2_XS** (ou Q2_0, le plus rapide) | les plus grandes tailles ne tiennent pas | +| **64 Go** | **IQ2_XS** (recommandé), ou IQ3_XXS / IQ3_S | toutes les tailles tiennent ; IQ3_S est le meilleur et le plus lent | +| **96 Go ou plus** | **IQ3_S**, ou l'UD-IQ4_XS d'Unsloth (~4 bits) | de la place pour les plus grandes tailles, avec tout le reste ouvert | + +- **[Coder](docs/MODELS.md#coder) :** une version pour le code, avec la moitié des experts en moins. Elle atteint 91 % + du score SWE-bench Verified du modèle complet (mesuré par ses auteurs) et tient dans 32 Go de RAM. Elle est moins + bonne en dehors du code, y compris pour le chinois et les autres textes CJK (#438). Pour ces cas, prenez Q2_0, + IQ2_XS ou IQ3_S, qui gardent tous les experts. +- **[Swift 1.5](docs/MODELS.md#swift-15) :** un fine-tune qui réfléchit bien moins longtemps avant de répondre. Vous + avez la réponse plus tôt, avec à peu près la même qualité. +- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs)** : la version ~4 bits d'Unsloth, entre IQ3_S et + UD-Q4_K_XL en qualité. Un téléchargement de 94 Go. Avec moins de ~80 Go de RAM, Strata en lit une partie depuis + le SSD pendant qu'il répond, il y est donc plus lent (un SSD NVMe aide). +- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (expérimental) : le plus proche du modèle + complet. Mais Strata en lit la plus grande partie depuis le SSD pendant qu'il répond, donc il n'écrit que + 7-8.5 tokens/s sur un PC avec 64 Go. +- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs) :** vous l'installez à la main. + Il n'est pas dans le menu de l'installateur. + +Tailles, téléchargements et ce qui tient où : [docs/MODELS.md](docs/MODELS.md). Pour ajouter un autre modèle plus +tard, lancez `SETUP.bat` (Linux : `./setup.sh --setup`). + +## Utilisation + +

L'onglet Monitor de l'application Strata à côté d'un agent de code
+Le Monitor de l'application Strata (à gauche) pendant qu'un agent de code écrit le jardin de pagode de la vidéo (à droite)

+ +- **Dans le navigateur :** ouvrez `http://127.0.0.1:8080`. Vous y trouvez **Chat**, un **Monitor** en direct du + modèle et de votre GPU/CPU/RAM, et **About** avec les réglages et les adresses. +- **Vos applications et agents de code :** ajoutez un fournisseur « OpenAI-compatible » avec l'URL de base + **`http://127.0.0.1:8080/v1`**. N'importe quelle clé API et n'importe quel nom de modèle fonctionnent. + - Applications qui utilisent l'API d'Anthropic : `http://127.0.0.1:8080/v1/messages` (Claude Code : + `ANTHROPIC_BASE_URL=http://127.0.0.1:8080`). + - Codex CLI et les autres applications qui utilisent l'API OpenAI Responses : `/v1/responses` + ([configuration](docs/DETAILS.md#the-responses-api-and-codex-cli)). +- **Réflexion :** choisissez **off, low, medium ou high** dans le menu du chat ou dans le « reasoning effort » de + votre application. Off est le plus rapide. High est le meilleur pour les questions difficiles. +- **Images :** répondez oui à « Images? » pendant l'installation. Ensuite cliquez sur **Picture** dans le chat, ou + joignez des images dans votre application. Les cartes AMD lisent les images sous Linux via le processeur ; sous + Windows, pas encore. +- **Depuis votre téléphone ou un autre PC :** `START-HERE.bat --setup --host 0.0.0.0 --api-key `. Définissez + toujours une clé. +- **Une requête à la fois :** par défaut, Strata répond à une requête et les autres attendent. Pour répondre à + plusieurs en même temps, mettez `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque + réponse devient alors plus lente. +- **Longs prompts :** Strata lit en entier le premier message d'une discussion, environ 1 minute pour 30,000 tokens. + Les messages suivants démarrent en quelques secondes. + +Plus d'infos : [où sont stockées vos discussions](docs/INSTALL.md#where-things-are-stored), [l'API](docs/DETAILS.md#using-it). + +## Un problème ? + +- **Mon PC s'est figé au premier démarrage de Strata.** C'est normal pendant le chargement du modèle. Attendez, et ne + fermez pas la fenêtre. Toujours figé après 10 minutes ? Redémarrez le PC, fermez les autres programmes et + réessayez, ou choisissez une taille plus petite. +- **Il s'est arrêté pendant le téléchargement ou l'installation.** Relancez `START-HERE.bat` (ou `./setup.sh`). Il + reprend là où il s'était arrêté. +- **C'est très lent et le voyant du disque clignote sans arrêt, ou il affiche « the engine stopped unexpectedly ».** + Votre PC n'a pas assez de RAM libre. Fermez les autres programmes (les navigateurs en utilisent beaucoup), ou + choisissez une taille plus petite (Q2_0 ou IQ2_XS). +- **Il dit que le port 8080 est déjà utilisé.** Strata tourne déjà. Cherchez sa fenêtre. + +Autres problèmes et leurs solutions : [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Toujours bloqué ? Ouvrez une +[issue](https://github.com/Niko1221/Strata/issues) et joignez `strata-.log` depuis le dossier Strata. Vous +avez trouvé un problème de sécurité ? Signalez-le en privé : [SECURITY.md](SECURITY.md). + +## Comment ça marche ? + +Les modèles comme celui-ci tournent d'habitude sur des serveurs avec des centaines de gigaoctets de mémoire +graphique. Votre carte graphique a 12-24 Go. Strata fait tenir le modèle en **répartissant le travail sur tout votre +PC**. Pensez à une cuisine : ce que vous utilisez tout le temps reste sur le plan de travail, et le reste attend dans +le placard. + +

Les 24,576 experts du modèle : les plus sollicités sur la carte graphique, tous en RAM, une table de correspondance sur le SSD

+ +- **Le modèle est une équipe de 24,576 petits spécialistes (les « experts »).** Chaque mot n'en demande que 10. +- **Votre carte graphique** garde les quelques milliers d'experts les plus utilisés. **Votre RAM** les contient tous, + et **votre processeur** travaille sur les autres en même temps. **Votre SSD** contient une grande table de + correspondance. + +

Un petit assistant devine les mots suivants ; le grand modèle les vérifie tous d'un coup et garde les bons

+ +- **Deviner, puis vérifier :** un petit assistant devine les quelques mots suivants. Le grand modèle les vérifie + tous d'un coup. Vous obtenez la même réponse, 1.6-1.8x plus tôt. +- **Les longs textes sont lus par gros morceaux** (jusqu'à 8,192 tokens à la fois), à plus de 1,000 tokens par seconde. + +L'explication plus longue : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Chaque partie et ses chiffres : +[les détails](docs/DETAILS.md#how-it-works) et l'[article](docs/paper/Strata-Paper.pdf). + +## Crédits et licence + +Le modèle est [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), de l'équipe Qwen. Il a été +compressé par [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5) +et Unsloth. Strata utilise des parties de [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Tous les +crédits : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata est open source sous [licence MIT](LICENSE). +Quelques parties et chaque modèle ont leur propre licence ([lesquelles](docs/HOW_IT_WORKS.md#license)). + +## Soutenir Strata + +Strata est gratuit et open source. S'il vous est utile, vous pouvez soutenir son développement : + +

Buy Me A Coffee

diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 0000000000..897322fb5c --- /dev/null +++ b/README.ja.md @@ -0,0 +1,210 @@ +

Strata

+ +[English](README.md) · [简体中文](README.zh-CN.md) · **日本語** · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md) + +

1,250 億パラメータの AI モデルを、手元のゲーミング PC で動かす
+NVIDIA または AMD のグラフィックカード(12 GB 以上) · Windows または Linux · 無料のオープンソース

+ +

Strata のモデルが書いたボクセルの五重塔の庭。ブラウザで動いている様子
+ボクセルの五重塔の庭。1 回のプロンプトで作成し、RTX 5070 上の Strata(IQ3_S、128K コンテキスト)で実行 · +動画全体(49 秒)

+ +Strata は **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を普通の PC で動かします。 +これは大きくて賢い AI モデルで、ふつうはサーバーが必要です。チャットをしたり、コードを書いたり、画像を読んだりできます。 +アプリやコーディングエージェントとも連携します。データが PC の外に出ることはありません。 + +## どのくらい速い? + +ごく普通のゲーミング PC 2 台で測りました。1 トークンは英単語のおよそ ¾ です。 + +- **回答の書き出し:** 短いチャットで返答が表示される速さです。毎秒 60 トークンあれば、読むより速く表示されます。 +- **プロンプトの読み込み:** 送った内容を取り込む速さです(ここでは 32K トークンの文書、コード、またはチャット履歴)。 + + + + +
NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAMAMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM
+ +| サイズ | 回答の書き出し | プロンプトの読み込み | +| --- | ---: | ---: | +| **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 | + + + +| サイズ | 回答の書き出し | プロンプトの読み込み | +| --- | ---: | ---: | +| **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 | + +
+ +NVIDIA:Q2_0 はエンジン 0.1.36、ほかの行は 0.1.26 で測定(回答 4K、プロンプト 32K)。表の全体は +[DETAILS.md](docs/DETAILS.md#speed-measured) にあります。VRAM が多いカードほど速くなります。RTX 3090(24 GB)なら +毎秒およそ 100-140 トークンで書き出せるはずです。長いチャットやほかのカードについて:[モデルごとの速度](docs/MODELS.md#how-fast-is-each-size)、 +[コミュニティの測定結果](docs/COMMUNITY_BENCHMARKS.md)。 + +

Buy Me A Coffee
+Strata は無料です。あなたの PC でうまく動いたら、コーヒー 1 杯の支援が開発を続ける力になります。

+ +## 必要なもの + +| | | +| --- | --- | +| **グラフィックカード** | **NVIDIA** GeForce RTX 20、30、40、50 シリーズ、または **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 シリーズ。**VRAM 12 GB 以上**が必要です。 | +| **RAM** | 32 GB 以上。RAM の量で[使えるモデル](#どのモデルを選べばいい)が決まります。64 GB あればすべてのサイズが動きます。 | +| **ディスク** | 空き容量が約 80 GB。できれば SSD を使ってください。初回の起動がずっと速くなります。 | +| **OS** | Windows 10 / 11 または Linux。NVIDIA または AMD の最新のグラフィックドライバー。 | + +ほかに必要なものはすべてインストーラーが用意します。2 枚や 3 枚のカードでモデルを分担することもできます([マルチ GPU](docs/MULTI_GPU.md))。 + +試験的なサポート。コミュニティのメンバーが自分のマシンで書き、テストしたものです: + +- **古いグラフィックカード**(Tesla P40 / V100、GTX 10、Radeon VII / MI50、RX 6700 XT、RX 5500 XT):[Older GPUs](docs/OLDER_GPUS.md)。 +- **Intel Arc**(Linux でソースからビルド):[Intel Arc](docs/INTEL_ARC.md)。 +- **AVX2 のない古いプロセッサー**:動きますが、遅いです。[Older CPUs](docs/INSTALL.md#older-cpus-experimental)。 + +全リスト:[docs/INSTALL.md](docs/INSTALL.md#what-you-need)。 + +## インストール + +### AI にセットアップしてもらう + +AI コーディングアシスタント(Claude Code、Cursor、Codex、GitHub Copilot など)を使っていますか? これを貼り付けてください: + +```text +Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository. +``` + +AI がグラフィックカード、RAM、ディスクを調べて、合うモデルを選びます。それからインストールして起動し、 +アプリのつなぎ方を教えてくれます。AI ツールは Strata の [MCP サーバー](docs/MCP_SERVER.md) を通じて、 +Strata のインストール、起動、停止もできます。 + +### 自分でやる + +[Strata をダウンロード](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip)して展開します(または `git clone` します)。 +**Windows:** **`START-HERE.bat`** をダブルクリック。**Linux:** Strata フォルダーで **`./setup.sh`** を実行します。 + +手順は NVIDIA でも AMD でも同じです。インストーラーがカードを見つけて、それに合うエンジンを用意します。 +いくつか質問があります: + +- どのモデルを、どのサイズで使うか +- コンテキストをどのくらいにするか(モデルが覚えておけるテキストの量) +- 画像を読ませるかどうか + +毎回 Enter を押せば、おすすめの答えになります。そのあとモデル(約 70 GB)をダウンロードして起動します。 +ダウンロードが止まったら、もう一度実行してください。続きから再開します。ブラウザーで Strata アプリが +`http://127.0.0.1:8080` に開きます。 + +> **モデルの起動中、1-3 分ほど PC が重くなったり、反応しなくなったりすることがあります**(初回がいちばん長いです)。 +> Strata は 35-55 GB を RAM に読み込み、その一部をグラフィックカード用に固定します。これは正常です。 +> ウィンドウを閉じずに待ってください。Strata が何をしているかはウィンドウに表示されます。 + +**次回からは** `START-HERE.bat`(または `./setup.sh`)をもう一度実行します。すぐに起動し、同じものを 2 回ダウンロードすることはありません。 +モデルを止めるときはウィンドウを閉じます。`UPDATE.bat`(`./update.sh`)を使うと、Strata を起動せずに更新できます。 +更新、Docker、複数のカード、ファイルの保存場所、すべてのオプションについて:[docs/INSTALL.md](docs/INSTALL.md)。 + +## どのモデルを選べばいい + +インストーラーが RAM に合わせてひとつ勧めます。同じモデルにも、圧縮の強さが違う複数のサイズがあります。 +小さいサイズほど速く、大きいサイズほど少し賢くなります。 + +| RAM | 選ぶもの | 理由 | +| --- | --- | --- | +| **32 GB** | **Coder** | 32 GB に収まり、コード向けに作られている(24 GB のカードなら Q2_0 と IQ2_XS も動く) | +| **48 GB** | **IQ2_XS**(または最速の Q2_0) | 大きいサイズは収まらない | +| **64 GB** | **IQ2_XS**(おすすめ)、または IQ3_XXS / IQ3_S | すべてのサイズが収まる。IQ3_S がいちばん賢く、いちばん遅い | +| **96 GB 以上** | **IQ3_S**、または Unsloth の UD-IQ4_XS(約 4-bit) | ほかのものを全部開いたままでも、最大のサイズが入る余裕がある | + +- **[Coder](docs/MODELS.md#coder):** エキスパートを半分取り除いたコーディング向け版です。フルモデルの SWE-bench Verified スコアの + 91% に届き(作者による測定)、32 GB の RAM に収まります。コード以外では弱く、中国語などの CJK のテキストも苦手です(#438)。 + そうした用途には、すべてのエキスパートを残している Q2_0、IQ2_XS、IQ3_S を選んでください。 +- **[Swift 1.5](docs/MODELS.md#swift-15):** 答える前に考える時間がずっと短いファインチューン版です。 + ほぼ同じ品質のまま、答えが早く返ってきます。 +- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth の約 4-bit 版で、品質は IQ3_S と UD-Q4_K_XL の間です。 + ダウンロードは 94 GB。RAM が約 80 GB 未満だと、答えている間に Strata がその一部を SSD から読むので遅くなります(NVMe SSD が有効です)。 +- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)**(試験的):フルモデルにいちばん近いものです。 + ただし答えている間、Strata はその大部分を SSD から読むので、64 GB の PC では毎秒 7-8.5 トークンしか書き出せません。 +- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** 手動でセットアップします。 + インストーラーのメニューにはありません。 + +サイズ、ダウンロード、どれがどこに収まるかについて:[docs/MODELS.md](docs/MODELS.md)。あとで別のモデルを追加するには +`SETUP.bat`(Linux:`./setup.sh --setup`)を実行します。 + +## 使い方 + +

Strata アプリの Monitor タブと、その横で動くコーディングエージェント
+Strata アプリの Monitor(左)。コーディングエージェントが動画の五重塔の庭を書いているところ(右)

+ +- **ブラウザーで:** `http://127.0.0.1:8080` を開きます。**Chat**、モデルと GPU/CPU/RAM の様子をリアルタイムで見る **Monitor**、 + 設定とアドレスが載った **About** があります。 +- **アプリやコーディングエージェントから:** 「OpenAI-compatible」プロバイダーを追加し、ベース URL に + **`http://127.0.0.1:8080/v1`** を指定します。API キーとモデル名は何でもかまいません。 + - Anthropic の API を使うアプリ:`http://127.0.0.1:8080/v1/messages`(Claude Code: + `ANTHROPIC_BASE_URL=http://127.0.0.1:8080`)。 + - Codex CLI など OpenAI Responses API を使うアプリ:`/v1/responses` + ([設定方法](docs/DETAILS.md#the-responses-api-and-codex-cli))。 +- **思考(Thinking):** チャットのメニュー、またはアプリの「reasoning effort」で **off、low、medium、high** を選びます。 + off がいちばん速く、high は難しい質問に向いています。 +- **画像:** セットアップで「Images?」に yes と答えます。そのあとチャットで **Picture** をクリックするか、アプリで画像を添付します。 + AMD のカードは、Linux ではプロセッサーを使って画像を読みます。Windows ではまだ読めません。 +- **スマートフォンや別の PC から:** `START-HERE.bat --setup --host 0.0.0.0 --api-key `。キーは必ず設定してください。 +- **リクエストは 1 つずつ:** 初期設定では Strata は 1 つのリクエストに答え、ほかは待ちます。複数に同時に答えるには + `"parallel": 2` を設定します([BATCHING.md](docs/BATCHING.md))。12 GB のカードでは、それぞれの回答が遅くなります。 +- **長いプロンプト:** Strata はチャットの最初のメッセージをすべて読みます。30,000 トークンあたり約 1 分かかります。 + 2 通目以降のメッセージは数秒で始まります。 + +詳しくは:[チャットの保存場所](docs/INSTALL.md#where-things-are-stored)、[API](docs/DETAILS.md#using-it)。 + +## うまくいかないとき + +- **Strata の初回起動で PC が固まった。** モデルの読み込み中はよくあることです。ウィンドウを閉じずに待ってください。 + 10 分たっても固まったまま? PC を再起動し、ほかのプログラムを閉じてもう一度試すか、小さいサイズを選んでください。 +- **ダウンロードやインストールの途中で止まった。** `START-HERE.bat`(または `./setup.sh`)をもう一度実行してください。 + 止まったところから続きます。 +- **とても遅く、ディスクのランプが点滅し続ける。または「the engine stopped unexpectedly」と表示される。** PC の空き RAM が + 足りません。ほかのプログラムを閉じるか(ブラウザーは多く使います)、小さいサイズ(Q2_0 または IQ2_XS)を選んでください。 +- **ポート 8080 がすでに使われていると表示される。** Strata はもう動いています。そのウィンドウを探してください。 + +ほかの問題と解決方法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。それでも解決しない場合は +[issue](https://github.com/Niko1221/Strata/issues) を作成し、Strata フォルダーにある `strata-.log` を添付してください。 +セキュリティの問題を見つけたら、非公開で報告してください:[SECURITY.md](SECURITY.md)。 + +## しくみ + +このようなモデルは、ふつう数百ギガバイトのグラフィックメモリを持つサーバーで動きます。あなたのグラフィックカードは +12-24 GB です。Strata は **PC 全体で作業を分担する** ことで、モデルを収めています。台所を思い浮かべてください。 +いつも使う道具は調理台に置き、残りは食料庫にしまっておきます。 + +

モデルの 24,576 個のエキスパート:よく使うものはグラフィックカードに、すべては RAM に、ルックアップテーブルは SSD に

+ +- **モデルは 24,576 個の小さな専門家(「エキスパート」)のチームです。** 1 語ごとに必要なのはそのうち 10 個だけです。 +- **グラフィックカード** は、よく使われる数千個のエキスパートを持ちます。**RAM** はすべてのエキスパートを持ち、 + **プロセッサー** が残りを同時に処理します。**SSD** は大きなルックアップテーブルを持ちます。 + +

小さなヘルパーが次の単語を予想し、大きなモデルがまとめて確認して正しいものを残す

+ +- **予想してから確認:** 小さなヘルパーが次の数語を予想します。大きなモデルがそれをまとめて確認します。 + 答えは同じまま、1.6-1.8x 早く返ってきます。 +- **長いテキストは大きなまとまりで読みます**(一度に最大 8,192 トークン)。速さは毎秒 1,000 トークン以上です。 + +もっと詳しい説明:[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)。各部分とその数値: +[詳細](docs/DETAILS.md#how-it-works)と[論文](docs/paper/Strata-Paper.pdf)。 + +## クレジットとライセンス + +モデルは Qwen チームの [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) です。 +圧縮は [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)、UkisAI(Swift 1.5)、 +Unsloth によるものです。Strata は [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) の一部を使っています。すべてのクレジット: +[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits)。Strata は [MIT License](LICENSE) のオープンソースです。一部のパーツと +すべてのモデルには、それぞれ独自のライセンスがあります([どれか](docs/HOW_IT_WORKS.md#license))。 + +## Strata を支援する + +Strata は無料のオープンソースです。役に立ったら、開発を支援していただけます: + +

Buy Me A Coffee

diff --git a/README.md b/README.md index 7d888af1f9..3cd197c46c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@

Downstream multi-lane fork of Niko1221/Strata.
Upstream Strata is created and maintained by Niko1221; this fork carries the GPU-per-lane / shared-arena extensions.

+**English** · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md) +

Run a 125-billion-parameter AI model on your own gaming PC
NVIDIA or AMD graphics card (12 GB or more) · Windows or Linux · free and open source

@@ -10,15 +12,16 @@ NVIDIA or AMD graphics card (12 GB or more) · Windows or Linux · free and open A voxel pagoda garden, 1 shot prompt running on an RTX 5070 with Strata (IQ3_S, 128K context) · full video (49 s)

-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 a normal PC. It chats, writes code, reads pictures and works with your apps and coding -agents, and nothing leaves your PC. +Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** on a normal PC. This is a +large, smart AI model that usually needs a server. It chats, writes code, reads pictures and works with your apps +and coding agents. Nothing leaves your PC. ## How fast is it? -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. +We measured it on two ordinary gaming PCs. A token is about ¾ of a word. + +- **Writes answers:** how fast the reply appears in a short chat. 60 tokens per second is faster than you can read. +- **Reads your prompt:** how fast it takes in what you send (here a 32K-token document, code or chat history). @@ -43,8 +46,10 @@ word, so 60 tokens per second is faster than you can read.
NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAMAMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM
-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). +NVIDIA: Q2_0 with engine 0.1.36, the other rows with 0.1.26 (4K answers, 32K prompts). The full tables are in +[DETAILS.md](docs/DETAILS.md#speed-measured). A card with more VRAM is faster: an RTX 3090 (24 GB) should write +about 100-140 tokens per second. Long chats and other cards: [speed of each model](docs/MODELS.md#how-fast-is-each-size), +[community results](docs/COMMUNITY_BENCHMARKS.md).

Buy Me A Coffee
Strata is free. If it runs well on your PC, a coffee keeps the work on it going.

@@ -53,26 +58,33 @@ other cards: [speed of each model](docs/MODELS.md#how-fast-is-each-size), [commu | | | | --- | --- | -| **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 | +| **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. It needs **12 GB of VRAM or more**. | +| **RAM** | 32 GB or more. Your RAM decides [which model](#which-model-should-i-pick) fits. 64 GB runs every size. | +| **Disk** | About 80 GB free. Use 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. | + +The installer sets up everything else. Two or three cards can share the model ([multi-GPU](docs/MULTI_GPU.md)). + +Experimental, written and tested by community members on their own machines: + +- **Older graphics cards** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md). +- **Intel Arc**, built from source on Linux: [Intel Arc](docs/INTEL_ARC.md). +- **Older processors without AVX2**: they work, but slowly. [Older CPUs](docs/INSTALL.md#older-cpus-experimental). -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 ### Let your AI set it up -Use an AI coding assistant (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Paste this into it: +Do you use an AI coding assistant (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Paste this into it: ```text Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository. ``` -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 +It checks your graphics card, RAM and disk and picks the model that fits. Then it installs and starts it and tells +you how to connect your apps. AI tools can also install, start and stop Strata through its [MCP server](docs/MCP_SERVER.md). ### Or do it yourself @@ -80,43 +92,51 @@ connect your apps. AI tools can also install, start and stop Strata themselves t [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`. +The steps are the same for NVIDIA and AMD. The installer finds your card and sets up the right engine for it. It +asks you a few questions: + +- which model and which size, +- how much context (how much text the model keeps in mind), +- whether it should read pictures. + +Press Enter each time for the recommended answer. Then it downloads the model (about 70 GB) and starts it. If the +download stops, run it again: it continues where it left off. 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. +> **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. This is normal. Wait, and don't +> close the window. The window shows what Strata is doing. -**Next time**, run `START-HERE.bat` (or `./setup.sh`) again: it starts right away, nothing is downloaded twice. Close +**Next time**, run `START-HERE.bat` (or `./setup.sh`) again. It starts right away and downloads nothing 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). +several cards, where the files go and every option: [docs/INSTALL.md](docs/INSTALL.md). ## Which model should I pick? -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. +The installer recommends one for your RAM. The same model comes in several sizes, compressed more or less. Smaller +sizes are faster. Larger sizes are a bit smarter. | 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 | - -- **[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. - -Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). You can add another model later with +| **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 UD-IQ4_XS (~4-bit) | room for the largest sizes with everything else open | + +- **[Coder](docs/MODELS.md#coder):** a coding version with half of the experts removed. It reaches 91% of the full + model's SWE-bench Verified score (measured by its authors) and fits 32 GB of RAM. It is 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 for a much shorter time before it answers. You + get the answer sooner, at about the same quality. +- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth's ~4-bit version, between IQ3_S and + UD-Q4_K_XL in quality. A 94 GB download. With less than ~80 GB of RAM, Strata reads part of it from the SSD + while it answers, so it is slower there (an NVMe SSD helps). +- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental): the closest to the full + model. But Strata reads most of it from the SSD while it answers, so it writes only 7-8.5 tokens/s on a 64 GB PC. +- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** you set it up by hand. It is + not in the installer's menu. + +Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). To add another model later, run `SETUP.bat` (Linux: `./setup.sh --setup`). ## Using it @@ -141,61 +161,69 @@ For long-lived chats/agents, send a stable `X-Strata-Session-Id` on every turn s

The Strata app's Monitor tab next to a coding agent
The Strata app's Monitor (left) while a coding agent writes the pagoda garden from the video (right)

-- **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 ` - always with a key. -- **Good to know:** the ordinary single-engine server answers one generation request at a time. In **Strata-Lanes multi-lane mode**, each lane still runs one active generation, while separate lanes can serve separate requests concurrently. The first message of a chat is read in full; follow-ups can reuse lane-local state when session affinity is preserved. +- **In the browser:** open `http://127.0.0.1:8080`. It has **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 the base URL + **`http://127.0.0.1:8080/v1`**. Any API key and any model name work. + - 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`). + - Codex CLI and other apps that use the OpenAI Responses API: `/v1/responses` + ([setup](docs/DETAILS.md#the-responses-api-and-codex-cli)). +- **Thinking:** choose **off, low, medium or high** in the chat menu or in your app's "reasoning effort". Off is the + fastest. High is best for hard questions. +- **Pictures:** say yes to "Images?" in setup. Then click **Picture** in the chat, or attach pictures in your app. + AMD cards read pictures on Linux through the processor; on Windows they can't yet. +- **From your phone or another PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key `. Always set a key. +- **Concurrency:** the ordinary upstream-style single-engine server answers one request at a time by default; it can + opt into `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). In **Strata-Lanes multi-lane mode**, each ordinary + lane remains an independent whole-GPU engine by default, while separate lanes serve separate requests concurrently. +- **Long prompts:** Strata reads the first message of a chat in full, about 1 minute per 30,000 tokens. Follow-up + messages start in seconds when their lane-local state can be reused. 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 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 +- **My PC froze the first time Strata started.** This is normal while it loads the model. Wait, and 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's very slow and the disk light keeps blinking, or it says "the engine stopped unexpectedly".** Your PC does + not have 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. 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-.log` from the Strata folder. +[issue](https://github.com/Niko1221/Strata/issues) and attach `strata-.log` from the Strata folder. Found a +security problem? Report it privately: [SECURITY.md](SECURITY.md). ## 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** - like a kitchen, where the things you use -all the time stay on the counter and the rest waits in the pantry. +Models like this one usually run on servers with hundreds of gigabytes of graphics memory. Your graphics card has +12-24 GB. Strata makes the model fit by **sharing the work across your whole PC**. Think of a kitchen: the things +you use all the time stay on the counter, and the rest waits in the pantry.

The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD

-- **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, +- **The model is a team of 24,576 small specialists ("experts").** Each word needs only 10 of them. +- **Your graphics card** keeps the few thousand experts that are used 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.

A small helper guesses the next words; the big model checks them all at once and keeps the right ones

-- **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. +- **Guess, then check:** a small helper guesses the next few words. The big model checks them all at once. 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), at over 1,000 tokens per second. -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). +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 and license -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)). +The model is [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team. It was +compressed by [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5) +and Unsloth. Strata uses 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 have their own licenses ([which ones](docs/HOW_IT_WORKS.md#license)). ## Support Strata diff --git a/README.pt-BR.md b/README.pt-BR.md new file mode 100644 index 0000000000..a34dbf2f50 --- /dev/null +++ b/README.pt-BR.md @@ -0,0 +1,216 @@ +

Strata

+ +[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · **Português** + +

Rode um modelo de IA de 125 bilhões de parâmetros no seu próprio PC gamer
+Placa de vídeo NVIDIA ou AMD (12 GB ou mais) · Windows ou Linux · gratuito e de código aberto

+ +

Um jardim com pagode em voxels que o modelo do Strata escreveu, rodando no navegador
+Um jardim com pagode em voxels, feito com um único prompt numa RTX 5070 com o Strata (IQ3_S, contexto de 128K) · +vídeo completo (49 s)

+ +O Strata roda o **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** num PC comum. É um modelo +de IA grande e inteligente que normalmente precisa de um servidor. Ele conversa, escreve código, entende imagens e +funciona com seus apps e agentes de programação. Nada sai do seu PC. + +## Qual é a velocidade? + +Medimos em dois PCs gamer comuns. Um token equivale a cerca de ¾ de uma palavra. + +- **Escreve respostas:** a velocidade com que a resposta aparece numa conversa curta. 60 tokens por segundo é mais rápido do que você consegue ler. +- **Lê seu prompt:** a velocidade com que ele absorve o que você envia (aqui, um documento, código ou histórico de conversa de 32K tokens). + + + + +
NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB de RAMAMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB de RAM
+ +| Tamanho | Escreve respostas | Lê seu 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 | + + + +| Tamanho | Escreve respostas | Lê seu 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 | + +
+ +NVIDIA: Q2_0 com o engine 0.1.36, as outras linhas com 0.1.26 (respostas de 4K, prompts de 32K). As tabelas +completas estão em [DETAILS.md](docs/DETAILS.md#speed-measured). Uma placa com mais VRAM é mais rápida: uma +RTX 3090 (24 GB) deve escrever cerca de 100-140 tokens por segundo. Conversas longas e outras placas: +[velocidade de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados da comunidade](docs/COMMUNITY_BENCHMARKS.md). + +

Buy Me A Coffee
+O Strata é gratuito. Se ele roda bem no seu PC, um café ajuda a manter o trabalho nele.

+ +## Do que você precisa + +| | | +| --- | --- | +| **Placa de vídeo** | **NVIDIA** GeForce RTX séries 20, 30, 40 ou 50, ou **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 ou séries RX 6800 / 6900. Ela precisa de **12 GB de VRAM ou mais**. | +| **RAM** | 32 GB ou mais. Sua RAM decide [qual modelo](#qual-modelo-devo-escolher) cabe. Com 64 GB, roda qualquer tamanho. | +| **Disco** | Cerca de 80 GB livres. Use um SSD se puder: a primeira inicialização fica bem mais rápida. | +| **Sistema** | Windows 10 / 11 ou Linux, e um driver de vídeo atualizado da NVIDIA ou da AMD. | + +O instalador cuida de todo o resto. Duas ou três placas podem dividir o modelo ([multi-GPU](docs/MULTI_GPU.md)). + +Experimental, escrito e testado por membros da comunidade nas próprias máquinas: + +- **Placas de vídeo mais antigas** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md). +- **Intel Arc**, compilado a partir do código-fonte no Linux: [Intel Arc](docs/INTEL_ARC.md). +- **Processadores mais antigos sem AVX2**: funcionam, mas devagar. [Older CPUs](docs/INSTALL.md#older-cpus-experimental). + +A lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need). + +## Instalação + +### Deixe sua IA instalar + +Você usa um assistente de programação com IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Cole isto nele: + +```text +Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository. +``` + +Ele verifica sua placa de vídeo, RAM e disco e escolhe o modelo que cabe. Depois instala, inicia e explica como +conectar seus apps. Ferramentas de IA também podem instalar, iniciar e parar o Strata pelo +[servidor MCP](docs/MCP_SERVER.md) dele. + +### Ou faça você mesmo + +[Baixe o Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) e descompacte (ou use `git clone`). +**Windows:** clique duas vezes em **`START-HERE.bat`**. **Linux:** rode **`./setup.sh`** na pasta do Strata. + +Os passos são os mesmos para NVIDIA e AMD. O instalador encontra sua placa e configura o engine certo para ela. +Ele faz algumas perguntas: + +- qual modelo e qual tamanho, +- quanto contexto (quanto texto o modelo guarda na memória), +- se ele deve entender imagens. + +Aperte Enter em cada uma para usar a resposta recomendada. Depois ele baixa o modelo (cerca de 70 GB) e o inicia. +Se o download parar, rode de novo: ele continua de onde parou. Seu navegador abre o app do Strata em +`http://127.0.0.1:8080`. + +> **Enquanto o modelo inicia, seu PC pode ficar lento ou parar de responder por 1-3 minutos** (mais na primeira vez). +> O Strata carrega 35-55 GB na sua RAM e reserva parte dela para a placa de vídeo. Isso é normal. Espere e não +> feche a janela. A janela mostra o que o Strata está fazendo. + +**Da próxima vez**, rode `START-HERE.bat` (ou `./setup.sh`) de novo. Ele inicia na hora e não baixa nada duas +vezes. Feche a janela para parar o modelo. `UPDATE.bat` (`./update.sh`) atualiza o Strata sem iniciá-lo. +Atualização, Docker, várias placas, onde ficam os arquivos e todas as opções: [docs/INSTALL.md](docs/INSTALL.md). + +## Qual modelo devo escolher? + +O instalador recomenda um de acordo com sua RAM. O mesmo modelo vem em vários tamanhos, mais ou menos comprimidos. +Os tamanhos menores são mais rápidos. Os maiores são um pouco mais inteligentes. + +| Sua RAM | Escolha | Por quê | +| --- | --- | --- | +| **32 GB** | **Coder** | cabe em 32 GB e é feito para código (com uma placa de 24 GB, Q2_0 e IQ2_XS também rodam) | +| **48 GB** | **IQ2_XS** (ou Q2_0, o mais rápido) | os tamanhos maiores não cabem | +| **64 GB** | **IQ2_XS** (recomendado), ou IQ3_XXS / IQ3_S | todos os tamanhos cabem; IQ3_S é o melhor e o mais lento | +| **96 GB ou mais** | **IQ3_S**, ou o UD-IQ4_XS da Unsloth (~4-bit) | espaço para os maiores tamanhos com todo o resto aberto | + +- **[Coder](docs/MODELS.md#coder):** uma versão para programação com metade dos experts removida. Ela alcança 91% + da pontuação do modelo completo no SWE-bench Verified (medido pelos autores) e cabe em 32 GB de RAM. É mais fraca + fora do código, inclusive em chinês e outros textos CJK (#438). Para isso, escolha Q2_0, IQ2_XS ou IQ3_S, que + mantêm todos os experts. +- **[Swift 1.5](docs/MODELS.md#swift-15):** um fine-tune que pensa por bem menos tempo antes de responder. Você + recebe a resposta mais cedo, com quase a mesma qualidade. +- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** a versão de ~4 bits da Unsloth, entre IQ3_S e + UD-Q4_K_XL em qualidade. Um download de 94 GB. Com menos de ~80 GB de RAM, o Strata lê uma parte dele do SSD + enquanto responde, então ali ele fica mais lento (um SSD NVMe ajuda). +- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental): o mais próximo do modelo + completo. Mas o Strata lê a maior parte dele do SSD enquanto responde, então ele escreve só 7-8.5 tokens/s num PC + com 64 GB. +- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** você configura à mão. Ele + não está no menu do instalador. + +Tamanhos, downloads e o que cabe onde: [docs/MODELS.md](docs/MODELS.md). Para adicionar outro modelo depois, rode +`SETUP.bat` (Linux: `./setup.sh --setup`). + +## Como usar + +

A aba Monitor do app Strata ao lado de um agente de programação
+O Monitor do app Strata (à esquerda) enquanto um agente de programação escreve o jardim com pagode do vídeo (à direita)

+ +- **No navegador:** abra `http://127.0.0.1:8080`. Lá tem o **Chat**, um **Monitor** ao vivo do modelo e da sua + GPU/CPU/RAM, e o **About** com as configurações e os endereços. +- **Seus apps e agentes de programação:** adicione um provedor "OpenAI-compatible" com a URL base + **`http://127.0.0.1:8080/v1`**. Qualquer chave de API e qualquer nome de modelo funcionam. + - Apps que usam a API da Anthropic: `http://127.0.0.1:8080/v1/messages` (Claude Code: + `ANTHROPIC_BASE_URL=http://127.0.0.1:8080`). + - Codex CLI e outros apps que usam a OpenAI Responses API: `/v1/responses` + ([configuração](docs/DETAILS.md#the-responses-api-and-codex-cli)). +- **Raciocínio:** escolha **off, low, medium ou high** no menu do chat ou no "reasoning effort" do seu app. Off é o + mais rápido. High é o melhor para perguntas difíceis. +- **Imagens:** responda sim para "Images?" na instalação. Depois clique em **Picture** no chat, ou anexe imagens no + seu app. Placas AMD entendem imagens no Linux usando o processador; no Windows, ainda não. +- **Do seu celular ou de outro PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key `. Sempre defina uma chave. +- **Uma requisição por vez:** por padrão, o Strata responde uma requisição e as outras esperam. Para responder + várias ao mesmo tempo, defina `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa + cada resposta mais lenta. +- **Prompts longos:** o Strata lê a primeira mensagem de uma conversa inteira, cerca de 1 minuto a cada 30,000 + tokens. As mensagens seguintes começam em segundos. + +Mais: [onde suas conversas ficam salvas](docs/INSTALL.md#where-things-are-stored), [a API](docs/DETAILS.md#using-it). + +## Algo deu errado? + +- **Meu PC travou na primeira vez que o Strata iniciou.** Isso é normal enquanto ele carrega o modelo. Espere e não + feche a janela. Ainda travado depois de 10 minutos? Reinicie o PC, feche outros programas e tente de novo, ou + escolha um tamanho menor. +- **Parou durante o download ou a instalação.** Rode `START-HERE.bat` (ou `./setup.sh`) de novo. Ele continua de + onde parou. +- **Está muito lento e a luz do disco não para de piscar, ou aparece "the engine stopped unexpectedly".** Seu PC + não tem RAM livre suficiente. Feche outros programas (navegadores usam muita), ou escolha um tamanho menor (Q2_0 + ou IQ2_XS). +- **Diz que a porta 8080 já está em uso.** O Strata já está rodando. Procure a janela dele. + +Mais problemas e como resolver: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Ainda sem solução? Abra uma +[issue](https://github.com/Niko1221/Strata/issues) e anexe o `strata-.log` da pasta do Strata. Encontrou um +problema de segurança? Informe em particular: [SECURITY.md](SECURITY.md). + +## Como funciona? + +Modelos como este normalmente rodam em servidores com centenas de gigabytes de memória de vídeo. Sua placa de vídeo +tem 12-24 GB. O Strata faz o modelo caber **dividindo o trabalho pelo PC inteiro**. Pense numa cozinha: o que você +usa o tempo todo fica na bancada, e o resto espera na despensa. + +

Os 24,576 experts do modelo: os mais usados na placa de vídeo, todos na RAM, uma tabela de consulta no SSD

+ +- **O modelo é uma equipe de 24,576 pequenos especialistas ("experts").** Cada palavra precisa de só 10 deles. +- **Sua placa de vídeo** guarda os poucos milhares de experts usados com mais frequência. **Sua RAM** guarda todos + eles, e **seu processador** trabalha no resto ao mesmo tempo. **Seu SSD** guarda uma grande tabela de consulta. + +

Um pequeno ajudante adivinha as próximas palavras; o modelo grande confere todas de uma vez e fica com as certas

+ +- **Adivinhar e conferir:** um pequeno ajudante adivinha as próximas palavras. O modelo grande confere todas de + uma vez. Você recebe a mesma resposta, 1.6-1.8x mais cedo. +- **Textos longos são lidos em grandes pedaços** (até 8,192 tokens por vez), a mais de 1,000 tokens por segundo. + +A explicação mais longa: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte e seus números: +[os detalhes](docs/DETAILS.md#how-it-works) e o [artigo](docs/paper/Strata-Paper.pdf). + +## Créditos e licença + +O modelo é o [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), da equipe Qwen. Ele foi +comprimido por [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5) +e Unsloth. O Strata usa partes do [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos os créditos: +[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). O Strata é de código aberto sob a [MIT License](LICENSE). +Algumas partes e todos os modelos têm licenças próprias ([quais](docs/HOW_IT_WORKS.md#license)). + +## Apoie o Strata + +O Strata é gratuito e de código aberto. Se ele é útil para você, você pode apoiar o desenvolvimento: + +

Buy Me A Coffee

diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000000..6eb1220dd1 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,199 @@ +

Strata

+ +[English](README.md) · **简体中文** · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md) + +

在你自己的游戏电脑上运行 1250 亿参数的 AI 模型
+NVIDIA 或 AMD 显卡(12 GB 及以上)· Windows 或 Linux · 免费开源

+ +

Strata 的模型写出的体素宝塔花园,在浏览器中运行
+体素宝塔花园,一次提示生成,在 RTX 5070 上用 Strata 运行(IQ3_S,128K 上下文)· +完整视频(49 秒)

+ +Strata 能在普通电脑上运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**。这是一个又大又聪明的 +AI 模型,通常要用服务器才能跑。它能聊天、写代码、看图片,还能配合你的应用和编程智能体一起工作。所有数据都留在你的电脑上。 + +## 速度有多快? + +我们在两台普通的游戏电脑上做了测试。一个 token 大约相当于 ¾ 个英文单词。 + +- **写回答:** 短对话中回复出现的速度。每秒 60 个 token 已经比你的阅读速度还快。 +- **读提示:** 读入你发送内容的速度(这里是一份 32K token 的文档、代码或聊天记录)。 + + + + +
NVIDIA:RTX 5070(12 GB)、Ryzen 5 7600、64 GB 内存AMD:RX 9070 XT(16 GB)、Ryzen 9 3900X、47 GB 内存
+ +| 规格 | 写回答 | 读提示 | +| --- | ---: | ---: | +| **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 | + + + +| 规格 | 写回答 | 读提示 | +| --- | ---: | ---: | +| **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 | + +
+ +NVIDIA:Q2_0 用的是引擎 0.1.36,其他各行用的是 0.1.26(4K 回答,32K 提示)。完整表格见 +[DETAILS.md](docs/DETAILS.md#speed-measured)。显存越大的显卡越快:RTX 3090(24 GB)写回答应该能达到每秒约 +100-140 个 token。长对话和其他显卡的数据:[各模型的速度](docs/MODELS.md#how-fast-is-each-size)、 +[社区测试结果](docs/COMMUNITY_BENCHMARKS.md)。 + +

请我喝杯咖啡
+Strata 是免费的。如果它在你的电脑上跑得不错,请我喝杯咖啡,让这个项目继续下去。

+ +## 你需要什么 + +| | | +| --- | --- | +| **显卡** | **NVIDIA** GeForce RTX 20、30、40 或 50 系列,或 **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 系列。需要 **12 GB 或以上显存**。 | +| **内存** | 32 GB 或以上。内存大小决定能装下[哪个模型](#该选哪个模型)。64 GB 可以运行所有规格。 | +| **硬盘** | 约 80 GB 可用空间。尽量用 SSD:第一次启动会快很多。 | +| **系统** | Windows 10 / 11 或 Linux,以及 NVIDIA 或 AMD 的最新显卡驱动。 | + +其他的都由安装程序搞定。两到三张显卡可以分担同一个模型([多 GPU](docs/MULTI_GPU.md))。 + +实验性支持,由社区成员在自己的机器上编写和测试: + +- **较老的显卡**(Tesla P40 / V100、GTX 10、Radeon VII / MI50、RX 6700 XT、RX 5500 XT):[较老的 GPU](docs/OLDER_GPUS.md)。 +- **Intel Arc**,在 Linux 上从源码构建:[Intel Arc](docs/INTEL_ARC.md)。 +- **不支持 AVX2 的老处理器**:能用,但很慢。[较老的 CPU](docs/INSTALL.md#older-cpus-experimental)。 + +完整列表:[docs/INSTALL.md](docs/INSTALL.md#what-you-need)。 + +## 安装 + +### 让你的 AI 帮你装 + +你在用 AI 编程助手吗(Claude Code、Cursor、Codex、GitHub Copilot 等)?把下面这段粘贴给它: + +```text +Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository. +``` + +它会检查你的显卡、内存和硬盘,选出合适的模型。然后安装并启动它,再告诉你怎么连接你的应用。AI 工具也可以通过 +Strata 的 [MCP 服务器](docs/MCP_SERVER.md)来安装、启动和停止 Strata。 + +### 或者自己动手 + +[下载 Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) 并解压(或者用 `git clone`)。 +**Windows:** 双击 **`START-HERE.bat`**。**Linux:** 在 Strata 文件夹里运行 **`./setup.sh`**。 + +NVIDIA 和 AMD 的步骤完全一样。安装程序会识别你的显卡,并装好对应的引擎。它会问你几个问题: + +- 用哪个模型、哪个规格, +- 上下文多大(模型能记住多少文字), +- 是否要识别图片。 + +每次直接按回车就是推荐选项。之后它会下载模型(约 70 GB)并启动。如果下载中断了,再运行一次即可:它会从中断的地方继续。 +浏览器会打开 Strata 应用,地址是 `http://127.0.0.1:8080`。 + +> **模型启动时,你的电脑可能会变慢或卡住 1-3 分钟**(第一次最久)。 +> Strata 会把 35-55 GB 加载到内存,并为显卡锁定其中一部分。这是正常的。请耐心等待,不要关闭窗口。 +> 窗口里会显示 Strata 正在做什么。 + +**下次使用时**,再运行一次 `START-HERE.bat`(或 `./setup.sh`)。它会马上启动,已下载的东西不会重复下载。关闭它的窗口就能停止模型。 +`UPDATE.bat`(`./update.sh`)只更新 Strata,不启动。更新、Docker、多张显卡、文件存放位置以及所有选项: +[docs/INSTALL.md](docs/INSTALL.md)。 + +## 该选哪个模型 + +安装程序会根据你的内存推荐一个。同一个模型有几种规格,压缩程度不同。规格越小越快,越大越聪明一些。 + +| 你的内存 | 选择 | 原因 | +| --- | --- | --- | +| **32 GB** | **Coder** | 32 GB 装得下,而且专为代码打造(如果显卡是 24 GB,Q2_0 和 IQ2_XS 也能跑) | +| **48 GB** | **IQ2_XS**(或 Q2_0,最快) | 更大的规格装不下 | +| **64 GB** | **IQ2_XS**(推荐),或 IQ3_XXS / IQ3_S | 所有规格都装得下;IQ3_S 最好,也最慢 | +| **96 GB 或以上** | **IQ3_S**,或 Unsloth 的 UD-IQ4_XS(约 4-bit) | 开着其他程序也能放下最大的规格 | + +- **[Coder](docs/MODELS.md#coder):** 编程版本,去掉了一半专家。它达到完整模型 SWE-bench Verified 分数的 91%(由其作者测得), + 32 GB 内存就能装下。代码以外的能力较弱,包括中文和其他中日韩文本(#438)。这类用途请选 Q2_0、IQ2_XS 或 IQ3_S, + 它们保留了所有专家。 +- **[Swift 1.5](docs/MODELS.md#swift-15):** 一个微调版本,回答前思考的时间短得多。你能更早拿到答案,质量基本不变。 +- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth 的约 4-bit 版本,质量介于 IQ3_S 和 UD-Q4_K_XL 之间。 + 下载 94 GB。内存少于约 80 GB 时,Strata 回答时要从 SSD 读取其中一部分,所以会更慢(NVMe SSD 有帮助)。 +- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)**(实验性):最接近完整模型。但 Strata + 回答时要从 SSD 读取其中大部分内容,所以在 64 GB 的电脑上每秒只能写 7-8.5 个 token。 +- **[OrcaRouter 的 Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** 需要手动设置,不在安装程序的菜单里。 + +规格、下载以及各配置能装下什么:[docs/MODELS.md](docs/MODELS.md)。以后想再添加模型,运行 +`SETUP.bat`(Linux:`./setup.sh --setup`)。 + +## 使用方法 + +

Strata 应用的 Monitor 标签页,旁边是一个编程智能体
+Strata 应用的 Monitor(左),编程智能体正在写视频里的宝塔花园(右)

+ +- **在浏览器里:** 打开 `http://127.0.0.1:8080`。里面有 **Chat**(聊天)、实时 **Monitor**(监控模型和你的 + GPU/CPU/内存),以及 **About**(设置和地址)。 +- **你的应用和编程智能体:** 添加一个“OpenAI 兼容”的提供商,base URL 填 **`http://127.0.0.1:8080/v1`**。 + API key 和模型名随便填都行。 + - 使用 Anthropic API 的应用:`http://127.0.0.1:8080/v1/messages`(Claude Code: + `ANTHROPIC_BASE_URL=http://127.0.0.1:8080`)。 + - Codex CLI 和其他使用 OpenAI Responses API 的应用:`/v1/responses` + ([设置方法](docs/DETAILS.md#the-responses-api-and-codex-cli))。 +- **思考:** 在聊天菜单或应用的“reasoning effort”里选择 **off、low、medium 或 high**。off 最快, + high 最适合难题。 +- **图片:** 在安装时对“Images?”选是。然后在聊天里点 **Picture**,或在你的应用里附上图片。 + AMD 显卡在 Linux 上通过处理器识别图片;在 Windows 上暂时还不行。 +- **从手机或另一台电脑访问:** `START-HERE.bat --setup --host 0.0.0.0 --api-key `。一定要设置 key。 +- **一次处理一个请求:** 默认情况下 Strata 一次只回答一个请求,其他请求排队等待。想同时回答多个, + 设置 `"parallel": 2`([BATCHING.md](docs/BATCHING.md))。在 12 GB 显卡上,这会让每个回答变慢。 +- **长提示:** Strata 会完整读入对话的第一条消息,大约每 30,000 个 token 需要 1 分钟。之后的消息几秒内就开始回答。 + +更多:[聊天记录存在哪里](docs/INSTALL.md#where-things-are-stored)、[API](docs/DETAILS.md#using-it)。 + +## 出问题了? + +- **Strata 第一次启动时电脑卡死了。** 加载模型时这是正常的。请耐心等待,不要关闭窗口。 + 10 分钟后还卡着?重启电脑,关掉其他程序再试一次,或者换一个更小的规格。 +- **下载或安装时中断了。** 再运行一次 `START-HERE.bat`(或 `./setup.sh`)。它会从中断的地方继续。 +- **非常慢,硬盘灯一直闪,或者提示“the engine stopped unexpectedly”。** 你的电脑可用内存不够。 + 关掉其他程序(浏览器很占内存),或者换一个更小的规格(Q2_0 或 IQ2_XS)。 +- **提示 8080 端口已被占用。** Strata 已经在运行了。找一下它的窗口。 + +更多问题和解决办法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。还是解决不了?提交一个 +[issue](https://github.com/Niko1221/Strata/issues),并附上 Strata 文件夹里的 `strata-.log`。发现了安全问题? +请私下报告:[SECURITY.md](SECURITY.md)。 + +## 它是怎么工作的? + +这类模型通常运行在拥有数百 GB 显存的服务器上。而你的显卡只有 12-24 GB。Strata 让模型装得下的办法是 +**把工作分摊到整台电脑上**。可以想象一个厨房:常用的东西放在台面上,其余的放在储藏室里。 + +

模型的 24,576 个专家:最常用的在显卡上,全部在内存里,一张查找表在 SSD 上

+ +- **这个模型是由 24,576 个小专家(“experts”)组成的团队。** 每个词只需要其中 10 个。 +- **你的显卡** 存放最常用的几千个专家。**你的内存** 存放全部专家, + **你的处理器** 同时处理其余的部分。**你的 SSD** 存放一张大查找表。 + +

一个小助手猜测接下来的几个词;大模型一次性全部检查,保留正确的

+ +- **先猜,再检查:** 一个小助手先猜接下来的几个词,大模型再一次性检查它们。答案完全一样,但快 1.6-1.8 倍。 +- **长文本分大块读入**(每次最多 8,192 个 token),速度超过每秒 1,000 个 token。 + +更详细的解释:[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)。每个部分及其数据: +[详细说明](docs/DETAILS.md#how-it-works)和[论文](docs/paper/Strata-Paper.pdf)。 + +## 致谢与许可证 + +模型是 Qwen 团队的 [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)。它由 +[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)、UkisAI(Swift 1.5) +和 Unsloth 压缩。Strata 使用了 [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) 的部分代码。完整致谢: +[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits)。Strata 以 [MIT 许可证](LICENSE)开源。少数部分和每个模型 +有各自的许可证([具体是哪些](docs/HOW_IT_WORKS.md#license))。 + +## 支持 Strata + +Strata 免费且开源。如果它对你有用,欢迎支持它的开发: + +

请我喝杯咖啡

diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..94be4eadee --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,44 @@ +# Security + +## Reporting a vulnerability + +Please report anything that should not be public until it is fixed **privately**, through GitHub's private +vulnerability reporting: the repository's **Security** tab, **Report a vulnerability** +([direct link](https://github.com/Niko1221/Strata/security/advisories/new)). Say what you found, how to reproduce it +(the request, the config keys involved, the Strata version), and what an attacker gains. We answer there, fix it in +a release, and credit you in the advisory unless you ask us not to. + +Ordinary hardening ideas and findings that are safe to discuss in public are welcome as an +[issue](https://github.com/Niko1221/Strata/issues) (#544 collects audit findings) or as a pull request, which we +review like our own code. + +Supported: the latest release. Fixes go into the next release, not into older ones. + +## What the server exposes + +Strata runs a model on your PC and serves it over HTTP (`serve/server.py`). Out of the box it is reachable from +this PC only. The details and every setting are in [docs/DETAILS.md](docs/DETAILS.md) ("From other devices", +"Host names", "Web pages without an API key", "Tools from MCP servers"). + +- **Where it listens.** `127.0.0.1` by default. `--host 0.0.0.0` (or `"host"` in `strata-.json`) opens it to + your network, and the server then warns when no API key is set. +- **API key.** `"api_key"` in the run config (or `STRATA_API_KEY`) is required on `/v1/*` and on every endpoint + that shows the model's state, requests or answers (`/status`, `/metrics`, `/settings`, `/mcp`, `/props`, `/slots`, + `/api/requests`, `/config`) and on every `POST`. It is compared in constant time. Set one before you open the + server to your network or put a tunnel in front of it. +- **Host check (DNS rebinding, 0.1.38).** Without an API key the server answers only requests whose `Host` is a name + it knows (`localhost`, an IP address, the address it listens on and, when it listens beyond this PC, this PC's + name). Others get 403. `"allowed_hosts"` adds names; with an API key the key decides. +- **Origin check (0.1.38).** Without an API key, a browser `POST` to `/v1/*` from another web site's page (it carries + an `Origin` header) gets 403 unless that origin is Strata's own page, `localhost`, an allowed host, or listed in + `"trusted_origins"` / `"cors_origins"`. Changing settings (`/settings`, and `/config`: the few run config keys the + page's Model settings may change - never the network, key, MCP or program keys), `/load` and `/unload` and the + MCP tools are accepted only as JSON from Strata's own page (or `"trusted_origins"`), so a page elsewhere cannot + change settings or run tools. +- **CORS** is off unless `"cors_origins"` lists origins, and then only for `/v1/*`. +- **MCP tools** are opt-in: only the servers you put in the run config, only for requests from Strata's own page + that ask for them. They run with your user's rights, and the model decides when to call them. +- **The request monitor** (`/api-monitor`, which keeps the last prompts and answers in memory) is off unless + `"api_monitor": true` is set. + +Strata has not had an outside security audit yet. diff --git a/bench/results/2026-10-03-community-2x-mi50/README.md b/bench/results/2026-10-03-community-2x-mi50/README.md new file mode 100644 index 0000000000..8d0301fba0 --- /dev/null +++ b/bench/results/2026-10-03-community-2x-mi50/README.md @@ -0,0 +1,121 @@ +# Community benchmark: 2x AMD Instinct MI50 16 GB (gfx906), Xeon E5-2666 v3 + +Measured on 2026-10-03 by [JeanP00l](https://github.com/JeanP00l) on a Linux server. This tests the Coder +(IQ1_M) with a 131,072-token context, the layer split across two MI50 cards, and the gfx906 build of #638. That +build is an opt-in HIP build, separate from the wave32 RDNA backend. + +The median decode throughput was **50.1 tok/s at 4,096 prompt tokens, 47.8 tok/s at 32,768 and 45.7 tok/s at +128,000**. Prompts were read at 321, 523 and 517 tok/s. The requests are synthetic code-explanation requests +with greedy decoding and a 256-token output cap. They do not establish general answer quality or performance on +other workloads. + +## Hardware and software + +- **GPUs:** 2x AMD Instinct MI50 16 GB (Vega 20, gfx906, wave64; HIP names them "AMD Radeon VII"). Power limit + 85 W per card, set by the owner (the cards' own limit is higher). PCIe 3.0 x16 on both. Peer access between them + works, through the CPU's root complex (no direct bridge). Blower fans; no clock locking beyond the power limit. + The engine's PCIe probe measured 13.8 GB/s host to device. +- **CPU and RAM:** Intel Xeon E5-2666 v3 (10 cores, 20 threads, AVX2, no AVX-512); the engine used 9 expert-pool + workers plus its host thread. 32 GB DDR4 quad-channel (Linux reports 31.2 GiB). 8 GiB swap file. One SATA SSD. +- **OS and runtime:** Ubuntu 24.04.4 LTS, kernel 6.8.0-138-generic, the kernel's amdgpu driver. HIP 7.14 from the + community image `mixa3607/rocm-gfx906:7.14-complete` (AMD's current ROCm no longer ships gfx906 libraries). +- **Source:** [`mi50-bench-build`](https://github.com/JeanP00l/Strata/tree/mi50-bench-build) at `ea51fd3`, which is + `main` 99f3dbd (engine 0.1.38) with #638 (at 17beb33), #639, #640 and #637 merged. #638's later commits + (a914b13) only fix the CUDA/RDNA compile and do not change the gfx906 binary's behaviour. +- **Build:** `-DSTRATA_HIP_GFX906=ON -DCMAKE_HIP_ARCHITECTURES=gfx906 -DSTRATA_PORTABLE=ON`, ROCm clang, Ninja, + llama.cpp at the pinned 3cf0325. +- **Other services:** the machine's other GPU services were stopped, so the two cards ran only Strata. CPU services + (a database, a sync daemon) kept running. This was not a fully isolated operating system. + +## Model and configuration + +**Model:** [`ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF`](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF), +IQ1_M. The repository revision was not recorded; the shard SHA-256 hashes matched the `.sha256` files downloaded +with them: + +- `Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00001-of-00002.gguf` `e11083ba855e7666b48ea3f2db6a9c3a20c18751a012cc24f948de91b7087fad` +- `Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00002-of-00002.gguf` `316b46f3a2dbd68c900f43136ab9449f9dcc3725dfd8c794847c204bc161e113` + +The native pack and the MTP draft layer were prepared with the repository's tools on 2026-09-29. The expert profile is the bundled +`data/expert-profile-coder.bin`. + +**Configuration** ([strata-coder.json](strata-coder.json); `/m` is the model directory, `/work` the build): + +- context 131,072, `--kv int8 --kv-resident 32768` (32,768 KV cells per attention layer in VRAM, the rest streamed + from RAM); +- `layer_split 27`: layers 0-26 on GPU 0, 27-47 on GPU 1. With #639 each GPU loads only its own layers' dense + weights; +- expert cache `auto`, `--vram-reserve-mib 600`: 6,912 + 5,376 slots = **all 12,288 experts in VRAM**, prefilled + from the profile, no eviction; +- `--prefill 4096`, with the prompt path borrowing 1,193 + 1,084 cache slots; +- MTP `--spec 4 --spec-min-p 0.5`; suffix drafting on (the default); +- `--pcie-frac 0`, required with `STRATA_ARENA_MMAP=1` (#640, the expert arena as a read-only mapped file; 23.4 + GiB of VRAM-held experts handed back to the OS at start); +- no vision, no calibration, no speed projection, no control vectors; +- reasoning off (`reasoning_effort: none`), temperature 0, 256 generated tokens per speed run. + +## Method + +[benchmark.py](benchmark.py) is the script of the +[RTX 5090 report](../2026-09-30-community-rtx-5090/README.md), unchanged. It builds deterministic synthetic Python +filler, puts a different nonce near the start of each request, and counts the rendered chat prompt with Strata's +tokenizer. Its request hashes and the output text of every run are in [results.json](results.json); the aggregates +are in [summary.json](summary.json). + +The server ran in a container with both cards (the repository's `serve/` and `tools/` from the source above): + +```bash +docker run --device=/dev/kfd --device=/dev/dri --group-add video --ipc=host --ulimit memlock=-1 \ + -e HSA_OVERRIDE_GFX_VERSION=9.0.6 -e STRATA_ARENA_MMAP=1 ... \ + python3 serve/server.py --engine strata --config strata-coder.json --port 8080 +python3 benchmark.py --root /opt/strata --pack /m/pack-coder --url http://127.0.0.1:8080 --out results/ +python3 tools/needle_bench.py --url http://127.0.0.1:8080 --lengths 32k,128k --depths 10,50,90 --out needles.json +``` + +- One short warm-up request is excluded. +- Three runs at each length ran serially, in increasing-length order, on the same loaded engine. +- All nine speed requests read their whole prompt: **zero reused tokens**. +- The expert cache was filled at start and kept between requests. Loading time is excluded. +- TTFT is streaming, from just before the HTTP request to the first nonempty text delta, over loopback. +- Prompt throughput is freshly read tokens / `prompt_ms`. Decode throughput is `engine_generated / decode_ms`. +- [engine.log](engine.log) is the engine's whole log for this session, including the start-up decisions. + +## Results + +Each cell is the median **[minimum-maximum]** of three runs. Every request generated 256 tokens and stopped at +the output limit. No speed request failed or was cancelled. + +| Prompt tokens | Reused | Prompt tok/s | Decode tok/s | TTFT seconds | Total seconds | +| ---: | ---: | ---: | ---: | ---: | ---: | +| 4,096 | 0 | 320.9 [309.9-324.7] | 50.1 [49.3-54.0] | 12.81 [12.66-13.26] | 17.90 [17.38-18.43] | +| 32,768 | 0 | 522.8 [522.6-523.4] | 47.8 [47.6-48.5] | 62.81 [62.75-62.84] | 68.10 [68.10-68.15] | +| 128,000 | 0 | 516.8 [516.5-516.9] | 45.7 [44.8-51.5] | 248.09 [248.06-248.27] | 253.75 [253.03-253.84] | + +- The decode expert-cache hit rate was 100% for every request. All experts are in VRAM; the CPU pool computes none. +- The 4K prompt reads at about 60% of the longer ones' speed, because a 4,096-token chunk pays the fixed per-chunk + cost once. +- Draft acceptance varies between runs (decode 44.8-51.5 tok/s at 128K) although the text is greedy. + +**Memory**, from [telemetry.jsonl](telemetry.jsonl) (1,699 one-second samples over 1,763 s, from start through +the recall checks): + +- VRAM peak: 15.42 GiB on GPU 0 and 14.70 GiB on GPU 1 (`mem_info_vram_used`, the whole card). +- RAM: `MemAvailable` never below **25.1 GiB** of 31.2. +- Swap: no growth during the run. The swap file held ~1.3 GiB from earlier use before the start. + +These are sampled values; brief peaks between samples can be missed. + +## Recall and limitations + +The repository's unchanged `tools/needle_bench.py` found **all six needles**, at depths 10%, 50% and 90% at both +lengths ([needles.json](needles.json)): + +- `32k` prompts were 25,776-25,777 tokens and `128k` prompts 103,725-103,727 tokens. +- The last 128K case reused 49,785 prompt tokens; the other five reused none. + +**Limits of this report:** +- One machine, one quantization, one configuration, and a small synthetic workload. +- Long output, sampled decoding, thinking, coding-task correctness, vision, tool use, concurrency and a sustained + thermal run were not part of this report. +- For context, on the same machine and model, llama.cpp (3cf0325, ROCm) measured 26.9 tok/s `tg128` on 2026-09-29. + That is a different workload, not a controlled comparison. diff --git a/bench/results/2026-10-03-community-2x-mi50/benchmark.py b/bench/results/2026-10-03-community-2x-mi50/benchmark.py new file mode 100644 index 0000000000..cdb6e4cc06 --- /dev/null +++ b/bench/results/2026-10-03-community-2x-mi50/benchmark.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Serial, fresh-prompt Strata benchmark. Run inside its prepared environment.""" +import argparse +import hashlib +import json +import statistics +import sys +import time +import urllib.request +from pathlib import Path + + +def get(url): + with urllib.request.urlopen(url, timeout=30) as response: + return json.load(response) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=Path("/workspace/Strata")) + parser.add_argument("--pack", type=Path, default=Path("/workspace/Strata-data/packs/iq2_xs")) + parser.add_argument("--url", default="http://127.0.0.1:18080") + parser.add_argument("--out", type=Path, default=Path("/workspace/results")) + parser.add_argument("--targets", default="4096,32768,128000") + parser.add_argument("--runs", type=int, default=3) + args = parser.parse_args() + args.out.mkdir(parents=True, exist_ok=True) + sys.path[:0] = [str(args.root), str(args.root / "tools")] + from strata_tokenizer import Tokenizer + from serve.frontend import ChatTemplate, openai_to_messages + directory = args.pack / "tokenizer" + vocab = json.loads((directory / "vocab.json").read_text()) + tokens = [None] * len(vocab) + for token, number in vocab.items(): + tokens[number] = token + tokenizer = Tokenizer(tokens, (directory / "merges.txt").read_text().splitlines(), + json.loads((directory / "token_type.json").read_text())) + template = ChatTemplate(directory / "chat_template.jinja") + initial = get(args.url + "/v1/status") + (args.out / "initial-status.json").write_text(json.dumps(initial, indent=2) + "\n") + rows = [] + + def request(content, maximum=256): + return {"model": "strata", "messages": [{"role": "user", "content": content}], + "temperature": 0, "reasoning_effort": "none", "max_tokens": maximum, + "stream": True, "stream_options": {"include_usage": True}} + + def count(req): + messages, tools, kwargs = openai_to_messages(req) + return len(tokenizer.encode(template.render(messages, tools, **kwargs), parse_special=True)) + + def perform(label, req, expected=None): + body = json.dumps(req).encode() + (args.out / (label + "-request.json")).write_bytes(body + b"\n") + print("start", label, "tokens", expected, flush=True) + chunks, texts, first, finish = [], [], None, None + started = time.perf_counter() + wire = urllib.request.Request(args.url + "/v1/chat/completions", data=body, + headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(wire, timeout=1800) as response: + for line in response: + if not line.startswith(b"data: "): + continue + payload = line[6:].strip() + if payload == b"[DONE]": + break + chunk = json.loads(payload) + chunks.append(chunk) + for choice in chunk.get("choices", []): + delta = choice.get("delta", {}) + text = (delta.get("content") or "") + (delta.get("reasoning_content") or "") + if text: + first = first if first is not None else time.perf_counter() - started + texts.append(text) + finish = choice.get("finish_reason") or finish + elapsed = time.perf_counter() - started + metrics = get(args.url + "/metrics") + engine = metrics["requests"][0] + usage = next((chunk["usage"] for chunk in reversed(chunks) if "usage" in chunk), {}) + row = {"label": label, "expected_prompt_tokens": expected, + "request_sha256": hashlib.sha256(body).hexdigest(), + "client_ttft_s": first, "client_elapsed_s": elapsed, "finish_reason": finish, + "usage": usage, "engine": engine, "text": "".join(texts)} + if expected is not None and engine["prompt_tokens"] != expected: + raise RuntimeError(f"token count mismatch: {expected} vs {engine['prompt_tokens']}") + if not texts: + raise RuntimeError(f"no text received for {label}") + (args.out / (label + "-raw.json")).write_text(json.dumps({"chunks": chunks, "row": row}, indent=2) + "\n") + rows.append(row) + (args.out / "results.json").write_text(json.dumps(rows, indent=2) + "\n") + print("done", label, json.dumps({"ttft_s": first, "elapsed_s": elapsed, "engine": engine}), flush=True) + return row + + perform("warmup", request("Reply with exactly the word READY.", 16)) + # Each nonce occurs before the filler so previous long prompts cannot be reused. + filler = "\n".join(f"def task_{i:05d}(value: int) -> int: return (value * {(i % 97) + 1} + {i}) % 100003" + for i in range(12000)) + ending = ("\n\nWrite a detailed explanation of the code above. Discuss deterministic integer transforms, " + "modulo arithmetic, testing, naming, complexity, and maintainability. Write at least 600 words.") + for target in map(int, args.targets.split(",")): + for run in range(1, args.runs + 1): + prefix = f"Benchmark nonce: series-{target}-trial-{run}.\nReview this synthetic Python module:\n" + lo, hi = 0, len(filler) + while lo < hi: + middle = (lo + hi + 1) // 2 + if count(request(prefix + filler[:middle] + ending)) <= target: + lo = middle + else: + hi = middle - 1 + req = request(prefix + filler[:lo] + ending) + actual = count(req) + if actual < target - 20 or actual > target: + raise RuntimeError(f"could not construct target {target}: {actual}") + perform(f"tokens-{target}-run-{run}", req, actual) + measured = [row for row in rows if row["label"] != "warmup"] + groups = {} + for target in map(int, args.targets.split(",")): + subset = [r for r in measured if r["label"].startswith(f"tokens-{target}-")] + values = { + "prompt_tokens": [r["engine"]["prompt_tokens"] for r in subset], + "cached_tokens": [r["engine"].get("reused", 0) for r in subset], + "output_tokens": [r["engine"]["engine_generated"] for r in subset], + "client_ttft_s": [r["client_ttft_s"] for r in subset], + "client_elapsed_s": [r["client_elapsed_s"] for r in subset], + "prefill_tok_s": [(r["engine"]["prompt_tokens"] - (r["engine"].get("reused") or 0)) / (r["engine"]["prompt_ms"] / 1000) for r in subset], + "decode_tok_s": [r["engine"]["engine_generated"] / (r["engine"]["decode_ms"] / 1000) for r in subset], + } + groups[str(target)] = {key: {"median": statistics.median(v), "min": min(v), "max": max(v)} + for key, v in values.items()} + (args.out / "summary.json").write_text(json.dumps(groups, indent=2) + "\n") + print("summary", json.dumps(groups), flush=True) + + +if __name__ == "__main__": + main() diff --git a/bench/results/2026-10-03-community-2x-mi50/initial-status.json b/bench/results/2026-10-03-community-2x-mi50/initial-status.json new file mode 100644 index 0000000000..fd96e13c05 --- /dev/null +++ b/bench/results/2026-10-03-community-2x-mi50/initial-status.json @@ -0,0 +1,50 @@ +{ + "service": "strata", + "model": "qwen3.8-flash-next-coder", + "loaded": true, + "auto_load": true, + "structured_output": { + "formats": [ + "json_object", + "json_schema" + ], + "method": "prompt_and_validate", + "constrained_decoding": false, + "stream_buffered": true + }, + "engine": "0.1.38", + "started": 1791055531, + "uptime_s": 2, + "cache_max_tokens": 131072, + "context": { + "native": 131072, + "max_positions": 131072 + }, + "concurrency": { + "serving": 1, + "requested": 1 + }, + "dialects": [ + "/v1/chat/completions", + "/v1/messages" + ], + "vision": { + "enabled": false, + "available": false, + "error": null + }, + "activity": { + "requests": 0, + "in_flight": 0, + "last_request_at": null + }, + "last_timings": null, + "machine": { + "at": 1791055534, + "gpu": null, + "ram": { + "used_gib": 4.9, + "total_gib": 31.2 + } + } +} diff --git a/bench/results/2026-10-03-community-2x-mi50/needles.json b/bench/results/2026-10-03-community-2x-mi50/needles.json new file mode 100644 index 0000000000..0eda889604 --- /dev/null +++ b/bench/results/2026-10-03-community-2x-mi50/needles.json @@ -0,0 +1,56 @@ +[ + { + "length": "32k", + "depth": 10, + "prompt_tokens": 25776, + "seconds": 52.6, + "found": true, + "word": "meadow-copper-504", + "answer": "meadow-copper-504" + }, + { + "length": "32k", + "depth": 50, + "prompt_tokens": 25776, + "seconds": 50.7, + "found": true, + "word": "falcon-quartz-940", + "answer": "falcon-quartz-940" + }, + { + "length": "32k", + "depth": 90, + "prompt_tokens": 25777, + "seconds": 50.8, + "found": true, + "word": "willow-cobalt-696", + "answer": "willow-cobalt-696" + }, + { + "length": "128k", + "depth": 10, + "prompt_tokens": 103727, + "seconds": 206.9, + "found": true, + "word": "falcon-saffron-138", + "answer": "falcon-saffron-138" + }, + { + "length": "128k", + "depth": 50, + "prompt_tokens": 103725, + "seconds": 205.9, + "found": true, + "word": "quartz-tundra-528", + "answer": "quartz-tundra-528" + }, + { + "length": "128k", + "depth": 90, + "prompt_tokens": 103725, + "seconds": 112.5, + "found": true, + "word": "quartz-glacier-192", + "answer": "quartz-glacier-192" + } +] \ No newline at end of file diff --git a/bench/results/2026-10-03-community-2x-mi50/results.json b/bench/results/2026-10-03-community-2x-mi50/results.json new file mode 100644 index 0000000000..10d3c2b675 --- /dev/null +++ b/bench/results/2026-10-03-community-2x-mi50/results.json @@ -0,0 +1,382 @@ +[ + { + "label": "warmup", + "expected_prompt_tokens": null, + "request_sha256": "b63e57873441a996a529b246a66cf12de70a7c671777f99329354d5c195f788f", + "client_ttft_s": 0.44287488800182473, + "client_elapsed_s": 0.5121355729934294, + "finish_reason": "stop", + "usage": { + "prompt_tokens": 19, + "completion_tokens": 2, + "total_tokens": 21, + "prompt_tokens_details": { + "cached_tokens": 0 + } + }, + "engine": { + "projection": null, + "time": 1791055534.084382, + "duration_s": 0.5, + "finish": "stop", + "prompt_tokens": 19, + "reused": 0, + "output_tokens": 2, + "prompt_total": 19, + "prompt_read": 19, + "engine_generated": 2, + "prompt_ms": 398.1, + "decode_ms": 108.4, + "decode_tok_s": 18.5, + "hit_rate": 1.0, + "ram_blobs": 0, + "file_blobs": 0, + "file_mb": 0.0, + "drafts_offered": 3, + "drafts_accepted": 3 + }, + "text": "READY" + }, + { + "label": "tokens-4096-run-1", + "expected_prompt_tokens": 4096, + "request_sha256": "5d94c652067354bbe72930d2bdad3886564f08cf2546b32411af02d33ad3ce05", + "client_ttft_s": 13.26403624098748, + "client_elapsed_s": 18.427036346998648, + "finish_reason": "length", + "usage": { + "prompt_tokens": 4096, + "completion_tokens": 256, + "total_tokens": 4352, + "prompt_tokens_details": { + "cached_tokens": 0 + } + }, + "engine": { + "projection": null, + "time": 1791055535.8394356, + "duration_s": 18.4, + "finish": "length", + "prompt_tokens": 4096, + "reused": 0, + "output_tokens": 256, + "prompt_total": 4096, + "prompt_read": 4096, + "engine_generated": 256, + "prompt_ms": 13216.0, + "decode_ms": 5189.0, + "decode_tok_s": 49.3, + "hit_rate": 1.0, + "ram_blobs": 0, + "file_blobs": 0, + "file_mb": 0.0, + "drafts_offered": 210, + "drafts_accepted": 145 + }, + "text": "The provided Python module presents a series of deterministic integer transforms, each encapsulated within a uniquely named function. These functions, labeled from `task_00000` to `task_00112` (and potentially beyond), follow a consistent mathematical pattern: they take an integer input, apply a linear transformation of the form `(value * multiplier + offset) % modulus`, and return the result. This structure is a classic example of a linear congruential generator (LCG) component, often used in pseudo-random number generation, hashing, or cryptographic primitives, though here it appears to be a synthetic benchmark or test harness.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nEach function implements a linear congruential step: `f(x) = (a * x + c) % m`, where `a` is the multiplier, `c` is the increment, and `m` is the modulus (100003 in all cases). The modulus 100003 is a prime number, which is significant because it ensures that the multiplicative group modulo 100003 is cyclic, and for certain choices of `a` and `c`, the transformation can be a permutation of the" + }, + { + "label": "tokens-4096-run-2", + "expected_prompt_tokens": 4096, + "request_sha256": "8da8a04e70e4d916f6630729082bdfb9d6ebbb47117aad60bc0541bce198a213", + "client_ttft_s": 12.663496508001117, + "client_elapsed_s": 17.377855258993804, + "finish_reason": "length", + "usage": { + "prompt_tokens": 4096, + "completion_tokens": 256, + "total_tokens": 4352, + "prompt_tokens_details": { + "cached_tokens": 0 + } + }, + "engine": { + "projection": null, + "time": 1791055555.3915784, + "duration_s": 17.4, + "finish": "length", + "prompt_tokens": 4096, + "reused": 0, + "output_tokens": 256, + "prompt_total": 4096, + "prompt_read": 4096, + "engine_generated": 256, + "prompt_ms": 12615.4, + "decode_ms": 4740.3, + "decode_tok_s": 54.0, + "hit_rate": 1.0, + "ram_blobs": 0, + "file_blobs": 0, + "file_mb": 0.0, + "drafts_offered": 208, + "drafts_accepted": 156 + }, + "text": "The provided Python module consists of a series of functions, each named with a specific pattern (`task_XXXXX`), that perform a deterministic integer transformation. Each function takes a single integer argument `value` and returns an integer result computed using a linear transformation followed by a modulo operation. The general form of each function is:\n\n```python\ndef task_XXXXX(value: int) -> int:\n return (value * A + B) % 100003\n```\n\nwhere `A` and `B` are constants specific to each function. The modulus `100003` is a prime number, which is significant in modular arithmetic because it ensures that the multiplicative group modulo 100003 is cyclic and that every non-zero element has a multiplicative inverse. This property is often exploited in cryptographic algorithms, hash functions, and pseudo-random number generators.\n\n### Deterministic Integer Transforms\n\nEach function implements a linear congruential transformation, a common technique in number theory and computer science. The transformation `value * A + B` is an affine transformation over the integers. When combined with the modulo operation, it becomes a function from the finite field (or ring) of integers modulo 10" + }, + { + "label": "tokens-4096-run-3", + "expected_prompt_tokens": 4096, + "request_sha256": "946079edb58e764050805886156dc8f877cc29e1ca8d01c84e9a1e7917d49492", + "client_ttft_s": 12.811999237994314, + "client_elapsed_s": 17.900370211005793, + "finish_reason": "length", + "usage": { + "prompt_tokens": 4096, + "completion_tokens": 256, + "total_tokens": 4352, + "prompt_tokens_details": { + "cached_tokens": 0 + } + }, + "engine": { + "projection": null, + "time": 1791055573.9388902, + "duration_s": 17.9, + "finish": "length", + "prompt_tokens": 4096, + "reused": 0, + "output_tokens": 256, + "prompt_total": 4096, + "prompt_read": 4096, + "engine_generated": 256, + "prompt_ms": 12763.9, + "decode_ms": 5114.7, + "decode_tok_s": 50.1, + "hit_rate": 1.0, + "ram_blobs": 0, + "file_blobs": 0, + "file_mb": 0.0, + "drafts_offered": 222, + "drafts_accepted": 146 + }, + "text": "The provided Python module presents a series of deterministic integer transforms, each encapsulated within a uniquely named function. These functions, labeled from `task_00000` to `task_00112` (and potentially beyond), follow a consistent mathematical pattern: they take an integer input, apply a linear transformation, and return the result modulo a large prime number, specifically 100003. This structure serves as a synthetic benchmark for evaluating code generation, pattern recognition, and algorithmic consistency in automated systems.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nAt the core of each function is the expression `(value * multiplier + offset) % 100003`. This is a classic linear congruential generator (LCG) step, often used in pseudo-random number generation or cryptographic hashing, though here it is applied deterministically to a single input. The multiplier and offset vary per function, creating a unique mapping for each task. For instance, `task_00000` uses a multiplier of 1 and an offset of 0, effectively returning `value % 100003`. In contrast, `task_00097` uses a multiplier of 1" + }, + { + "label": "tokens-32768-run-1", + "expected_prompt_tokens": 32768, + "request_sha256": "e37cfc2ab9867d91130927440df24a57255a736b8d0d829d07d828e22e3de572", + "client_ttft_s": 62.74767600699852, + "client_elapsed_s": 68.09618598900852, + "finish_reason": "length", + "usage": { + "prompt_tokens": 32768, + "completion_tokens": 256, + "total_tokens": 33024, + "prompt_tokens_details": { + "cached_tokens": 0 + } + }, + "engine": { + "projection": null, + "time": 1791055594.1634562, + "duration_s": 68.0, + "finish": "length", + "prompt_tokens": 32768, + "reused": 0, + "output_tokens": 256, + "prompt_total": 32768, + "prompt_read": 32768, + "engine_generated": 256, + "prompt_ms": 62608.4, + "decode_ms": 5375.3, + "decode_tok_s": 47.6, + "hit_rate": 1.0, + "ram_blobs": 0, + "file_blobs": 0, + "file_mb": 0.0, + "drafts_offered": 230, + "drafts_accepted": 152 + }, + "text": "This Python module presents a highly repetitive, synthetic collection of functions that serve as a benchmark for deterministic integer transforms. The code consists of a series of functions named `task_00000` through `task_00888` (and potentially beyond), each performing a specific linear transformation on an input integer `value` and returning the result modulo 100003.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nAt the core of each function is a linear congruential generator (LCG) style operation: `(value * multiplier + addend) % 100003`. The modulus, 100003, is a prime number. This choice of modulus is significant because it ensures that the multiplicative group of integers modulo 100003 is cyclic, which is a desirable property for cryptographic or pseudo-random number generation contexts, although here it is used for deterministic hashing or transformation.\n\nThe structure of the functions reveals a pattern in the `multiplier` and `addend` parameters. For the first 97 functions (`task_00000` to `task_00096`), the multiplier increases linearly from 1 to" + }, + { + "label": "tokens-32768-run-2", + "expected_prompt_tokens": 32768, + "request_sha256": "ed2dfaa11aecad9f4563f33bb5f5d4b8704486508700b8fa572722cc1225ee26", + "client_ttft_s": 62.81415034100064, + "client_elapsed_s": 68.14694995200261, + "finish_reason": "length", + "usage": { + "prompt_tokens": 32768, + "completion_tokens": 256, + "total_tokens": 33024, + "prompt_tokens_details": { + "cached_tokens": 0 + } + }, + "engine": { + "projection": null, + "time": 1791055664.4150822, + "duration_s": 68.0, + "finish": "length", + "prompt_tokens": 32768, + "reused": 0, + "output_tokens": 256, + "prompt_total": 32768, + "prompt_read": 32768, + "engine_generated": 256, + "prompt_ms": 62675.9, + "decode_ms": 5359.3, + "decode_tok_s": 47.8, + "hit_rate": 1.0, + "ram_blobs": 0, + "file_blobs": 0, + "file_mb": 0.0, + "drafts_offered": 222, + "drafts_accepted": 147 + }, + "text": "This Python module presents a highly repetitive, synthetic collection of functions that perform deterministic integer transforms. Each function follows a strict pattern: it accepts an integer `value`, applies a linear transformation of the form `(value * multiplier + offset) % modulus`, and returns the result. The modulus is consistently set to `100003`, which is a prime number. This choice of modulus is significant because it ensures that the multiplicative group of integers modulo 100003 is cyclic, and every non-zero multiplier has a unique modular inverse. This property is often desirable in cryptographic or hashing contexts, though here it appears to be used for simple arithmetic obfuscation or benchmarking.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nThe core operation in each function is a linear congruential generator (LCG) step, commonly used in pseudo-random number generation. The formula $f(x) = (ax + c) \\pmod m$ defines an affine transformation over the finite field $\\mathbb{Z}_m$. In this module, $m = 100003$. The multipliers ($a$) and offsets ($c$) vary per function. For example, `task_00000`" + }, + { + "label": "tokens-32768-run-3", + "expected_prompt_tokens": 32768, + "request_sha256": "2a3c7342622e80c3cd8b37e062f0a2f64640c119ece8cccbf168639041eac079", + "client_ttft_s": 62.844907227001386, + "client_elapsed_s": 68.09568050300004, + "finish_reason": "length", + "usage": { + "prompt_tokens": 32768, + "completion_tokens": 256, + "total_tokens": 33024, + "prompt_tokens_details": { + "cached_tokens": 0 + } + }, + "engine": { + "projection": null, + "time": 1791055734.689819, + "duration_s": 68.0, + "finish": "length", + "prompt_tokens": 32768, + "reused": 0, + "output_tokens": 256, + "prompt_total": 32768, + "prompt_read": 32768, + "engine_generated": 256, + "prompt_ms": 62706.6, + "decode_ms": 5277.5, + "decode_tok_s": 48.5, + "hit_rate": 1.0, + "ram_blobs": 0, + "file_blobs": 0, + "file_mb": 0.0, + "drafts_offered": 234, + "drafts_accepted": 153 + }, + "text": "This Python module presents a highly repetitive, synthetic collection of functions that perform deterministic integer transforms. Each function follows a strict pattern: it accepts an integer `value`, applies a linear transformation of the form `(value * multiplier + offset) % modulus`, and returns the result. The modulus is consistently set to `100003`, which is a prime number. This choice of modulus is significant because it ensures that the multiplicative group of integers modulo 100003 is cyclic, meaning that for any non-zero multiplier, the transformation is bijective (a permutation) over the set of integers modulo 100003. This property is often desirable in cryptographic contexts or pseudo-random number generation, although here it appears to be used for benchmarking or synthetic data generation.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nThe core operation in each function is a linear congruential generator (LCG) step, defined by the formula $f(x) = (ax + c) \\pmod m$. In this module, $m = 100003$. The multiplier $a$ and the increment $c$ vary per function. For example, `task_00000` uses $a" + }, + { + "label": "tokens-128000-run-1", + "expected_prompt_tokens": 128000, + "request_sha256": "3235aac3f934cf1d889c25973ca172b355509dc2522fdfb463fbefe979a9075d", + "client_ttft_s": 248.26734286299325, + "client_elapsed_s": 253.83500957599608, + "finish_reason": "length", + "usage": { + "prompt_tokens": 128000, + "completion_tokens": 256, + "total_tokens": 128256, + "prompt_tokens_details": { + "cached_tokens": 0 + } + }, + "engine": { + "projection": null, + "time": 1791055809.118869, + "duration_s": 253.5, + "finish": "length", + "prompt_tokens": 128000, + "reused": 0, + "output_tokens": 256, + "prompt_total": 128000, + "prompt_read": 128000, + "engine_generated": 256, + "prompt_ms": 247841.6, + "decode_ms": 5596.2, + "decode_tok_s": 45.7, + "hit_rate": 1.0, + "ram_blobs": 0, + "file_blobs": 0, + "file_mb": 0.0, + "drafts_offered": 232, + "drafts_accepted": 152 + }, + "text": "This Python module represents a synthetic benchmark designed to test the performance and correctness of integer arithmetic operations, specifically focusing on linear transforms and modular arithmetic. The code consists of a series of functions, each named `task_XXXXX`, where `XXXXX` is a zero-padded integer index. Each function accepts a single integer argument `value` and returns an integer result.\n\n### 1. Deterministic Integer Transforms\n\nThe core logic of every function in this module is identical in structure:\n```python\nreturn (value * A + B) % M\n```\nWhere:\n- `value` is the input integer.\n- `A` is a multiplier (coefficient).\n- `B` is an additive constant.\n- `M` is the modulus, which is consistently `100003` throughout the entire module.\n\nThis represents a **linear congruential transform**. In computer science, such transforms are fundamental in pseudorandom number generation (LCG), hashing functions, and cryptographic primitives. The specific form `(value * A + B) % M` ensures that the output is always within the range `[0, M-1]`, regardless of how large the input `value` is.\n\n### 2" + }, + { + "label": "tokens-128000-run-2", + "expected_prompt_tokens": 128000, + "request_sha256": "5f4b8bc9f0afc7765320abd89960c0938e0d7cf5974e962fdbd0613fef31c8d4", + "client_ttft_s": 248.0881588330085, + "client_elapsed_s": 253.0268927290017, + "finish_reason": "length", + "usage": { + "prompt_tokens": 128000, + "completion_tokens": 256, + "total_tokens": 128256, + "prompt_tokens_details": { + "cached_tokens": 0 + } + }, + "engine": { + "projection": null, + "time": 1791056068.9450965, + "duration_s": 252.7, + "finish": "length", + "prompt_tokens": 128000, + "reused": 0, + "output_tokens": 256, + "prompt_total": 128000, + "prompt_read": 128000, + "engine_generated": 256, + "prompt_ms": 247669.4, + "decode_ms": 4967.1, + "decode_tok_s": 51.5, + "hit_rate": 1.0, + "ram_blobs": 0, + "file_blobs": 0, + "file_mb": 0.0, + "drafts_offered": 205, + "drafts_accepted": 163 + }, + "text": "This Python module represents a synthetic benchmark designed to test the performance and correctness of integer arithmetic operations, specifically focusing on linear transforms and modular arithmetic. The code consists of a series of functions, each named `task_XXXXX`, where `XXXXX` is a zero-padded integer index. Each function accepts a single integer argument `value` and returns an integer result.\n\n### 1. Deterministic Integer Transforms\n\nThe core logic of each function is a linear transformation of the form:\n$$ f(x) = (a \\cdot x + b) \\pmod m $$\nwhere:\n- $x$ is the input `value`.\n- $a$ is the multiplier (coefficient).\n- $b$ is the additive constant.\n- $m$ is the modulus, which is consistently set to `100003` across all functions.\n\nThe multiplier $a$ and the constant $b$ vary deterministically based on the function's index. For example:\n- `task_00000`: $a=1, b=0$\n- `task_00001`: $a=2, b=1$\n- `task_00002`:" + }, + { + "label": "tokens-128000-run-3", + "expected_prompt_tokens": 128000, + "request_sha256": "5c3fb71db9f9f44765f28389d1032f2c8b6687d1bf5ca6f33aeb924072cfa6e4", + "client_ttft_s": 248.0611828550027, + "client_elapsed_s": 253.7529949669988, + "finish_reason": "length", + "usage": { + "prompt_tokens": 128000, + "completion_tokens": 256, + "total_tokens": 128256, + "prompt_tokens_details": { + "cached_tokens": 0 + } + }, + "engine": { + "projection": null, + "time": 1791056328.0391443, + "duration_s": 253.4, + "finish": "length", + "prompt_tokens": 128000, + "reused": 0, + "output_tokens": 256, + "prompt_total": 128000, + "prompt_read": 128000, + "engine_generated": 256, + "prompt_ms": 247650.3, + "decode_ms": 5720.0, + "decode_tok_s": 44.8, + "hit_rate": 1.0, + "ram_blobs": 0, + "file_blobs": 0, + "file_mb": 0.0, + "drafts_offered": 235, + "drafts_accepted": 149 + }, + "text": "This Python module represents a synthetic benchmark designed to test the performance and correctness of integer arithmetic operations, specifically focusing on linear transformations combined with modular arithmetic. The code consists of a series of functions, each named `task_XXXXX`, where `XXXXX` is a zero-padded integer index. Each function accepts a single integer argument `value` and returns a new integer calculated using the formula:\n\n$$ f(value) = (value \\times A + B) \\pmod{M} $$\n\nwhere $A$ is a multiplier, $B$ is an additive constant, and $M$ is the modulus, which is consistently set to `100003` throughout the module.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nThe core logic of each function is a deterministic linear congruential transformation. In computer science, such transformations are fundamental to pseudorandom number generators (PRNGs), hash functions, and cryptographic primitives. The specific form $ (value \\times A + B) \\pmod{M} $ ensures that the output is always within the range $[0, M-1]$.\n\nThe modulus $M = 100003$ is a prime number. This choice is significant because" + } +] diff --git a/bench/results/2026-10-03-community-2x-mi50/strata-coder.json b/bench/results/2026-10-03-community-2x-mi50/strata-coder.json new file mode 100644 index 0000000000..d766bbf9c2 --- /dev/null +++ b/bench/results/2026-10-03-community-2x-mi50/strata-coder.json @@ -0,0 +1,45 @@ +{ + "exe": "/work/build-906/strata", + "args": [ + "--pack", + "/m/pack-coder", + "--native", + "/m/coder/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00001-of-00002.gguf", + "--ple-gguf", + "/m/coder/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00002-of-00002.gguf", + "--expert-profile", + "/opt/strata/data/expert-profile-coder.bin", + "--expert-cache", + "auto", + "--prefill", + "4096", + "--spec", + "4", + "--spec-min-p", + "0.5", + "--mtp", + "/m/mtp/rt", + "--max-context", + "131072", + "--kv", + "int8", + "--vram-reserve-mib", + "600", + "--kv-resident", + "32768", + "--pcie-frac", + "0" + ], + "cwd": "/opt/strata", + "tokenizer": "/m/pack-coder/tokenizer", + "model_name": "qwen3.8-flash-next-coder", + "log": "/work/strata-coder.log", + "lib_dirs": [], + "port": 8080, + "host": "0.0.0.0", + "gpu": [ + 0, + 1 + ], + "layer_split": "27" +} \ No newline at end of file diff --git a/bench/results/2026-10-03-community-2x-mi50/summary.json b/bench/results/2026-10-03-community-2x-mi50/summary.json new file mode 100644 index 0000000000..eddf5ac372 --- /dev/null +++ b/bench/results/2026-10-03-community-2x-mi50/summary.json @@ -0,0 +1,113 @@ +{ + "4096": { + "prompt_tokens": { + "median": 4096, + "min": 4096, + "max": 4096 + }, + "cached_tokens": { + "median": 0, + "min": 0, + "max": 0 + }, + "output_tokens": { + "median": 256, + "min": 256, + "max": 256 + }, + "client_ttft_s": { + "median": 12.811999237994314, + "min": 12.663496508001117, + "max": 13.26403624098748 + }, + "client_elapsed_s": { + "median": 17.900370211005793, + "min": 17.377855258993804, + "max": 18.427036346998648 + }, + "prefill_tok_s": { + "median": 320.90505253096626, + "min": 309.9273607748184, + "max": 324.68253087496237 + }, + "decode_tok_s": { + "median": 50.05181144544157, + "min": 49.3351320100212, + "max": 54.00502077927557 + } + }, + "32768": { + "prompt_tokens": { + "median": 32768, + "min": 32768, + "max": 32768 + }, + "cached_tokens": { + "median": 0, + "min": 0, + "max": 0 + }, + "output_tokens": { + "median": 256, + "min": 256, + "max": 256 + }, + "client_ttft_s": { + "median": 62.81415034100064, + "min": 62.74767600699852, + "max": 62.844907227001386 + }, + "client_elapsed_s": { + "median": 68.09618598900852, + "min": 68.09568050300004, + "max": 68.14694995200261 + }, + "prefill_tok_s": { + "median": 522.8165850031671, + "min": 522.5606236026192, + "max": 523.3802492956216 + }, + "decode_tok_s": { + "median": 47.767432313921596, + "min": 47.625248823321485, + "max": 48.50781620085268 + } + }, + "128000": { + "prompt_tokens": { + "median": 128000, + "min": 128000, + "max": 128000 + }, + "cached_tokens": { + "median": 0, + "min": 0, + "max": 0 + }, + "output_tokens": { + "median": 256, + "min": 256, + "max": 256 + }, + "client_ttft_s": { + "median": 248.0881588330085, + "min": 248.0611828550027, + "max": 248.26734286299325 + }, + "client_elapsed_s": { + "median": 253.7529949669988, + "min": 253.0268927290017, + "max": 253.83500957599608 + }, + "prefill_tok_s": { + "median": 516.8179839737974, + "min": 516.4588995551998, + "max": 516.857843499483 + }, + "decode_tok_s": { + "median": 45.745327186305, + "min": 44.75524475524476, + "max": 51.5391274586781 + } + } +} diff --git a/bench/results/2026-10-03-community-2x-mi50/telemetry.jsonl b/bench/results/2026-10-03-community-2x-mi50/telemetry.jsonl new file mode 100644 index 0000000000..bafdea8089 --- /dev/null +++ b/bench/results/2026-10-03-community-2x-mi50/telemetry.jsonl @@ -0,0 +1,1699 @@ +{"t": 1791055498.231, "mem_avail_kib": 30786360, "swap_free_kib": 7052840, "vram_used": [10924032,10924032]} +{"t": 1791055499.271, "mem_avail_kib": 30377400, "swap_free_kib": 7052840, "vram_used": [10964992,10964992]} +{"t": 1791055500.334, "mem_avail_kib": 30173356, "swap_free_kib": 7053096, "vram_used": [1064579072,11710464]} +{"t": 1791055501.391, "mem_avail_kib": 30172568, "swap_free_kib": 7053096, "vram_used": [1064579072,11710464]} +{"t": 1791055502.433, "mem_avail_kib": 30169280, "swap_free_kib": 7053096, "vram_used": [1064579072,11710464]} +{"t": 1791055503.477, "mem_avail_kib": 30191972, "swap_free_kib": 7053096, "vram_used": [1588826112,11661312]} +{"t": 1791055504.566, "mem_avail_kib": 30031712, "swap_free_kib": 7053096, "vram_used": [2394238976,892637184]} +{"t": 1791055505.624, "mem_avail_kib": 30051892, "swap_free_kib": 7053096, "vram_used": [2394238976,892637184]} +{"t": 1791055506.686, "mem_avail_kib": 30032244, "swap_free_kib": 7053096, "vram_used": [2394238976,892637184]} +{"t": 1791055507.731, "mem_avail_kib": 30040204, "swap_free_kib": 7053096, "vram_used": [2394189824,1863573504]} +{"t": 1791055508.773, "mem_avail_kib": 28961132, "swap_free_kib": 7053096, "vram_used": [2820055040,2377576448]} +{"t": 1791055509.818, "mem_avail_kib": 28300928, "swap_free_kib": 7053096, "vram_used": [2820943872,3018100736]} +{"t": 1791055510.884, "mem_avail_kib": 28350460, "swap_free_kib": 7053096, "vram_used": [2820943872,3726938112]} +{"t": 1791055512.032, "mem_avail_kib": 28368196, "swap_free_kib": 7053096, "vram_used": [2820943872,3726938112]} +{"t": 1791055513.107, "mem_avail_kib": 28343544, "swap_free_kib": 7053096, "vram_used": [2820943872,3726938112]} +{"t": 1791055514.156, "mem_avail_kib": 28271040, "swap_free_kib": 7053096, "vram_used": [16318500864,3802742784]} +{"t": 1791055515.207, "mem_avail_kib": 28260748, "swap_free_kib": 7053096, "vram_used": [16318509056,3802742784]} +{"t": 1791055516.252, "mem_avail_kib": 28267500, "swap_free_kib": 7053096, "vram_used": [16318509056,3802742784]} +{"t": 1791055517.291, "mem_avail_kib": 28265100, "swap_free_kib": 7053096, "vram_used": [16318509056,3802742784]} +{"t": 1791055518.332, "mem_avail_kib": 28258636, "swap_free_kib": 7053096, "vram_used": [16318509056,3802742784]} +{"t": 1791055519.379, "mem_avail_kib": 28277724, "swap_free_kib": 7053096, "vram_used": [16318509056,15456628736]} +{"t": 1791055520.431, "mem_avail_kib": 28278036, "swap_free_kib": 7053096, "vram_used": [16318509056,15456628736]} +{"t": 1791055521.479, "mem_avail_kib": 28278204, "swap_free_kib": 7053096, "vram_used": [16318509056,15456628736]} +{"t": 1791055522.525, "mem_avail_kib": 28272556, "swap_free_kib": 7053096, "vram_used": [16318509056,15456628736]} +{"t": 1791055523.573, "mem_avail_kib": 28265648, "swap_free_kib": 7053096, "vram_used": [16318509056,15456628736]} +{"t": 1791055524.619, "mem_avail_kib": 28246724, "swap_free_kib": 7053096, "vram_used": [16318509056,15456628736]} +{"t": 1791055525.666, "mem_avail_kib": 28241676, "swap_free_kib": 7053352, "vram_used": [16318509056,15456628736]} +{"t": 1791055526.719, "mem_avail_kib": 28245824, "swap_free_kib": 7053608, "vram_used": [16318509056,15456628736]} +{"t": 1791055527.769, "mem_avail_kib": 28248292, "swap_free_kib": 7053608, "vram_used": [16318509056,15456628736]} +{"t": 1791055528.860, "mem_avail_kib": 28238060, "swap_free_kib": 7053608, "vram_used": [16318509056,15456628736]} +{"t": 1791055529.899, "mem_avail_kib": 28231984, "swap_free_kib": 7053608, "vram_used": [16318513152,15456632832]} +{"t": 1791055530.936, "mem_avail_kib": 27595720, "swap_free_kib": 7053608, "vram_used": [16374341632,15571443712]} +{"t": 1791055531.981, "mem_avail_kib": 27529568, "swap_free_kib": 7053608, "vram_used": [16475328512,15663824896]} +{"t": 1791055533.023, "mem_avail_kib": 27517964, "swap_free_kib": 7053608, "vram_used": [16475328512,15663824896]} +{"t": 1791055534.065, "mem_avail_kib": 27358404, "swap_free_kib": 7053608, "vram_used": [16475328512,15663824896]} +{"t": 1791055535.108, "mem_avail_kib": 27225240, "swap_free_kib": 7053608, "vram_used": [16557064192,15718391808]} +{"t": 1791055536.145, "mem_avail_kib": 27226976, "swap_free_kib": 7053608, "vram_used": [16557481984,15718391808]} +{"t": 1791055537.183, "mem_avail_kib": 27227508, "swap_free_kib": 7053608, "vram_used": [16560840704,15718416384]} +{"t": 1791055538.221, "mem_avail_kib": 27226424, "swap_free_kib": 7053608, "vram_used": [16560840704,15718416384]} +{"t": 1791055539.259, "mem_avail_kib": 27218752, "swap_free_kib": 7053608, "vram_used": [16560840704,15718416384]} +{"t": 1791055540.297, "mem_avail_kib": 27224788, "swap_free_kib": 7054120, "vram_used": [16560840704,15718416384]} +{"t": 1791055541.333, "mem_avail_kib": 27209492, "swap_free_kib": 7054120, "vram_used": [16561463296,15718420480]} +{"t": 1791055542.372, "mem_avail_kib": 27218412, "swap_free_kib": 7054120, "vram_used": [16561463296,15718420480]} +{"t": 1791055543.410, "mem_avail_kib": 27208168, "swap_free_kib": 7054120, "vram_used": [16561471488,15720955904]} +{"t": 1791055544.449, "mem_avail_kib": 27197644, "swap_free_kib": 7054120, "vram_used": [16561483776,15728717824]} +{"t": 1791055545.486, "mem_avail_kib": 27225460, "swap_free_kib": 7054120, "vram_used": [16561483776,15728717824]} +{"t": 1791055546.522, "mem_avail_kib": 27222844, "swap_free_kib": 7054120, "vram_used": [16561483776,15728717824]} +{"t": 1791055547.560, "mem_avail_kib": 27216632, "swap_free_kib": 7054120, "vram_used": [16561483776,15728717824]} +{"t": 1791055548.601, "mem_avail_kib": 27109628, "swap_free_kib": 7054120, "vram_used": [16561487872,15729442816]} +{"t": 1791055549.641, "mem_avail_kib": 27107924, "swap_free_kib": 7054120, "vram_used": [16562458624,15730737152]} +{"t": 1791055550.677, "mem_avail_kib": 27109980, "swap_free_kib": 7054120, "vram_used": [16562458624,15730737152]} +{"t": 1791055551.717, "mem_avail_kib": 27102036, "swap_free_kib": 7054120, "vram_used": [16562458624,15730737152]} +{"t": 1791055552.756, "mem_avail_kib": 27094240, "swap_free_kib": 7054120, "vram_used": [16562458624,15730737152]} +{"t": 1791055553.797, "mem_avail_kib": 27104564, "swap_free_kib": 7054120, "vram_used": [16562458624,15730737152]} +{"t": 1791055554.835, "mem_avail_kib": 27102540, "swap_free_kib": 7054120, "vram_used": [16562458624,15730737152]} +{"t": 1791055555.873, "mem_avail_kib": 27188636, "swap_free_kib": 7054120, "vram_used": [16562458624,15730737152]} +{"t": 1791055556.915, "mem_avail_kib": 27199532, "swap_free_kib": 7054376, "vram_used": [16562458624,15730737152]} +{"t": 1791055557.953, "mem_avail_kib": 27201496, "swap_free_kib": 7054376, "vram_used": [16562458624,15730737152]} +{"t": 1791055558.992, "mem_avail_kib": 27210500, "swap_free_kib": 7054376, "vram_used": [16562458624,15730737152]} +{"t": 1791055560.029, "mem_avail_kib": 27205028, "swap_free_kib": 7054376, "vram_used": [16562458624,15730737152]} +{"t": 1791055561.065, "mem_avail_kib": 27217216, "swap_free_kib": 7054376, "vram_used": [16562458624,15730737152]} +{"t": 1791055562.104, "mem_avail_kib": 27223476, "swap_free_kib": 7054376, "vram_used": [16562458624,15730737152]} +{"t": 1791055563.142, "mem_avail_kib": 27227648, "swap_free_kib": 7054376, "vram_used": [16562458624,15730737152]} +{"t": 1791055564.181, "mem_avail_kib": 27240288, "swap_free_kib": 7054376, "vram_used": [16562458624,15730737152]} +{"t": 1791055565.217, "mem_avail_kib": 27237232, "swap_free_kib": 7054376, "vram_used": [16562458624,15730737152]} +{"t": 1791055566.257, "mem_avail_kib": 27223536, "swap_free_kib": 7054376, "vram_used": [16562458624,15730737152]} +{"t": 1791055567.293, "mem_avail_kib": 27211536, "swap_free_kib": 7054376, "vram_used": [16562458624,15730737152]} +{"t": 1791055568.330, "mem_avail_kib": 27105576, "swap_free_kib": 7054376, "vram_used": [16562458624,15730737152]} +{"t": 1791055569.368, "mem_avail_kib": 27108292, "swap_free_kib": 7054376, "vram_used": [16562458624,15730737152]} +{"t": 1791055570.402, "mem_avail_kib": 27100288, "swap_free_kib": 7054376, "vram_used": [16562458624,15730896896]} +{"t": 1791055571.441, "mem_avail_kib": 27094024, "swap_free_kib": 7054376, "vram_used": [16562458624,15730896896]} +{"t": 1791055572.477, "mem_avail_kib": 27101680, "swap_free_kib": 7054376, "vram_used": [16562458624,15730896896]} +{"t": 1791055573.515, "mem_avail_kib": 27100552, "swap_free_kib": 7054376, "vram_used": [16562458624,15730896896]} +{"t": 1791055574.552, "mem_avail_kib": 27178496, "swap_free_kib": 7054376, "vram_used": [16562458624,15730896896]} +{"t": 1791055575.589, "mem_avail_kib": 27181928, "swap_free_kib": 7054376, "vram_used": [16562458624,15730896896]} +{"t": 1791055576.626, "mem_avail_kib": 27191396, "swap_free_kib": 7054376, "vram_used": [16562458624,15730896896]} +{"t": 1791055577.664, "mem_avail_kib": 27189424, "swap_free_kib": 7054376, "vram_used": [16562458624,15730896896]} +{"t": 1791055578.701, "mem_avail_kib": 27200084, "swap_free_kib": 7054376, "vram_used": [16562458624,15730896896]} +{"t": 1791055579.742, "mem_avail_kib": 27211748, "swap_free_kib": 7054376, "vram_used": [16562458624,15730896896]} +{"t": 1791055580.783, "mem_avail_kib": 27221836, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055581.821, "mem_avail_kib": 27231360, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055582.861, "mem_avail_kib": 27222248, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055583.900, "mem_avail_kib": 27228260, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055584.938, "mem_avail_kib": 27231048, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055585.978, "mem_avail_kib": 27236244, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055587.018, "mem_avail_kib": 27121292, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055588.053, "mem_avail_kib": 27122072, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055589.090, "mem_avail_kib": 27122960, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055590.127, "mem_avail_kib": 27114652, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055591.163, "mem_avail_kib": 27117328, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055592.207, "mem_avail_kib": 27107232, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055593.244, "mem_avail_kib": 27103404, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055594.284, "mem_avail_kib": 27146720, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055595.321, "mem_avail_kib": 27160428, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055596.356, "mem_avail_kib": 27166948, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055597.395, "mem_avail_kib": 27164564, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055598.431, "mem_avail_kib": 27178824, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055599.470, "mem_avail_kib": 27196184, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055600.507, "mem_avail_kib": 27206660, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055601.545, "mem_avail_kib": 27208100, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055602.587, "mem_avail_kib": 27207380, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055603.625, "mem_avail_kib": 27198348, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055604.663, "mem_avail_kib": 27199968, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055605.699, "mem_avail_kib": 27208528, "swap_free_kib": 7054376, "vram_used": [16562462720,15730900992]} +{"t": 1791055606.736, "mem_avail_kib": 27212224, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055607.776, "mem_avail_kib": 27203128, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055608.815, "mem_avail_kib": 27196956, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055609.854, "mem_avail_kib": 27201396, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055610.889, "mem_avail_kib": 27206104, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055611.929, "mem_avail_kib": 27208428, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055612.968, "mem_avail_kib": 27213028, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055614.005, "mem_avail_kib": 27225020, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055615.041, "mem_avail_kib": 27210732, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055616.080, "mem_avail_kib": 27209548, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055617.121, "mem_avail_kib": 27217180, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055618.160, "mem_avail_kib": 27220700, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055619.196, "mem_avail_kib": 27230556, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055620.233, "mem_avail_kib": 27221736, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055621.270, "mem_avail_kib": 27218132, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055622.308, "mem_avail_kib": 27148140, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055623.346, "mem_avail_kib": 27132364, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055624.384, "mem_avail_kib": 27128956, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055625.423, "mem_avail_kib": 27135464, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055626.460, "mem_avail_kib": 27139852, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055627.498, "mem_avail_kib": 27092632, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055628.536, "mem_avail_kib": 27091820, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055629.575, "mem_avail_kib": 27099320, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055630.615, "mem_avail_kib": 27098028, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055631.654, "mem_avail_kib": 27095252, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055632.692, "mem_avail_kib": 27097848, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055633.732, "mem_avail_kib": 27096964, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055634.771, "mem_avail_kib": 27100540, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055635.809, "mem_avail_kib": 27098044, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055636.848, "mem_avail_kib": 27083572, "swap_free_kib": 7054376, "vram_used": [16562462720,15731044352]} +{"t": 1791055637.889, "mem_avail_kib": 27081652, "swap_free_kib": 7054632, "vram_used": [16562462720,15731044352]} +{"t": 1791055638.930, "mem_avail_kib": 27092164, "swap_free_kib": 7054632, "vram_used": [16562462720,15731044352]} +{"t": 1791055639.967, "mem_avail_kib": 27076424, "swap_free_kib": 7054632, "vram_used": [16562462720,15731044352]} +{"t": 1791055641.007, "mem_avail_kib": 27086376, "swap_free_kib": 7054632, "vram_used": [16562462720,15731044352]} +{"t": 1791055642.043, "mem_avail_kib": 27094036, "swap_free_kib": 7054632, "vram_used": [16562462720,15731044352]} +{"t": 1791055643.082, "mem_avail_kib": 27091056, "swap_free_kib": 7054632, "vram_used": [16562462720,15731044352]} +{"t": 1791055644.119, "mem_avail_kib": 27083248, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055645.157, "mem_avail_kib": 27083096, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055646.194, "mem_avail_kib": 27084144, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055647.236, "mem_avail_kib": 27084076, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055648.273, "mem_avail_kib": 27080940, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055649.311, "mem_avail_kib": 27073976, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055650.349, "mem_avail_kib": 27078040, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055651.388, "mem_avail_kib": 27079168, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055652.427, "mem_avail_kib": 27084012, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055653.464, "mem_avail_kib": 27087476, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055654.502, "mem_avail_kib": 27079020, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055655.540, "mem_avail_kib": 27044148, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055656.579, "mem_avail_kib": 26957072, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055657.616, "mem_avail_kib": 26941260, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055658.653, "mem_avail_kib": 26944536, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055659.689, "mem_avail_kib": 26948040, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055660.729, "mem_avail_kib": 26946764, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055661.768, "mem_avail_kib": 26956812, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055662.805, "mem_avail_kib": 26958276, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055663.841, "mem_avail_kib": 26966880, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055664.877, "mem_avail_kib": 27187456, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055665.916, "mem_avail_kib": 27192464, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055666.954, "mem_avail_kib": 27202652, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055667.995, "mem_avail_kib": 27209164, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055669.032, "mem_avail_kib": 27199048, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055670.071, "mem_avail_kib": 27201904, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055671.110, "mem_avail_kib": 27211872, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055672.148, "mem_avail_kib": 27197860, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055673.188, "mem_avail_kib": 27200252, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055674.230, "mem_avail_kib": 27203396, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055675.272, "mem_avail_kib": 27205876, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055676.313, "mem_avail_kib": 27215284, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055677.349, "mem_avail_kib": 27215468, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055678.387, "mem_avail_kib": 27215768, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055679.426, "mem_avail_kib": 27224480, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055680.465, "mem_avail_kib": 27220480, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055681.504, "mem_avail_kib": 27224980, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055682.541, "mem_avail_kib": 27213148, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055683.579, "mem_avail_kib": 27215612, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055684.616, "mem_avail_kib": 27210608, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055685.655, "mem_avail_kib": 27211608, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055686.695, "mem_avail_kib": 27230392, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055687.737, "mem_avail_kib": 27232452, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055688.776, "mem_avail_kib": 27222972, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055689.819, "mem_avail_kib": 27226164, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055690.858, "mem_avail_kib": 27226732, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055691.894, "mem_avail_kib": 27215112, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055692.932, "mem_avail_kib": 27136108, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055693.970, "mem_avail_kib": 27123876, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055695.008, "mem_avail_kib": 27131908, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055696.043, "mem_avail_kib": 27134480, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055697.081, "mem_avail_kib": 27123560, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055698.122, "mem_avail_kib": 27088224, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055699.161, "mem_avail_kib": 27082256, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055700.200, "mem_avail_kib": 27083448, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055701.238, "mem_avail_kib": 27084708, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055702.275, "mem_avail_kib": 27083268, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055703.312, "mem_avail_kib": 27084276, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055704.349, "mem_avail_kib": 27070776, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055705.385, "mem_avail_kib": 27072360, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055706.421, "mem_avail_kib": 27064332, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055707.461, "mem_avail_kib": 27074996, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055708.499, "mem_avail_kib": 27069124, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055709.536, "mem_avail_kib": 27064808, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055710.576, "mem_avail_kib": 27065376, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055711.616, "mem_avail_kib": 27071152, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055712.658, "mem_avail_kib": 27073824, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055713.696, "mem_avail_kib": 27078324, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055714.735, "mem_avail_kib": 27077388, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055715.776, "mem_avail_kib": 27081472, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055716.817, "mem_avail_kib": 27059232, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055717.855, "mem_avail_kib": 27056972, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055718.896, "mem_avail_kib": 27070544, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055719.934, "mem_avail_kib": 27063232, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055720.972, "mem_avail_kib": 27063100, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055722.013, "mem_avail_kib": 27068064, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055723.053, "mem_avail_kib": 27061360, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055724.091, "mem_avail_kib": 27072316, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055725.129, "mem_avail_kib": 27071112, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055726.165, "mem_avail_kib": 27079216, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055727.206, "mem_avail_kib": 26964468, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055728.244, "mem_avail_kib": 26960688, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055729.279, "mem_avail_kib": 26968196, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055730.316, "mem_avail_kib": 26968836, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055731.352, "mem_avail_kib": 26972928, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055732.388, "mem_avail_kib": 26975004, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055733.429, "mem_avail_kib": 26973536, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055734.470, "mem_avail_kib": 26967296, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055735.509, "mem_avail_kib": 27167356, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055736.548, "mem_avail_kib": 27173116, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055737.586, "mem_avail_kib": 27170296, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055738.623, "mem_avail_kib": 27166192, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055739.662, "mem_avail_kib": 27171320, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055740.699, "mem_avail_kib": 27176712, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055741.737, "mem_avail_kib": 27179900, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055742.776, "mem_avail_kib": 27171924, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055743.815, "mem_avail_kib": 27175460, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055744.853, "mem_avail_kib": 27182596, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055745.888, "mem_avail_kib": 27187884, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055746.924, "mem_avail_kib": 27188676, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055747.961, "mem_avail_kib": 27193968, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055749.000, "mem_avail_kib": 27188348, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055750.041, "mem_avail_kib": 27192248, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055751.079, "mem_avail_kib": 27208128, "swap_free_kib": 7054632, "vram_used": [16562462720,15780212736]} +{"t": 1791055752.120, "mem_avail_kib": 27211044, "swap_free_kib": 7054888, "vram_used": [16562462720,15780212736]} +{"t": 1791055753.162, "mem_avail_kib": 27213464, "swap_free_kib": 7054888, "vram_used": [16562462720,15780212736]} +{"t": 1791055754.208, "mem_avail_kib": 27199404, "swap_free_kib": 7054888, "vram_used": [16562462720,15780212736]} +{"t": 1791055755.250, "mem_avail_kib": 27203928, "swap_free_kib": 7054888, "vram_used": [16562462720,15780212736]} +{"t": 1791055756.290, "mem_avail_kib": 27205276, "swap_free_kib": 7054888, "vram_used": [16562462720,15780212736]} +{"t": 1791055757.330, "mem_avail_kib": 27215248, "swap_free_kib": 7054888, "vram_used": [16562462720,15780212736]} +{"t": 1791055758.370, "mem_avail_kib": 27212892, "swap_free_kib": 7054888, "vram_used": [16562462720,15780212736]} +{"t": 1791055759.406, "mem_avail_kib": 27202072, "swap_free_kib": 7054888, "vram_used": [16562462720,15780212736]} +{"t": 1791055760.448, "mem_avail_kib": 27183044, "swap_free_kib": 7054888, "vram_used": [16562462720,15780212736]} +{"t": 1791055761.486, "mem_avail_kib": 27189404, "swap_free_kib": 7054888, "vram_used": [16562462720,15780212736]} +{"t": 1791055762.524, "mem_avail_kib": 27118668, "swap_free_kib": 7054888, "vram_used": [16562462720,15780212736]} +{"t": 1791055763.564, "mem_avail_kib": 27119428, "swap_free_kib": 7055144, "vram_used": [16562462720,15780212736]} +{"t": 1791055764.602, "mem_avail_kib": 27111956, "swap_free_kib": 7055144, "vram_used": [16562462720,15780212736]} +{"t": 1791055765.640, "mem_avail_kib": 27118060, "swap_free_kib": 7055400, "vram_used": [16562462720,15780212736]} +{"t": 1791055766.680, "mem_avail_kib": 27101512, "swap_free_kib": 7055400, "vram_used": [16562462720,15780212736]} +{"t": 1791055767.717, "mem_avail_kib": 27059700, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055768.756, "mem_avail_kib": 27064220, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055769.795, "mem_avail_kib": 27077464, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055770.834, "mem_avail_kib": 27079996, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055771.871, "mem_avail_kib": 27085352, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055772.911, "mem_avail_kib": 27073280, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055773.951, "mem_avail_kib": 27069192, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055774.989, "mem_avail_kib": 27074492, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055776.026, "mem_avail_kib": 27080564, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055777.069, "mem_avail_kib": 27069436, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055778.108, "mem_avail_kib": 27042728, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055779.148, "mem_avail_kib": 27050112, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055780.187, "mem_avail_kib": 27055592, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055781.225, "mem_avail_kib": 27052344, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055782.264, "mem_avail_kib": 27057472, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055783.301, "mem_avail_kib": 27064884, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055784.338, "mem_avail_kib": 27060428, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055785.378, "mem_avail_kib": 27071728, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055786.415, "mem_avail_kib": 27068660, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055787.454, "mem_avail_kib": 27080756, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055788.492, "mem_avail_kib": 27085204, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055789.530, "mem_avail_kib": 27073348, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055790.571, "mem_avail_kib": 27073200, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055791.610, "mem_avail_kib": 27084916, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055792.648, "mem_avail_kib": 27089456, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055793.687, "mem_avail_kib": 27077496, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055794.722, "mem_avail_kib": 27080172, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055795.759, "mem_avail_kib": 27085100, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055796.800, "mem_avail_kib": 26962752, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055797.837, "mem_avail_kib": 26968120, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055798.878, "mem_avail_kib": 26960616, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055799.914, "mem_avail_kib": 26948040, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055800.951, "mem_avail_kib": 26952144, "swap_free_kib": 7055656, "vram_used": [16562462720,15780212736]} +{"t": 1791055801.989, "mem_avail_kib": 26953992, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055803.028, "mem_avail_kib": 26960532, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055804.067, "mem_avail_kib": 26965552, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055805.106, "mem_avail_kib": 26974128, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055806.144, "mem_avail_kib": 26972560, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055807.183, "mem_avail_kib": 26975264, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055808.221, "mem_avail_kib": 26982524, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055809.259, "mem_avail_kib": 27194788, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055810.295, "mem_avail_kib": 27192136, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055811.334, "mem_avail_kib": 27194644, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055812.370, "mem_avail_kib": 27187420, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055813.411, "mem_avail_kib": 27187204, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055814.448, "mem_avail_kib": 27188332, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055815.483, "mem_avail_kib": 27177640, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055816.520, "mem_avail_kib": 27174588, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055817.559, "mem_avail_kib": 27176848, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055818.601, "mem_avail_kib": 27182692, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055819.642, "mem_avail_kib": 27187836, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055820.684, "mem_avail_kib": 27195216, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055821.724, "mem_avail_kib": 27199644, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055822.763, "mem_avail_kib": 27191188, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055823.802, "mem_avail_kib": 27196408, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055824.838, "mem_avail_kib": 27197040, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055825.874, "mem_avail_kib": 27201972, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055826.913, "mem_avail_kib": 27185608, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055827.953, "mem_avail_kib": 27191132, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055828.996, "mem_avail_kib": 27184156, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055830.037, "mem_avail_kib": 27195604, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055831.076, "mem_avail_kib": 27198932, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055832.113, "mem_avail_kib": 27203004, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055833.153, "mem_avail_kib": 27195552, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055834.195, "mem_avail_kib": 27181956, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055835.233, "mem_avail_kib": 27174912, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055836.268, "mem_avail_kib": 27177720, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055837.305, "mem_avail_kib": 27096924, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055838.343, "mem_avail_kib": 27066736, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055839.384, "mem_avail_kib": 27068032, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055840.422, "mem_avail_kib": 27064496, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055841.460, "mem_avail_kib": 27077312, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055842.497, "mem_avail_kib": 27020740, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055843.534, "mem_avail_kib": 27012660, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055844.573, "mem_avail_kib": 27017972, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055845.612, "mem_avail_kib": 27028316, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055846.649, "mem_avail_kib": 27037696, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055847.687, "mem_avail_kib": 27048544, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055848.726, "mem_avail_kib": 27051524, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055849.763, "mem_avail_kib": 27057704, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055850.799, "mem_avail_kib": 27064040, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055851.838, "mem_avail_kib": 27062364, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055852.875, "mem_avail_kib": 27069208, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055853.914, "mem_avail_kib": 27059192, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055854.954, "mem_avail_kib": 27057308, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055855.992, "mem_avail_kib": 27062296, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055857.031, "mem_avail_kib": 27071592, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055858.067, "mem_avail_kib": 27075048, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055859.109, "mem_avail_kib": 27076368, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055860.147, "mem_avail_kib": 27079348, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055861.185, "mem_avail_kib": 27076916, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055862.221, "mem_avail_kib": 27080080, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055863.258, "mem_avail_kib": 27087536, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055864.296, "mem_avail_kib": 27086868, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055865.334, "mem_avail_kib": 27078000, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055866.370, "mem_avail_kib": 27022304, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055867.407, "mem_avail_kib": 27016408, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055868.444, "mem_avail_kib": 27022060, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055869.483, "mem_avail_kib": 27025664, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055870.521, "mem_avail_kib": 27017064, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055871.559, "mem_avail_kib": 26952836, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055872.594, "mem_avail_kib": 26957724, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055873.631, "mem_avail_kib": 26959336, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055874.667, "mem_avail_kib": 26956340, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055875.706, "mem_avail_kib": 26956024, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055876.744, "mem_avail_kib": 26957508, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055877.782, "mem_avail_kib": 26961868, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055878.820, "mem_avail_kib": 26956108, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055879.859, "mem_avail_kib": 26964524, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055880.896, "mem_avail_kib": 26957848, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055881.938, "mem_avail_kib": 26961056, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055882.977, "mem_avail_kib": 26965252, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055884.014, "mem_avail_kib": 26967340, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055885.053, "mem_avail_kib": 26963848, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055886.089, "mem_avail_kib": 26960116, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055887.125, "mem_avail_kib": 26952072, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055888.163, "mem_avail_kib": 26946456, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055889.200, "mem_avail_kib": 26949076, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055890.237, "mem_avail_kib": 26950316, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055891.274, "mem_avail_kib": 26944588, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055892.311, "mem_avail_kib": 26946096, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055893.350, "mem_avail_kib": 26950504, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055894.388, "mem_avail_kib": 26959364, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055895.426, "mem_avail_kib": 26897296, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055896.462, "mem_avail_kib": 26886288, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055897.504, "mem_avail_kib": 26887292, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055898.546, "mem_avail_kib": 26893456, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055899.585, "mem_avail_kib": 26867416, "swap_free_kib": 7055912, "vram_used": [16562462720,15780212736]} +{"t": 1791055900.622, "mem_avail_kib": 26882632, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055901.658, "mem_avail_kib": 26835604, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055902.695, "mem_avail_kib": 26835292, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055903.733, "mem_avail_kib": 26841128, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055904.771, "mem_avail_kib": 26847472, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055905.805, "mem_avail_kib": 26834784, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055906.842, "mem_avail_kib": 26833228, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055907.882, "mem_avail_kib": 26829424, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055908.919, "mem_avail_kib": 26833128, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055909.957, "mem_avail_kib": 26848500, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055910.996, "mem_avail_kib": 26849664, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055912.033, "mem_avail_kib": 26842484, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055913.072, "mem_avail_kib": 26840556, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055914.112, "mem_avail_kib": 26842864, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055915.149, "mem_avail_kib": 26833392, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055916.189, "mem_avail_kib": 26837860, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055917.227, "mem_avail_kib": 26837744, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055918.265, "mem_avail_kib": 26842960, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055919.307, "mem_avail_kib": 26843360, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055920.346, "mem_avail_kib": 26847904, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055921.386, "mem_avail_kib": 26845176, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055922.426, "mem_avail_kib": 26838244, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055923.465, "mem_avail_kib": 26833300, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055924.502, "mem_avail_kib": 26830768, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055925.539, "mem_avail_kib": 26755348, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055926.575, "mem_avail_kib": 26758488, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055927.615, "mem_avail_kib": 26766984, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055928.652, "mem_avail_kib": 26770060, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055929.691, "mem_avail_kib": 26772532, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055930.727, "mem_avail_kib": 26782348, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055931.765, "mem_avail_kib": 26733304, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055932.803, "mem_avail_kib": 26731644, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055933.839, "mem_avail_kib": 26733236, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055934.877, "mem_avail_kib": 26737008, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055935.916, "mem_avail_kib": 26729460, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055936.954, "mem_avail_kib": 26729484, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055937.992, "mem_avail_kib": 26733140, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055939.030, "mem_avail_kib": 26733180, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055940.070, "mem_avail_kib": 26735396, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055941.106, "mem_avail_kib": 26735716, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055942.143, "mem_avail_kib": 26728044, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055943.184, "mem_avail_kib": 26728068, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055944.225, "mem_avail_kib": 26736532, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055945.263, "mem_avail_kib": 26722368, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055946.299, "mem_avail_kib": 26717752, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055947.336, "mem_avail_kib": 26712156, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055948.377, "mem_avail_kib": 26707700, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055949.416, "mem_avail_kib": 26700744, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055950.456, "mem_avail_kib": 26704772, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055951.498, "mem_avail_kib": 26720240, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055952.540, "mem_avail_kib": 26723660, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055953.580, "mem_avail_kib": 26725448, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055954.616, "mem_avail_kib": 26733992, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055955.652, "mem_avail_kib": 26726104, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055956.688, "mem_avail_kib": 26661972, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055957.727, "mem_avail_kib": 26668216, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055958.765, "mem_avail_kib": 26667440, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055959.803, "mem_avail_kib": 26665844, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055960.844, "mem_avail_kib": 26669992, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055961.884, "mem_avail_kib": 26670312, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055962.923, "mem_avail_kib": 26620728, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055963.961, "mem_avail_kib": 26618116, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055964.996, "mem_avail_kib": 26609020, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055966.034, "mem_avail_kib": 26603424, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055967.071, "mem_avail_kib": 26601940, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055968.108, "mem_avail_kib": 26601968, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055969.149, "mem_avail_kib": 26606744, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055970.187, "mem_avail_kib": 26612860, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055971.225, "mem_avail_kib": 26603300, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055972.263, "mem_avail_kib": 26617248, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055973.302, "mem_avail_kib": 26601532, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055974.339, "mem_avail_kib": 26595316, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055975.378, "mem_avail_kib": 26588948, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055976.417, "mem_avail_kib": 26581960, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055977.454, "mem_avail_kib": 26580832, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055978.492, "mem_avail_kib": 26587904, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055979.529, "mem_avail_kib": 26580052, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055980.573, "mem_avail_kib": 26592072, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055981.612, "mem_avail_kib": 26602408, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055982.651, "mem_avail_kib": 26609672, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055983.689, "mem_avail_kib": 26599384, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055984.726, "mem_avail_kib": 26600456, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055985.764, "mem_avail_kib": 26601536, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055986.800, "mem_avail_kib": 26605764, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055987.838, "mem_avail_kib": 26601476, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055988.876, "mem_avail_kib": 26539840, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055989.917, "mem_avail_kib": 26549632, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055990.960, "mem_avail_kib": 26554508, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055991.996, "mem_avail_kib": 26557172, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055993.035, "mem_avail_kib": 26559132, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055994.071, "mem_avail_kib": 26558932, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055995.111, "mem_avail_kib": 26508892, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055996.151, "mem_avail_kib": 26505396, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055997.186, "mem_avail_kib": 26500328, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055998.226, "mem_avail_kib": 26503400, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791055999.264, "mem_avail_kib": 26508472, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791056000.303, "mem_avail_kib": 26499316, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791056001.339, "mem_avail_kib": 26503112, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791056002.377, "mem_avail_kib": 26500700, "swap_free_kib": 7056168, "vram_used": [16562462720,15780212736]} +{"t": 1791056003.414, "mem_avail_kib": 26494204, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056004.453, "mem_avail_kib": 26475432, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056005.490, "mem_avail_kib": 26478272, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056006.530, "mem_avail_kib": 26469108, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056007.570, "mem_avail_kib": 26480516, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056008.610, "mem_avail_kib": 26488048, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056009.648, "mem_avail_kib": 26497012, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056010.686, "mem_avail_kib": 26503304, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056011.722, "mem_avail_kib": 26505548, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056012.761, "mem_avail_kib": 26499584, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056013.806, "mem_avail_kib": 26499544, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056014.846, "mem_avail_kib": 26499948, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056015.886, "mem_avail_kib": 26496844, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056016.924, "mem_avail_kib": 26503252, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056017.961, "mem_avail_kib": 26501492, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056018.998, "mem_avail_kib": 26506836, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056020.034, "mem_avail_kib": 26502124, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056021.069, "mem_avail_kib": 26502204, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056022.106, "mem_avail_kib": 26418472, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056023.144, "mem_avail_kib": 26422932, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056024.180, "mem_avail_kib": 26416784, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056025.220, "mem_avail_kib": 26422548, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056026.259, "mem_avail_kib": 26425028, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056027.294, "mem_avail_kib": 26417928, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056028.331, "mem_avail_kib": 26374484, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056029.370, "mem_avail_kib": 26475908, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056030.406, "mem_avail_kib": 26477268, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056031.444, "mem_avail_kib": 26470940, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056032.484, "mem_avail_kib": 26483548, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056033.522, "mem_avail_kib": 26494140, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056034.560, "mem_avail_kib": 26492772, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056035.601, "mem_avail_kib": 26484268, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056036.640, "mem_avail_kib": 26483784, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056037.678, "mem_avail_kib": 26474496, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056038.714, "mem_avail_kib": 26474740, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056039.750, "mem_avail_kib": 26468500, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056040.786, "mem_avail_kib": 26474560, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056041.823, "mem_avail_kib": 26473304, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056042.862, "mem_avail_kib": 26481400, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056043.902, "mem_avail_kib": 26489612, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056044.941, "mem_avail_kib": 26497944, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056045.978, "mem_avail_kib": 26502496, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056047.020, "mem_avail_kib": 26492140, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056048.059, "mem_avail_kib": 26491252, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056049.100, "mem_avail_kib": 26496820, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056050.138, "mem_avail_kib": 26502980, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056051.177, "mem_avail_kib": 26502276, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056052.216, "mem_avail_kib": 26503716, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056053.256, "mem_avail_kib": 26508472, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056054.295, "mem_avail_kib": 26507916, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056055.340, "mem_avail_kib": 26505080, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056056.380, "mem_avail_kib": 26370928, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056057.417, "mem_avail_kib": 26375044, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056058.455, "mem_avail_kib": 26375952, "swap_free_kib": 7056424, "vram_used": [16562462720,15780212736]} +{"t": 1791056059.493, "mem_avail_kib": 26388432, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056060.529, "mem_avail_kib": 26399116, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056061.567, "mem_avail_kib": 26401792, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056062.604, "mem_avail_kib": 26410992, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056063.642, "mem_avail_kib": 26428944, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056064.681, "mem_avail_kib": 26443564, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056065.717, "mem_avail_kib": 26440300, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056066.757, "mem_avail_kib": 26445148, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056067.795, "mem_avail_kib": 26447432, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056068.836, "mem_avail_kib": 26459360, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056069.874, "mem_avail_kib": 27160652, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056070.912, "mem_avail_kib": 27153184, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056071.949, "mem_avail_kib": 27156136, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056072.989, "mem_avail_kib": 27163684, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056074.027, "mem_avail_kib": 27160772, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056075.065, "mem_avail_kib": 27168152, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056076.102, "mem_avail_kib": 27152460, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056077.142, "mem_avail_kib": 27161560, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056078.180, "mem_avail_kib": 27156604, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056079.220, "mem_avail_kib": 27155992, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056080.261, "mem_avail_kib": 27148936, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056081.299, "mem_avail_kib": 27154572, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056082.335, "mem_avail_kib": 27135620, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056083.372, "mem_avail_kib": 27147816, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056084.413, "mem_avail_kib": 27152776, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056085.451, "mem_avail_kib": 27159968, "swap_free_kib": 7056680, "vram_used": [16562462720,15780212736]} +{"t": 1791056086.486, "mem_avail_kib": 27163956, "swap_free_kib": 7057192, "vram_used": [16562462720,15780212736]} +{"t": 1791056087.524, "mem_avail_kib": 27164560, "swap_free_kib": 7057192, "vram_used": [16562462720,15780212736]} +{"t": 1791056088.562, "mem_avail_kib": 27160000, "swap_free_kib": 7057192, "vram_used": [16562462720,15780212736]} +{"t": 1791056089.602, "mem_avail_kib": 27156140, "swap_free_kib": 7057192, "vram_used": [16562462720,15780212736]} +{"t": 1791056090.643, "mem_avail_kib": 27162940, "swap_free_kib": 7057192, "vram_used": [16562462720,15780212736]} +{"t": 1791056091.682, "mem_avail_kib": 27167484, "swap_free_kib": 7057192, "vram_used": [16562462720,15780212736]} +{"t": 1791056092.719, "mem_avail_kib": 27172256, "swap_free_kib": 7057192, "vram_used": [16562462720,15780212736]} +{"t": 1791056093.757, "mem_avail_kib": 27179256, "swap_free_kib": 7057192, "vram_used": [16562462720,15780212736]} +{"t": 1791056094.796, "mem_avail_kib": 27172512, "swap_free_kib": 7057192, "vram_used": [16562462720,15780212736]} +{"t": 1791056095.833, "mem_avail_kib": 27161336, "swap_free_kib": 7057192, "vram_used": [16562462720,15780212736]} +{"t": 1791056096.875, "mem_avail_kib": 27093708, "swap_free_kib": 7057192, "vram_used": [16562462720,15780212736]} +{"t": 1791056097.919, "mem_avail_kib": 27099928, "swap_free_kib": 7057192, "vram_used": [16562462720,15780212736]} +{"t": 1791056098.958, "mem_avail_kib": 27105480, "swap_free_kib": 7057192, "vram_used": [16562462720,15780212736]} +{"t": 1791056099.997, "mem_avail_kib": 27107512, "swap_free_kib": 7057704, "vram_used": [16562462720,15780212736]} +{"t": 1791056101.034, "mem_avail_kib": 27105132, "swap_free_kib": 7057704, "vram_used": [16562462720,15780212736]} +{"t": 1791056102.073, "mem_avail_kib": 27057848, "swap_free_kib": 7057704, "vram_used": [16562462720,15780212736]} +{"t": 1791056103.110, "mem_avail_kib": 27054940, "swap_free_kib": 7057704, "vram_used": [16562462720,15780212736]} +{"t": 1791056104.149, "mem_avail_kib": 27061248, "swap_free_kib": 7057704, "vram_used": [16562462720,15780212736]} +{"t": 1791056105.186, "mem_avail_kib": 27052944, "swap_free_kib": 7057704, "vram_used": [16562462720,15780212736]} +{"t": 1791056106.224, "mem_avail_kib": 27044384, "swap_free_kib": 7057704, "vram_used": [16562462720,15780212736]} +{"t": 1791056107.264, "mem_avail_kib": 27051396, "swap_free_kib": 7057704, "vram_used": [16562462720,15780212736]} +{"t": 1791056108.301, "mem_avail_kib": 27057152, "swap_free_kib": 7057704, "vram_used": [16562462720,15780212736]} +{"t": 1791056109.342, "mem_avail_kib": 27047636, "swap_free_kib": 7057704, "vram_used": [16562462720,15780212736]} +{"t": 1791056110.380, "mem_avail_kib": 27044136, "swap_free_kib": 7057704, "vram_used": [16562462720,15780212736]} +{"t": 1791056111.416, "mem_avail_kib": 27051244, "swap_free_kib": 7057704, "vram_used": [16562462720,15780212736]} +{"t": 1791056112.452, "mem_avail_kib": 27037292, "swap_free_kib": 7057704, "vram_used": [16562462720,15780212736]} +{"t": 1791056113.488, "mem_avail_kib": 27041392, "swap_free_kib": 7057704, "vram_used": [16562462720,15780212736]} +{"t": 1791056114.526, "mem_avail_kib": 27052100, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056115.562, "mem_avail_kib": 27060292, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056116.603, "mem_avail_kib": 27052104, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056117.642, "mem_avail_kib": 27053824, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056118.682, "mem_avail_kib": 27056972, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056119.722, "mem_avail_kib": 27060620, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056120.763, "mem_avail_kib": 27058968, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056121.800, "mem_avail_kib": 27053384, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056122.837, "mem_avail_kib": 27049972, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056123.874, "mem_avail_kib": 27048216, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056124.911, "mem_avail_kib": 27045528, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056125.945, "mem_avail_kib": 26974804, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056126.982, "mem_avail_kib": 26973428, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056128.020, "mem_avail_kib": 26981284, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056129.063, "mem_avail_kib": 26984344, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056130.099, "mem_avail_kib": 26974396, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056131.139, "mem_avail_kib": 26928388, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056132.176, "mem_avail_kib": 26921292, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056133.214, "mem_avail_kib": 26915944, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056134.251, "mem_avail_kib": 26923340, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056135.288, "mem_avail_kib": 26926644, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056136.325, "mem_avail_kib": 26922148, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056137.366, "mem_avail_kib": 26926284, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056138.404, "mem_avail_kib": 26935284, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056139.442, "mem_avail_kib": 26934052, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056140.480, "mem_avail_kib": 26932784, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056141.519, "mem_avail_kib": 26937600, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056142.558, "mem_avail_kib": 26947676, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056143.594, "mem_avail_kib": 26917988, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056144.632, "mem_avail_kib": 26926396, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056145.670, "mem_avail_kib": 26924076, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056146.708, "mem_avail_kib": 26932924, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056147.746, "mem_avail_kib": 26936096, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056148.782, "mem_avail_kib": 26927864, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056149.819, "mem_avail_kib": 26934088, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056150.859, "mem_avail_kib": 26938940, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056151.897, "mem_avail_kib": 26936200, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056152.936, "mem_avail_kib": 26930348, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056153.975, "mem_avail_kib": 26935920, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056155.015, "mem_avail_kib": 26856312, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056156.052, "mem_avail_kib": 26859028, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056157.088, "mem_avail_kib": 26862032, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056158.125, "mem_avail_kib": 26874388, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056159.164, "mem_avail_kib": 26875240, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056160.202, "mem_avail_kib": 26874388, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056161.240, "mem_avail_kib": 26827160, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056162.280, "mem_avail_kib": 26830064, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056163.322, "mem_avail_kib": 26829156, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056164.360, "mem_avail_kib": 26827184, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056165.397, "mem_avail_kib": 26827320, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056166.433, "mem_avail_kib": 26828344, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056167.470, "mem_avail_kib": 26830900, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056168.506, "mem_avail_kib": 26835156, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056169.541, "mem_avail_kib": 26822744, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056170.579, "mem_avail_kib": 26825920, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056171.615, "mem_avail_kib": 26832956, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056172.657, "mem_avail_kib": 26830464, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056173.697, "mem_avail_kib": 26830576, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056174.734, "mem_avail_kib": 26834536, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056175.773, "mem_avail_kib": 26834636, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056176.811, "mem_avail_kib": 26838456, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056177.847, "mem_avail_kib": 26845532, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056178.885, "mem_avail_kib": 26849912, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056179.924, "mem_avail_kib": 26842688, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056180.965, "mem_avail_kib": 26845188, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056182.004, "mem_avail_kib": 26848964, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056183.045, "mem_avail_kib": 26858260, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056184.085, "mem_avail_kib": 26851808, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056185.122, "mem_avail_kib": 26794952, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056186.159, "mem_avail_kib": 26770972, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056187.199, "mem_avail_kib": 26772204, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056188.239, "mem_avail_kib": 26770428, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056189.274, "mem_avail_kib": 26777372, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056190.313, "mem_avail_kib": 26772236, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056191.350, "mem_avail_kib": 26722336, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056192.390, "mem_avail_kib": 26723520, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056193.428, "mem_avail_kib": 26728316, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056194.466, "mem_avail_kib": 26728640, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056195.509, "mem_avail_kib": 26717428, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056196.547, "mem_avail_kib": 26720120, "swap_free_kib": 7057960, "vram_used": [16562462720,15780212736]} +{"t": 1791056197.586, "mem_avail_kib": 26727212, "swap_free_kib": 7058216, "vram_used": [16562462720,15780212736]} +{"t": 1791056198.624, "mem_avail_kib": 26719348, "swap_free_kib": 7058216, "vram_used": [16562462720,15780212736]} +{"t": 1791056199.662, "mem_avail_kib": 26726360, "swap_free_kib": 7058216, "vram_used": [16562462720,15780212736]} +{"t": 1791056200.702, "mem_avail_kib": 26727572, "swap_free_kib": 7058216, "vram_used": [16562462720,15780212736]} +{"t": 1791056201.740, "mem_avail_kib": 26726808, "swap_free_kib": 7058216, "vram_used": [16562462720,15780212736]} +{"t": 1791056202.777, "mem_avail_kib": 26710352, "swap_free_kib": 7058216, "vram_used": [16562462720,15780212736]} +{"t": 1791056203.817, "mem_avail_kib": 26715956, "swap_free_kib": 7058216, "vram_used": [16562462720,15780212736]} +{"t": 1791056204.856, "mem_avail_kib": 26686244, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056205.892, "mem_avail_kib": 26692748, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056206.928, "mem_avail_kib": 26691344, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056207.967, "mem_avail_kib": 26706640, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056209.007, "mem_avail_kib": 26711480, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056210.046, "mem_avail_kib": 26714560, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056211.089, "mem_avail_kib": 26715768, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056212.127, "mem_avail_kib": 26714748, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056213.166, "mem_avail_kib": 26716824, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056214.205, "mem_avail_kib": 26722028, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056215.242, "mem_avail_kib": 26719692, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056216.281, "mem_avail_kib": 26661052, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056217.316, "mem_avail_kib": 26629380, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056218.353, "mem_avail_kib": 26627628, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056219.389, "mem_avail_kib": 26629080, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056220.430, "mem_avail_kib": 26634212, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056221.468, "mem_avail_kib": 26627980, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056222.508, "mem_avail_kib": 26578936, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056223.546, "mem_avail_kib": 26591204, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056224.588, "mem_avail_kib": 26593616, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056225.627, "mem_avail_kib": 26594836, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056226.665, "mem_avail_kib": 26589616, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056227.698, "mem_avail_kib": 26599280, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056228.736, "mem_avail_kib": 26600436, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056229.774, "mem_avail_kib": 26603676, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056230.813, "mem_avail_kib": 26605368, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056231.851, "mem_avail_kib": 26606572, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056232.888, "mem_avail_kib": 26601284, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056233.926, "mem_avail_kib": 26606720, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056234.967, "mem_avail_kib": 26618212, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056236.005, "mem_avail_kib": 26608300, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056237.044, "mem_avail_kib": 26604076, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056238.081, "mem_avail_kib": 26603260, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056239.119, "mem_avail_kib": 26598644, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056240.158, "mem_avail_kib": 26603504, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056241.197, "mem_avail_kib": 26608884, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056242.237, "mem_avail_kib": 26600264, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056243.276, "mem_avail_kib": 26590872, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056244.317, "mem_avail_kib": 26595832, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056245.357, "mem_avail_kib": 26603048, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056246.395, "mem_avail_kib": 26608292, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056247.432, "mem_avail_kib": 26600056, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056248.473, "mem_avail_kib": 26522500, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056249.511, "mem_avail_kib": 26514572, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056250.549, "mem_avail_kib": 26515632, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056251.585, "mem_avail_kib": 26524244, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056252.624, "mem_avail_kib": 26524988, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056253.666, "mem_avail_kib": 26524348, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056254.703, "mem_avail_kib": 26516788, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056255.740, "mem_avail_kib": 26469156, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056256.780, "mem_avail_kib": 26469304, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056257.816, "mem_avail_kib": 26461976, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056258.853, "mem_avail_kib": 26462492, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056259.890, "mem_avail_kib": 26466816, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056260.927, "mem_avail_kib": 26451764, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056261.966, "mem_avail_kib": 26455332, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056263.004, "mem_avail_kib": 26462756, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056264.040, "mem_avail_kib": 26465096, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056265.077, "mem_avail_kib": 26459236, "swap_free_kib": 7058472, "vram_used": [16562462720,15780212736]} +{"t": 1791056266.117, "mem_avail_kib": 26445820, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056267.154, "mem_avail_kib": 26454668, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056268.190, "mem_avail_kib": 26466980, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056269.229, "mem_avail_kib": 26464116, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056270.267, "mem_avail_kib": 26466836, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056271.306, "mem_avail_kib": 26468528, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056272.347, "mem_avail_kib": 26471008, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056273.385, "mem_avail_kib": 26463288, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056274.424, "mem_avail_kib": 26465856, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056275.463, "mem_avail_kib": 26485364, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056276.500, "mem_avail_kib": 26483660, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056277.540, "mem_avail_kib": 26489328, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056278.580, "mem_avail_kib": 26493088, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056279.622, "mem_avail_kib": 26484592, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056280.659, "mem_avail_kib": 26474480, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056281.695, "mem_avail_kib": 26409780, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056282.733, "mem_avail_kib": 26404464, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056283.771, "mem_avail_kib": 26405244, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056284.810, "mem_avail_kib": 26409472, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056285.849, "mem_avail_kib": 26410444, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056286.886, "mem_avail_kib": 26409880, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056287.922, "mem_avail_kib": 26394588, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056288.958, "mem_avail_kib": 26407828, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056289.996, "mem_avail_kib": 26419372, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056291.034, "mem_avail_kib": 26421544, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056292.074, "mem_avail_kib": 26441988, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056293.110, "mem_avail_kib": 26451816, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056294.149, "mem_avail_kib": 26449856, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056295.192, "mem_avail_kib": 26460632, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056296.230, "mem_avail_kib": 26468496, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056297.268, "mem_avail_kib": 26476476, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056298.306, "mem_avail_kib": 26483440, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056299.344, "mem_avail_kib": 26481528, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056300.383, "mem_avail_kib": 26472552, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056301.422, "mem_avail_kib": 26475336, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056302.459, "mem_avail_kib": 26471252, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056303.495, "mem_avail_kib": 26474984, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056304.532, "mem_avail_kib": 26483512, "swap_free_kib": 7058728, "vram_used": [16562462720,15780212736]} +{"t": 1791056305.569, "mem_avail_kib": 26487368, "swap_free_kib": 7058984, "vram_used": [16562462720,15780212736]} +{"t": 1791056306.611, "mem_avail_kib": 26480544, "swap_free_kib": 7058984, "vram_used": [16562462720,15780212736]} +{"t": 1791056307.649, "mem_avail_kib": 26491436, "swap_free_kib": 7058984, "vram_used": [16562462720,15780212736]} +{"t": 1791056308.686, "mem_avail_kib": 26500192, "swap_free_kib": 7058984, "vram_used": [16562462720,15780212736]} +{"t": 1791056309.728, "mem_avail_kib": 26503584, "swap_free_kib": 7059240, "vram_used": [16562462720,15780212736]} +{"t": 1791056310.768, "mem_avail_kib": 26502732, "swap_free_kib": 7059240, "vram_used": [16562462720,15780212736]} +{"t": 1791056311.806, "mem_avail_kib": 26501228, "swap_free_kib": 7059240, "vram_used": [16562462720,15780212736]} +{"t": 1791056312.844, "mem_avail_kib": 26496984, "swap_free_kib": 7059240, "vram_used": [16562462720,15780212736]} +{"t": 1791056313.880, "mem_avail_kib": 26487656, "swap_free_kib": 7059240, "vram_used": [16562462720,15780212736]} +{"t": 1791056314.921, "mem_avail_kib": 26494244, "swap_free_kib": 7059240, "vram_used": [16562462720,15780212736]} +{"t": 1791056315.959, "mem_avail_kib": 26431008, "swap_free_kib": 7059240, "vram_used": [16562462720,15780212736]} +{"t": 1791056316.997, "mem_avail_kib": 26367740, "swap_free_kib": 7059240, "vram_used": [16562462720,15780212736]} +{"t": 1791056318.038, "mem_avail_kib": 26378032, "swap_free_kib": 7059240, "vram_used": [16562462720,15780212736]} +{"t": 1791056319.077, "mem_avail_kib": 26389748, "swap_free_kib": 7059240, "vram_used": [16562462720,15780212736]} +{"t": 1791056320.111, "mem_avail_kib": 26396336, "swap_free_kib": 7059240, "vram_used": [16562462720,15780212736]} +{"t": 1791056321.148, "mem_avail_kib": 26409160, "swap_free_kib": 7059240, "vram_used": [16562462720,15780212736]} +{"t": 1791056322.187, "mem_avail_kib": 26423448, "swap_free_kib": 7059240, "vram_used": [16562462720,15780212736]} +{"t": 1791056323.226, "mem_avail_kib": 26427008, "swap_free_kib": 7059752, "vram_used": [16562462720,15780212736]} +{"t": 1791056324.262, "mem_avail_kib": 26436748, "swap_free_kib": 7059752, "vram_used": [16562462720,15780212736]} +{"t": 1791056325.298, "mem_avail_kib": 26436444, "swap_free_kib": 7059752, "vram_used": [16562462720,15780212736]} +{"t": 1791056326.338, "mem_avail_kib": 26419732, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056327.376, "mem_avail_kib": 26418196, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056328.414, "mem_avail_kib": 27107208, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056329.453, "mem_avail_kib": 27114624, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056330.493, "mem_avail_kib": 27109288, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056331.531, "mem_avail_kib": 27115992, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056332.567, "mem_avail_kib": 27128344, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056333.603, "mem_avail_kib": 27130812, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056334.639, "mem_avail_kib": 27139632, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056335.677, "mem_avail_kib": 27142536, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056336.718, "mem_avail_kib": 27144676, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056337.755, "mem_avail_kib": 27157084, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056338.794, "mem_avail_kib": 27163160, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056339.831, "mem_avail_kib": 27161192, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056340.869, "mem_avail_kib": 27167648, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056341.907, "mem_avail_kib": 27164496, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056342.942, "mem_avail_kib": 27166156, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056343.979, "mem_avail_kib": 27159380, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056345.020, "mem_avail_kib": 27171320, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056346.059, "mem_avail_kib": 27172392, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056347.095, "mem_avail_kib": 27168528, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056348.134, "mem_avail_kib": 27172960, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056349.175, "mem_avail_kib": 27167676, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056350.213, "mem_avail_kib": 27161588, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056351.250, "mem_avail_kib": 27165364, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056352.289, "mem_avail_kib": 27156116, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056353.327, "mem_avail_kib": 27157784, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056354.365, "mem_avail_kib": 27158216, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056355.405, "mem_avail_kib": 27157900, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056356.443, "mem_avail_kib": 27085888, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056357.482, "mem_avail_kib": 27093184, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056358.519, "mem_avail_kib": 27097112, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056359.557, "mem_avail_kib": 27092320, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056360.598, "mem_avail_kib": 27083200, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056361.636, "mem_avail_kib": 27031248, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056362.674, "mem_avail_kib": 27039524, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056363.716, "mem_avail_kib": 27049964, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056364.752, "mem_avail_kib": 27052340, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056365.789, "mem_avail_kib": 27054632, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056366.826, "mem_avail_kib": 27055652, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056367.864, "mem_avail_kib": 27043812, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056368.903, "mem_avail_kib": 27043312, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056369.944, "mem_avail_kib": 27037444, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056370.980, "mem_avail_kib": 27041392, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056372.021, "mem_avail_kib": 27039728, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056373.060, "mem_avail_kib": 27049312, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056374.097, "mem_avail_kib": 27052924, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056375.134, "mem_avail_kib": 27053828, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056376.169, "mem_avail_kib": 27056776, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056377.205, "mem_avail_kib": 27046544, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056378.241, "mem_avail_kib": 27056468, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056379.279, "mem_avail_kib": 27058276, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056380.317, "mem_avail_kib": 27064068, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056381.354, "mem_avail_kib": 27059344, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056382.390, "mem_avail_kib": 27072476, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056383.425, "mem_avail_kib": 27065612, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056384.463, "mem_avail_kib": 26998608, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056385.500, "mem_avail_kib": 26991096, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056386.536, "mem_avail_kib": 26991280, "swap_free_kib": 7060008, "vram_used": [16562462720,15780212736]} +{"t": 1791056387.573, "mem_avail_kib": 26952792, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056388.610, "mem_avail_kib": 26957176, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056389.647, "mem_avail_kib": 26966136, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056390.684, "mem_avail_kib": 26924604, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056391.724, "mem_avail_kib": 26929364, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056392.764, "mem_avail_kib": 26931860, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056393.805, "mem_avail_kib": 26933268, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056394.841, "mem_avail_kib": 26925500, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056395.881, "mem_avail_kib": 26938012, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056396.918, "mem_avail_kib": 26929728, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056397.955, "mem_avail_kib": 26932664, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056398.994, "mem_avail_kib": 26918164, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056400.031, "mem_avail_kib": 26929620, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056401.068, "mem_avail_kib": 26933580, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056402.106, "mem_avail_kib": 26939924, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056403.145, "mem_avail_kib": 26930428, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056404.184, "mem_avail_kib": 26938356, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056405.223, "mem_avail_kib": 26943280, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056406.260, "mem_avail_kib": 26946776, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056407.295, "mem_avail_kib": 26936652, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056408.331, "mem_avail_kib": 26941648, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056409.366, "mem_avail_kib": 26939432, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056410.404, "mem_avail_kib": 26933112, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056411.442, "mem_avail_kib": 26941192, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056412.482, "mem_avail_kib": 26934848, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056413.520, "mem_avail_kib": 26937536, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056414.557, "mem_avail_kib": 26862620, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056415.597, "mem_avail_kib": 26867168, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056416.634, "mem_avail_kib": 26860468, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056417.672, "mem_avail_kib": 26860476, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056418.716, "mem_avail_kib": 26859876, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056419.754, "mem_avail_kib": 26816916, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056420.791, "mem_avail_kib": 26814644, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056421.828, "mem_avail_kib": 26819700, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056422.867, "mem_avail_kib": 26830876, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056423.907, "mem_avail_kib": 26831360, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056424.948, "mem_avail_kib": 26829344, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056425.987, "mem_avail_kib": 26834072, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056427.024, "mem_avail_kib": 26834624, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056428.062, "mem_avail_kib": 26826336, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056429.098, "mem_avail_kib": 26823144, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056430.135, "mem_avail_kib": 26817984, "swap_free_kib": 7060264, "vram_used": [16562462720,15780212736]} +{"t": 1791056431.173, "mem_avail_kib": 26825216, "swap_free_kib": 7060520, "vram_used": [16562462720,15780212736]} +{"t": 1791056432.209, "mem_avail_kib": 26819872, "swap_free_kib": 7060520, "vram_used": [16562462720,15780212736]} +{"t": 1791056433.248, "mem_avail_kib": 26812552, "swap_free_kib": 7060520, "vram_used": [16562462720,15780212736]} +{"t": 1791056434.285, "mem_avail_kib": 26812104, "swap_free_kib": 7060520, "vram_used": [16562462720,15780212736]} +{"t": 1791056435.327, "mem_avail_kib": 26810664, "swap_free_kib": 7060520, "vram_used": [16562462720,15780212736]} +{"t": 1791056436.366, "mem_avail_kib": 26818768, "swap_free_kib": 7060520, "vram_used": [16562462720,15780212736]} +{"t": 1791056437.404, "mem_avail_kib": 26827684, "swap_free_kib": 7060520, "vram_used": [16562462720,15780212736]} +{"t": 1791056438.441, "mem_avail_kib": 26825888, "swap_free_kib": 7060520, "vram_used": [16562462720,15780212736]} +{"t": 1791056439.479, "mem_avail_kib": 26827940, "swap_free_kib": 7060520, "vram_used": [16562462720,15780212736]} +{"t": 1791056440.519, "mem_avail_kib": 26823660, "swap_free_kib": 7060520, "vram_used": [16562462720,15780212736]} +{"t": 1791056441.558, "mem_avail_kib": 26820544, "swap_free_kib": 7060520, "vram_used": [16562462720,15780212736]} +{"t": 1791056442.594, "mem_avail_kib": 26825624, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056443.632, "mem_avail_kib": 26816164, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056444.670, "mem_avail_kib": 26748920, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056445.706, "mem_avail_kib": 26759020, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056446.743, "mem_avail_kib": 26761728, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056447.779, "mem_avail_kib": 26756980, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056448.816, "mem_avail_kib": 26723176, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056449.853, "mem_avail_kib": 26727756, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056450.895, "mem_avail_kib": 26690000, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056451.937, "mem_avail_kib": 26688288, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056452.977, "mem_avail_kib": 26686608, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056454.016, "mem_avail_kib": 26690356, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056455.055, "mem_avail_kib": 26693484, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056456.093, "mem_avail_kib": 26690756, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056457.132, "mem_avail_kib": 26687712, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056458.166, "mem_avail_kib": 26703320, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056459.204, "mem_avail_kib": 26695548, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056460.245, "mem_avail_kib": 26699980, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056461.284, "mem_avail_kib": 26693664, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056462.323, "mem_avail_kib": 26703528, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056463.360, "mem_avail_kib": 26705700, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056464.395, "mem_avail_kib": 26704740, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056465.431, "mem_avail_kib": 26707748, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056466.469, "mem_avail_kib": 26706972, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056467.507, "mem_avail_kib": 26712500, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056468.545, "mem_avail_kib": 26687612, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056469.582, "mem_avail_kib": 26693524, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056470.620, "mem_avail_kib": 26688688, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056471.661, "mem_avail_kib": 26687240, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056472.701, "mem_avail_kib": 26684420, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056473.739, "mem_avail_kib": 26696344, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056474.779, "mem_avail_kib": 26701084, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056475.820, "mem_avail_kib": 26625316, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056476.862, "mem_avail_kib": 26632568, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056477.899, "mem_avail_kib": 26631368, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056478.935, "mem_avail_kib": 26643548, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056479.972, "mem_avail_kib": 26646812, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056481.011, "mem_avail_kib": 26649036, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056482.047, "mem_avail_kib": 26597980, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056483.084, "mem_avail_kib": 26597352, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056484.122, "mem_avail_kib": 26601356, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056485.162, "mem_avail_kib": 26600404, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056486.201, "mem_avail_kib": 26591804, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056487.238, "mem_avail_kib": 26594940, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056488.275, "mem_avail_kib": 26589616, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056489.313, "mem_avail_kib": 26595720, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056490.349, "mem_avail_kib": 26586124, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056491.388, "mem_avail_kib": 26585280, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056492.426, "mem_avail_kib": 26595164, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056493.463, "mem_avail_kib": 26599156, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056494.501, "mem_avail_kib": 26591680, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056495.537, "mem_avail_kib": 26584956, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056496.576, "mem_avail_kib": 26581144, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056497.614, "mem_avail_kib": 26596828, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056498.652, "mem_avail_kib": 26606072, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056499.689, "mem_avail_kib": 26607228, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056500.727, "mem_avail_kib": 26596608, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056501.766, "mem_avail_kib": 26602828, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056502.804, "mem_avail_kib": 26584404, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056503.842, "mem_avail_kib": 26594072, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056504.884, "mem_avail_kib": 26606064, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056505.920, "mem_avail_kib": 26612924, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056506.957, "mem_avail_kib": 26614020, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056507.994, "mem_avail_kib": 26529720, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056509.030, "mem_avail_kib": 26510572, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056510.066, "mem_avail_kib": 26494876, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056511.105, "mem_avail_kib": 26503716, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056512.143, "mem_avail_kib": 26509276, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056513.182, "mem_avail_kib": 26512172, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056514.218, "mem_avail_kib": 26468064, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056515.255, "mem_avail_kib": 26462744, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056516.292, "mem_avail_kib": 26459220, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056517.328, "mem_avail_kib": 26458756, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056518.364, "mem_avail_kib": 26468484, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056519.400, "mem_avail_kib": 26467308, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056520.440, "mem_avail_kib": 26474920, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056521.479, "mem_avail_kib": 26465712, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056522.514, "mem_avail_kib": 26457924, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056523.552, "mem_avail_kib": 26458152, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056524.588, "mem_avail_kib": 26467844, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056525.624, "mem_avail_kib": 26473036, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056526.660, "mem_avail_kib": 26476116, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056527.698, "mem_avail_kib": 26470296, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056528.734, "mem_avail_kib": 26478668, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056529.773, "mem_avail_kib": 26480912, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056530.810, "mem_avail_kib": 26473896, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056531.846, "mem_avail_kib": 26483160, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056532.883, "mem_avail_kib": 26494944, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056533.920, "mem_avail_kib": 26490612, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056534.958, "mem_avail_kib": 26491580, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056535.997, "mem_avail_kib": 26492172, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056537.034, "mem_avail_kib": 26481332, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056538.069, "mem_avail_kib": 26477212, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056539.105, "mem_avail_kib": 26484236, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056540.142, "mem_avail_kib": 26480300, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056541.187, "mem_avail_kib": 26413868, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056542.226, "mem_avail_kib": 26419576, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056543.262, "mem_avail_kib": 26419544, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056544.299, "mem_avail_kib": 26417352, "swap_free_kib": 7060776, "vram_used": [16562462720,15780212736]} +{"t": 1791056545.338, "mem_avail_kib": 26410184, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056546.373, "mem_avail_kib": 26411440, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056547.410, "mem_avail_kib": 26364204, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056548.449, "mem_avail_kib": 26429448, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056549.488, "mem_avail_kib": 26442104, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056550.524, "mem_avail_kib": 26445372, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056551.560, "mem_avail_kib": 26444128, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056552.602, "mem_avail_kib": 26447020, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056553.639, "mem_avail_kib": 26449068, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056554.677, "mem_avail_kib": 26459664, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056555.715, "mem_avail_kib": 26464968, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056556.758, "mem_avail_kib": 26466256, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056557.800, "mem_avail_kib": 26475960, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056558.839, "mem_avail_kib": 26470196, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056559.878, "mem_avail_kib": 26470076, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056560.916, "mem_avail_kib": 26470032, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056561.952, "mem_avail_kib": 26473744, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056562.991, "mem_avail_kib": 26483032, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056564.028, "mem_avail_kib": 26476552, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056565.065, "mem_avail_kib": 26478520, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056566.103, "mem_avail_kib": 26480716, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056567.143, "mem_avail_kib": 26483992, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056568.179, "mem_avail_kib": 26482280, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056569.214, "mem_avail_kib": 26474100, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056570.253, "mem_avail_kib": 26466308, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056571.290, "mem_avail_kib": 26456712, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056572.328, "mem_avail_kib": 26460928, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056573.365, "mem_avail_kib": 26455888, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056574.401, "mem_avail_kib": 26459284, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056575.438, "mem_avail_kib": 26330540, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056576.475, "mem_avail_kib": 26333524, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056577.512, "mem_avail_kib": 26345164, "swap_free_kib": 7061032, "vram_used": [16562462720,15780212736]} +{"t": 1791056578.550, "mem_avail_kib": 26356328, "swap_free_kib": 7061288, "vram_used": [16562462720,15780212736]} +{"t": 1791056579.586, "mem_avail_kib": 26368708, "swap_free_kib": 7061288, "vram_used": [16562462720,15780212736]} +{"t": 1791056580.623, "mem_avail_kib": 26378068, "swap_free_kib": 7061288, "vram_used": [16562462720,15780212736]} +{"t": 1791056581.662, "mem_avail_kib": 26536596, "swap_free_kib": 7061288, "vram_used": [16562462720,15780212736]} +{"t": 1791056582.701, "mem_avail_kib": 27246944, "swap_free_kib": 7061288, "vram_used": [16562462720,15780212736]} +{"t": 1791056583.735, "mem_avail_kib": 27246184, "swap_free_kib": 7061288, "vram_used": [16562462720,15780212736]} +{"t": 1791056584.770, "mem_avail_kib": 27246884, "swap_free_kib": 7061288, "vram_used": [16562462720,15780212736]} +{"t": 1791056585.808, "mem_avail_kib": 27249868, "swap_free_kib": 7061288, "vram_used": [16562462720,15780212736]} +{"t": 1791056586.845, "mem_avail_kib": 27264692, "swap_free_kib": 7061288, "vram_used": [16562462720,15780212736]} +{"t": 1791056587.884, "mem_avail_kib": 27278076, "swap_free_kib": 7061288, "vram_used": [16562462720,15780212736]} +{"t": 1791056588.921, "mem_avail_kib": 27278280, "swap_free_kib": 7061544, "vram_used": [16562462720,15780212736]} +{"t": 1791056589.959, "mem_avail_kib": 27286908, "swap_free_kib": 7061544, "vram_used": [16562462720,15780212736]} +{"t": 1791056590.996, "mem_avail_kib": 27280784, "swap_free_kib": 7061544, "vram_used": [16562462720,15780212736]} +{"t": 1791056592.030, "mem_avail_kib": 27284304, "swap_free_kib": 7061544, "vram_used": [16562462720,15780212736]} +{"t": 1791056593.068, "mem_avail_kib": 27289076, "swap_free_kib": 7061544, "vram_used": [16562462720,15780212736]} +{"t": 1791056594.105, "mem_avail_kib": 27295184, "swap_free_kib": 7061544, "vram_used": [16562462720,15780212736]} +{"t": 1791056595.141, "mem_avail_kib": 27295844, "swap_free_kib": 7061544, "vram_used": [16562462720,15780212736]} +{"t": 1791056596.179, "mem_avail_kib": 27300520, "swap_free_kib": 7061544, "vram_used": [16562462720,15780212736]} +{"t": 1791056597.216, "mem_avail_kib": 27308812, "swap_free_kib": 7061544, "vram_used": [16562462720,15780212736]} +{"t": 1791056598.249, "mem_avail_kib": 27309044, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056599.285, "mem_avail_kib": 27293708, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056600.322, "mem_avail_kib": 27290800, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056601.359, "mem_avail_kib": 27299680, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056602.398, "mem_avail_kib": 27287892, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056603.436, "mem_avail_kib": 27281440, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056604.475, "mem_avail_kib": 27283740, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056605.510, "mem_avail_kib": 27287336, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056606.546, "mem_avail_kib": 27291688, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056607.585, "mem_avail_kib": 27298488, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056608.624, "mem_avail_kib": 27293124, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056609.663, "mem_avail_kib": 27289564, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056610.704, "mem_avail_kib": 27290292, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056611.741, "mem_avail_kib": 27218636, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056612.778, "mem_avail_kib": 27228940, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056613.817, "mem_avail_kib": 27228884, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056614.854, "mem_avail_kib": 27217352, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056615.891, "mem_avail_kib": 27221384, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056616.929, "mem_avail_kib": 27180548, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056617.967, "mem_avail_kib": 27180168, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056619.000, "mem_avail_kib": 27168504, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056620.034, "mem_avail_kib": 27171584, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056621.071, "mem_avail_kib": 27170460, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056622.109, "mem_avail_kib": 27172984, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056623.149, "mem_avail_kib": 27167644, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056624.187, "mem_avail_kib": 27168872, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056625.226, "mem_avail_kib": 27174068, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056626.266, "mem_avail_kib": 27168032, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056627.304, "mem_avail_kib": 27177440, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056628.342, "mem_avail_kib": 27175160, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056629.376, "mem_avail_kib": 27183136, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056630.416, "mem_avail_kib": 27183732, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056631.458, "mem_avail_kib": 27145584, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056632.494, "mem_avail_kib": 27153072, "swap_free_kib": 7062056, "vram_used": [16562462720,15780212736]} +{"t": 1791056633.537, "mem_avail_kib": 27029244, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056634.576, "mem_avail_kib": 27165880, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056635.613, "mem_avail_kib": 27171352, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056636.649, "mem_avail_kib": 27174252, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056637.688, "mem_avail_kib": 27178088, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056638.731, "mem_avail_kib": 27176348, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056639.768, "mem_avail_kib": 27193528, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056640.806, "mem_avail_kib": 27212948, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056641.844, "mem_avail_kib": 27230136, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056642.880, "mem_avail_kib": 27243648, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056643.917, "mem_avail_kib": 27245220, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056644.957, "mem_avail_kib": 27256476, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056645.996, "mem_avail_kib": 27262944, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056647.033, "mem_avail_kib": 27260152, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056648.072, "mem_avail_kib": 27276816, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056649.111, "mem_avail_kib": 27286940, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056650.146, "mem_avail_kib": 27282488, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056651.183, "mem_avail_kib": 27276668, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056652.222, "mem_avail_kib": 27279968, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056653.259, "mem_avail_kib": 27277928, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056654.295, "mem_avail_kib": 27289496, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056655.335, "mem_avail_kib": 27284848, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056656.378, "mem_avail_kib": 27285108, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056657.419, "mem_avail_kib": 27294072, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056658.457, "mem_avail_kib": 27299836, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056659.493, "mem_avail_kib": 27302556, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056660.531, "mem_avail_kib": 27293024, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056661.570, "mem_avail_kib": 27282192, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056662.609, "mem_avail_kib": 27224944, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056663.645, "mem_avail_kib": 27226892, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056664.682, "mem_avail_kib": 27217612, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056665.721, "mem_avail_kib": 27217320, "swap_free_kib": 7062056, "vram_used": [16562462720,15780356096]} +{"t": 1791056666.759, "mem_avail_kib": 27220928, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056667.797, "mem_avail_kib": 27166284, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056668.835, "mem_avail_kib": 27169080, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056669.872, "mem_avail_kib": 27157400, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056670.911, "mem_avail_kib": 27161852, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056671.949, "mem_avail_kib": 27164192, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056672.991, "mem_avail_kib": 27171880, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056674.032, "mem_avail_kib": 27175900, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056675.071, "mem_avail_kib": 27180048, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056676.108, "mem_avail_kib": 27178816, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056677.146, "mem_avail_kib": 27183336, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056678.188, "mem_avail_kib": 27182644, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056679.225, "mem_avail_kib": 27180336, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056680.260, "mem_avail_kib": 27171328, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056681.298, "mem_avail_kib": 27160368, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056682.336, "mem_avail_kib": 27166876, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056683.375, "mem_avail_kib": 27160132, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056684.415, "mem_avail_kib": 27044632, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056685.456, "mem_avail_kib": 27183840, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056686.494, "mem_avail_kib": 27183056, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056687.531, "mem_avail_kib": 27206132, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056688.567, "mem_avail_kib": 27208128, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056689.603, "mem_avail_kib": 27216564, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056690.640, "mem_avail_kib": 27224072, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056691.678, "mem_avail_kib": 27237128, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056692.716, "mem_avail_kib": 27225840, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056693.755, "mem_avail_kib": 27232016, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056694.792, "mem_avail_kib": 27232940, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056695.830, "mem_avail_kib": 27249248, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056696.867, "mem_avail_kib": 27255452, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056697.908, "mem_avail_kib": 27250532, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056698.948, "mem_avail_kib": 27254992, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056699.985, "mem_avail_kib": 27262772, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056701.023, "mem_avail_kib": 27273692, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056702.062, "mem_avail_kib": 27269552, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056703.102, "mem_avail_kib": 27273960, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056704.139, "mem_avail_kib": 27279876, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056705.175, "mem_avail_kib": 27284552, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056706.213, "mem_avail_kib": 27285008, "swap_free_kib": 7063336, "vram_used": [16562462720,15780356096]} +{"t": 1791056707.248, "mem_avail_kib": 27283904, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056708.285, "mem_avail_kib": 27289520, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056709.325, "mem_avail_kib": 27298924, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056710.364, "mem_avail_kib": 27298892, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056711.400, "mem_avail_kib": 27302720, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056712.436, "mem_avail_kib": 27306144, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056713.473, "mem_avail_kib": 27234856, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056714.514, "mem_avail_kib": 27236428, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056715.552, "mem_avail_kib": 27222628, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056716.590, "mem_avail_kib": 27221956, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056717.628, "mem_avail_kib": 27220216, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056718.666, "mem_avail_kib": 27172220, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056719.706, "mem_avail_kib": 27169292, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056720.744, "mem_avail_kib": 27159108, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056721.782, "mem_avail_kib": 27167504, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056722.823, "mem_avail_kib": 27168972, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056723.860, "mem_avail_kib": 27177064, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056724.897, "mem_avail_kib": 27167316, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056725.935, "mem_avail_kib": 27175624, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056726.974, "mem_avail_kib": 27170764, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056728.013, "mem_avail_kib": 27176080, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056729.052, "mem_avail_kib": 27178284, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056730.090, "mem_avail_kib": 27170172, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056731.130, "mem_avail_kib": 27172264, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056732.166, "mem_avail_kib": 27176696, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056733.210, "mem_avail_kib": 27173748, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056734.251, "mem_avail_kib": 27178952, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056735.291, "mem_avail_kib": 27064460, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056736.329, "mem_avail_kib": 27049120, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056737.367, "mem_avail_kib": 27181884, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056738.404, "mem_avail_kib": 27182768, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056739.442, "mem_avail_kib": 27179504, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056740.476, "mem_avail_kib": 27181000, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056741.517, "mem_avail_kib": 27193236, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056742.556, "mem_avail_kib": 27210160, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056743.597, "mem_avail_kib": 27222064, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056744.637, "mem_avail_kib": 27221168, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056745.676, "mem_avail_kib": 27238156, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056746.715, "mem_avail_kib": 27243164, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056747.751, "mem_avail_kib": 27249048, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056748.786, "mem_avail_kib": 27264620, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056749.822, "mem_avail_kib": 27253444, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056750.861, "mem_avail_kib": 27260392, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056751.901, "mem_avail_kib": 27267208, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056752.939, "mem_avail_kib": 27254616, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056753.976, "mem_avail_kib": 27273968, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056755.016, "mem_avail_kib": 27280096, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056756.056, "mem_avail_kib": 27290644, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056757.095, "mem_avail_kib": 27280580, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056758.131, "mem_avail_kib": 27283628, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056759.167, "mem_avail_kib": 27274432, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056760.205, "mem_avail_kib": 27276652, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056761.242, "mem_avail_kib": 27270784, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056762.281, "mem_avail_kib": 27272056, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056763.321, "mem_avail_kib": 27278708, "swap_free_kib": 7063592, "vram_used": [16562462720,15780356096]} +{"t": 1791056764.358, "mem_avail_kib": 27197104, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056765.394, "mem_avail_kib": 27205764, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056766.429, "mem_avail_kib": 27211032, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056767.466, "mem_avail_kib": 27205440, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056768.502, "mem_avail_kib": 27197648, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056769.541, "mem_avail_kib": 27156648, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056770.581, "mem_avail_kib": 27150580, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056771.616, "mem_avail_kib": 27158092, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056772.655, "mem_avail_kib": 27151404, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056773.694, "mem_avail_kib": 27158576, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056774.732, "mem_avail_kib": 27167108, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056775.767, "mem_avail_kib": 27168544, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056776.804, "mem_avail_kib": 27169652, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056777.840, "mem_avail_kib": 27169424, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056778.874, "mem_avail_kib": 27171516, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056779.911, "mem_avail_kib": 27166564, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056780.948, "mem_avail_kib": 27161048, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056781.985, "mem_avail_kib": 27169404, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056783.023, "mem_avail_kib": 27176032, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056784.062, "mem_avail_kib": 27174376, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056785.098, "mem_avail_kib": 27170624, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056786.134, "mem_avail_kib": 27167532, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056787.173, "mem_avail_kib": 27166348, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056788.210, "mem_avail_kib": 27172048, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056789.248, "mem_avail_kib": 27175260, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056790.288, "mem_avail_kib": 27173536, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056791.325, "mem_avail_kib": 27165868, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056792.368, "mem_avail_kib": 27152892, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056793.406, "mem_avail_kib": 27030652, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056794.439, "mem_avail_kib": 27043260, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056795.478, "mem_avail_kib": 27043704, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056796.515, "mem_avail_kib": 27035424, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056797.553, "mem_avail_kib": 27040464, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056798.591, "mem_avail_kib": 27042400, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056799.628, "mem_avail_kib": 27039104, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056800.665, "mem_avail_kib": 27033548, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056801.700, "mem_avail_kib": 26978336, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056802.735, "mem_avail_kib": 26971956, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056803.775, "mem_avail_kib": 26980216, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056804.814, "mem_avail_kib": 26982276, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056805.852, "mem_avail_kib": 26986832, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056806.890, "mem_avail_kib": 26948560, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056807.930, "mem_avail_kib": 26948988, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056808.966, "mem_avail_kib": 26951868, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056810.002, "mem_avail_kib": 26940112, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056811.041, "mem_avail_kib": 26944016, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056812.079, "mem_avail_kib": 26949708, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056813.120, "mem_avail_kib": 26945644, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056814.159, "mem_avail_kib": 26943744, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056815.198, "mem_avail_kib": 26910316, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056816.233, "mem_avail_kib": 26899016, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056817.270, "mem_avail_kib": 26904452, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056818.305, "mem_avail_kib": 26910580, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056819.347, "mem_avail_kib": 26904028, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056820.385, "mem_avail_kib": 26902196, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056821.423, "mem_avail_kib": 26899152, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056822.461, "mem_avail_kib": 26906520, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056823.499, "mem_avail_kib": 26900256, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056824.533, "mem_avail_kib": 26893500, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056825.569, "mem_avail_kib": 26910824, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056826.606, "mem_avail_kib": 26912284, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056827.644, "mem_avail_kib": 26911564, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056828.681, "mem_avail_kib": 26919200, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056829.719, "mem_avail_kib": 26925228, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056830.758, "mem_avail_kib": 26929044, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056831.793, "mem_avail_kib": 26850008, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056832.831, "mem_avail_kib": 26852896, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056833.868, "mem_avail_kib": 26865016, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056834.907, "mem_avail_kib": 26864896, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056835.944, "mem_avail_kib": 26867136, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056836.982, "mem_avail_kib": 26817568, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056838.023, "mem_avail_kib": 26820036, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056839.060, "mem_avail_kib": 26811416, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056840.095, "mem_avail_kib": 26811936, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056841.130, "mem_avail_kib": 26812128, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056842.167, "mem_avail_kib": 26820260, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056843.208, "mem_avail_kib": 26815316, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056844.247, "mem_avail_kib": 26821004, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056845.281, "mem_avail_kib": 26835368, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056846.317, "mem_avail_kib": 26831480, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056847.353, "mem_avail_kib": 26825416, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056848.391, "mem_avail_kib": 26826536, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056849.430, "mem_avail_kib": 26831128, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056850.469, "mem_avail_kib": 26822828, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056851.507, "mem_avail_kib": 26813012, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056852.546, "mem_avail_kib": 26817212, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056853.583, "mem_avail_kib": 26819072, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056854.622, "mem_avail_kib": 26823168, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056855.660, "mem_avail_kib": 26827620, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056856.699, "mem_avail_kib": 26825708, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056857.738, "mem_avail_kib": 26816848, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056858.776, "mem_avail_kib": 26821920, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056859.815, "mem_avail_kib": 26826656, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056860.852, "mem_avail_kib": 26820228, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056861.887, "mem_avail_kib": 26744668, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056862.925, "mem_avail_kib": 26751844, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056863.964, "mem_avail_kib": 26759528, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056865.000, "mem_avail_kib": 26764416, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056866.043, "mem_avail_kib": 26754976, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056867.082, "mem_avail_kib": 26748568, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056868.121, "mem_avail_kib": 26709380, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056869.157, "mem_avail_kib": 26705412, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056870.193, "mem_avail_kib": 26687324, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056871.230, "mem_avail_kib": 26682380, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056872.266, "mem_avail_kib": 26685172, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056873.303, "mem_avail_kib": 26693936, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056874.340, "mem_avail_kib": 26691652, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056875.376, "mem_avail_kib": 26679728, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056876.414, "mem_avail_kib": 26694328, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056877.451, "mem_avail_kib": 26681808, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056878.488, "mem_avail_kib": 26674752, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056879.524, "mem_avail_kib": 26670300, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056880.560, "mem_avail_kib": 26677420, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056881.598, "mem_avail_kib": 26680136, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056882.637, "mem_avail_kib": 26673872, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056883.675, "mem_avail_kib": 26671700, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056884.713, "mem_avail_kib": 26680096, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056885.748, "mem_avail_kib": 26687196, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056886.786, "mem_avail_kib": 26686556, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056887.827, "mem_avail_kib": 26681220, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056888.862, "mem_avail_kib": 26677588, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056889.903, "mem_avail_kib": 26700524, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056890.945, "mem_avail_kib": 26704172, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056891.983, "mem_avail_kib": 26707240, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056893.020, "mem_avail_kib": 26636548, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056894.057, "mem_avail_kib": 26637328, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056895.095, "mem_avail_kib": 26641952, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056896.132, "mem_avail_kib": 26642128, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056897.170, "mem_avail_kib": 26642644, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056898.207, "mem_avail_kib": 26646672, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056899.244, "mem_avail_kib": 26589056, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056900.280, "mem_avail_kib": 26587188, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056901.317, "mem_avail_kib": 26589372, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056902.352, "mem_avail_kib": 26585352, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056903.389, "mem_avail_kib": 26586308, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056904.425, "mem_avail_kib": 26588040, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056905.462, "mem_avail_kib": 26591516, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056906.496, "mem_avail_kib": 26602804, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056907.531, "mem_avail_kib": 26595900, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056908.568, "mem_avail_kib": 26601196, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056909.605, "mem_avail_kib": 26596976, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056910.641, "mem_avail_kib": 26586220, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056911.679, "mem_avail_kib": 26584368, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056912.719, "mem_avail_kib": 26592776, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056913.756, "mem_avail_kib": 26599676, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056914.793, "mem_avail_kib": 26597960, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056915.830, "mem_avail_kib": 26599456, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056916.868, "mem_avail_kib": 26600032, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056917.912, "mem_avail_kib": 26606280, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056918.951, "mem_avail_kib": 26610500, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056919.984, "mem_avail_kib": 26592576, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056921.019, "mem_avail_kib": 26595364, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056922.058, "mem_avail_kib": 26593052, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056923.096, "mem_avail_kib": 26599416, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056924.137, "mem_avail_kib": 26598716, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056925.177, "mem_avail_kib": 26526708, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056926.217, "mem_avail_kib": 26532616, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056927.253, "mem_avail_kib": 26525132, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056928.289, "mem_avail_kib": 26527404, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056929.327, "mem_avail_kib": 26534524, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056930.368, "mem_avail_kib": 26517360, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056931.407, "mem_avail_kib": 26508320, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056932.444, "mem_avail_kib": 26518856, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056933.482, "mem_avail_kib": 26532072, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056934.527, "mem_avail_kib": 26525560, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056935.566, "mem_avail_kib": 26542084, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056936.604, "mem_avail_kib": 26525880, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056937.642, "mem_avail_kib": 26543500, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056938.681, "mem_avail_kib": 26551764, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056939.720, "mem_avail_kib": 26563440, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056940.758, "mem_avail_kib": 26567696, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056941.796, "mem_avail_kib": 26582568, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056942.835, "mem_avail_kib": 26492888, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056943.870, "mem_avail_kib": 27202780, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056944.906, "mem_avail_kib": 27200072, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056945.944, "mem_avail_kib": 27206604, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056946.983, "mem_avail_kib": 27216936, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056948.020, "mem_avail_kib": 27229080, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056949.059, "mem_avail_kib": 27241764, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056950.096, "mem_avail_kib": 27240568, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056951.134, "mem_avail_kib": 27240920, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056952.172, "mem_avail_kib": 27258576, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056953.210, "mem_avail_kib": 27259612, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056954.247, "mem_avail_kib": 27272748, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056955.286, "mem_avail_kib": 27277216, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056956.326, "mem_avail_kib": 27282428, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056957.361, "mem_avail_kib": 27281552, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056958.400, "mem_avail_kib": 27283852, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056959.440, "mem_avail_kib": 27280848, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056960.479, "mem_avail_kib": 27289980, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056961.515, "mem_avail_kib": 27290736, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056962.552, "mem_avail_kib": 27295236, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056963.590, "mem_avail_kib": 27296780, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056964.630, "mem_avail_kib": 27286064, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056965.668, "mem_avail_kib": 27293052, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056966.705, "mem_avail_kib": 27287168, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056967.743, "mem_avail_kib": 27294492, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056968.780, "mem_avail_kib": 27295432, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056969.818, "mem_avail_kib": 27284960, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056970.856, "mem_avail_kib": 27289032, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056971.892, "mem_avail_kib": 27216080, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056972.928, "mem_avail_kib": 27212760, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056973.970, "mem_avail_kib": 27217044, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056975.010, "mem_avail_kib": 27224000, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056976.049, "mem_avail_kib": 27235856, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056977.089, "mem_avail_kib": 27178204, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056978.127, "mem_avail_kib": 27174724, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056979.165, "mem_avail_kib": 27177504, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056980.201, "mem_avail_kib": 27175176, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056981.240, "mem_avail_kib": 27163472, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056982.279, "mem_avail_kib": 27168700, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056983.315, "mem_avail_kib": 27161536, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056984.352, "mem_avail_kib": 27165080, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056985.390, "mem_avail_kib": 27154624, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056986.426, "mem_avail_kib": 27147688, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056987.462, "mem_avail_kib": 27157564, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056988.501, "mem_avail_kib": 27152092, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056989.540, "mem_avail_kib": 27159644, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056990.577, "mem_avail_kib": 27160712, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056991.614, "mem_avail_kib": 27164748, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056992.650, "mem_avail_kib": 27158332, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056993.687, "mem_avail_kib": 27161512, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056994.727, "mem_avail_kib": 27155344, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056995.764, "mem_avail_kib": 27146944, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056996.798, "mem_avail_kib": 27160024, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056997.840, "mem_avail_kib": 27143144, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056998.876, "mem_avail_kib": 27138880, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791056999.912, "mem_avail_kib": 27030400, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791057000.948, "mem_avail_kib": 27031624, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791057001.988, "mem_avail_kib": 27024940, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791057003.029, "mem_avail_kib": 27029412, "swap_free_kib": 7063848, "vram_used": [16562462720,15780356096]} +{"t": 1791057004.069, "mem_avail_kib": 27035228, "swap_free_kib": 7064104, "vram_used": [16562462720,15780356096]} +{"t": 1791057005.106, "mem_avail_kib": 27041836, "swap_free_kib": 7064104, "vram_used": [16562462720,15780356096]} +{"t": 1791057006.144, "mem_avail_kib": 27045192, "swap_free_kib": 7064104, "vram_used": [16562462720,15780356096]} +{"t": 1791057007.182, "mem_avail_kib": 26988336, "swap_free_kib": 7064104, "vram_used": [16562462720,15780356096]} +{"t": 1791057008.219, "mem_avail_kib": 27001344, "swap_free_kib": 7064104, "vram_used": [16562462720,15780356096]} +{"t": 1791057009.257, "mem_avail_kib": 27001116, "swap_free_kib": 7064104, "vram_used": [16562462720,15780356096]} +{"t": 1791057010.293, "mem_avail_kib": 26998056, "swap_free_kib": 7064104, "vram_used": [16562462720,15780356096]} +{"t": 1791057011.332, "mem_avail_kib": 27001124, "swap_free_kib": 7064360, "vram_used": [16562462720,15780356096]} +{"t": 1791057012.373, "mem_avail_kib": 26998588, "swap_free_kib": 7064360, "vram_used": [16562462720,15780356096]} +{"t": 1791057013.411, "mem_avail_kib": 26947260, "swap_free_kib": 7064360, "vram_used": [16562462720,15780356096]} +{"t": 1791057014.451, "mem_avail_kib": 26939092, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057015.488, "mem_avail_kib": 26938508, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057016.527, "mem_avail_kib": 26936284, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057017.564, "mem_avail_kib": 26933688, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057018.600, "mem_avail_kib": 26933448, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057019.640, "mem_avail_kib": 26916876, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057020.677, "mem_avail_kib": 26914256, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057021.716, "mem_avail_kib": 26920008, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057022.753, "mem_avail_kib": 26922608, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057023.790, "mem_avail_kib": 26934740, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057024.828, "mem_avail_kib": 26941320, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057025.869, "mem_avail_kib": 26947652, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057026.909, "mem_avail_kib": 26934888, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057027.945, "mem_avail_kib": 26931616, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057028.982, "mem_avail_kib": 26933752, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057030.019, "mem_avail_kib": 26930524, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057031.059, "mem_avail_kib": 26928352, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057032.098, "mem_avail_kib": 26933440, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057033.138, "mem_avail_kib": 26938800, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057034.178, "mem_avail_kib": 26943004, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057035.216, "mem_avail_kib": 26935168, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057036.253, "mem_avail_kib": 26928456, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057037.291, "mem_avail_kib": 26854016, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057038.328, "mem_avail_kib": 26864556, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057039.366, "mem_avail_kib": 26869516, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057040.403, "mem_avail_kib": 26887532, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057041.444, "mem_avail_kib": 26880252, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057042.481, "mem_avail_kib": 26851308, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057043.519, "mem_avail_kib": 26837640, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057044.556, "mem_avail_kib": 26842932, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057045.595, "mem_avail_kib": 26845048, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057046.634, "mem_avail_kib": 26847252, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057047.672, "mem_avail_kib": 26846076, "swap_free_kib": 7064616, "vram_used": [16562462720,15780356096]} +{"t": 1791057048.709, "mem_avail_kib": 26838116, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057049.745, "mem_avail_kib": 26845448, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057050.782, "mem_avail_kib": 26833368, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057051.820, "mem_avail_kib": 26831704, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057052.858, "mem_avail_kib": 26827604, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057053.898, "mem_avail_kib": 26829840, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057054.939, "mem_avail_kib": 26838380, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057055.977, "mem_avail_kib": 26837196, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057057.015, "mem_avail_kib": 26828992, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057058.052, "mem_avail_kib": 26821608, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057059.090, "mem_avail_kib": 26824316, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057060.128, "mem_avail_kib": 26826412, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057061.165, "mem_avail_kib": 26822128, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057062.203, "mem_avail_kib": 26816756, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057063.242, "mem_avail_kib": 26820560, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057064.282, "mem_avail_kib": 26825456, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057065.320, "mem_avail_kib": 26830708, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057066.357, "mem_avail_kib": 26824308, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057067.394, "mem_avail_kib": 26752540, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057068.430, "mem_avail_kib": 26747444, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057069.466, "mem_avail_kib": 26740600, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057070.503, "mem_avail_kib": 26734032, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057071.542, "mem_avail_kib": 26731196, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057072.579, "mem_avail_kib": 26729872, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057073.617, "mem_avail_kib": 26684864, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057074.657, "mem_avail_kib": 26678148, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057075.693, "mem_avail_kib": 26689868, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057076.732, "mem_avail_kib": 26695888, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057077.772, "mem_avail_kib": 26702588, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057078.809, "mem_avail_kib": 26697280, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057079.850, "mem_avail_kib": 26702228, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057080.886, "mem_avail_kib": 26704592, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057081.923, "mem_avail_kib": 26698412, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057082.961, "mem_avail_kib": 26701956, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057083.999, "mem_avail_kib": 26704804, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057085.038, "mem_avail_kib": 26704812, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057086.076, "mem_avail_kib": 26707056, "swap_free_kib": 7065128, "vram_used": [16562462720,15780356096]} +{"t": 1791057087.115, "mem_avail_kib": 26684320, "swap_free_kib": 7065640, "vram_used": [16562462720,15780356096]} +{"t": 1791057088.155, "mem_avail_kib": 26678496, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057089.192, "mem_avail_kib": 26683716, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057090.229, "mem_avail_kib": 26685556, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057091.265, "mem_avail_kib": 26687284, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057092.304, "mem_avail_kib": 26691696, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057093.341, "mem_avail_kib": 26682872, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057094.380, "mem_avail_kib": 26686088, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057095.417, "mem_avail_kib": 26694112, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057096.453, "mem_avail_kib": 26696244, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057097.489, "mem_avail_kib": 26699616, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057098.527, "mem_avail_kib": 26706152, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057099.563, "mem_avail_kib": 26645076, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057100.599, "mem_avail_kib": 26651648, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057101.639, "mem_avail_kib": 26645064, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057102.675, "mem_avail_kib": 26636728, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057103.712, "mem_avail_kib": 26637844, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057104.753, "mem_avail_kib": 26637228, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057105.792, "mem_avail_kib": 26583192, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057106.832, "mem_avail_kib": 26581180, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057107.872, "mem_avail_kib": 26581108, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057108.911, "mem_avail_kib": 26588108, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057109.948, "mem_avail_kib": 26599952, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057110.985, "mem_avail_kib": 26600108, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057112.023, "mem_avail_kib": 26601732, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057113.061, "mem_avail_kib": 26602484, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057114.097, "mem_avail_kib": 26592736, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057115.135, "mem_avail_kib": 26594956, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057116.174, "mem_avail_kib": 26599640, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057117.211, "mem_avail_kib": 26593160, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057118.251, "mem_avail_kib": 26600084, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057119.288, "mem_avail_kib": 26573664, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057120.327, "mem_avail_kib": 26567104, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057121.365, "mem_avail_kib": 26584752, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057122.400, "mem_avail_kib": 26595396, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057123.437, "mem_avail_kib": 26591652, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057124.477, "mem_avail_kib": 26585420, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057125.517, "mem_avail_kib": 26584796, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057126.558, "mem_avail_kib": 26584636, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057127.593, "mem_avail_kib": 26580720, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057128.632, "mem_avail_kib": 26581332, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057129.668, "mem_avail_kib": 26573608, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057130.703, "mem_avail_kib": 26583168, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057131.741, "mem_avail_kib": 26518672, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057132.780, "mem_avail_kib": 26528668, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057133.816, "mem_avail_kib": 26532224, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057134.853, "mem_avail_kib": 26529100, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057135.891, "mem_avail_kib": 26530788, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057136.929, "mem_avail_kib": 26530888, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057137.964, "mem_avail_kib": 26527108, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057139.001, "mem_avail_kib": 26538132, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057140.040, "mem_avail_kib": 26557344, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057141.078, "mem_avail_kib": 26555040, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057142.114, "mem_avail_kib": 26559288, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057143.158, "mem_avail_kib": 26550828, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057144.196, "mem_avail_kib": 26550164, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057145.234, "mem_avail_kib": 26554032, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057146.270, "mem_avail_kib": 26558576, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057147.310, "mem_avail_kib": 26567844, "swap_free_kib": 7065896, "vram_used": [16562462720,15780356096]} +{"t": 1791057148.348, "mem_avail_kib": 26467652, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057149.389, "mem_avail_kib": 26936572, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057150.426, "mem_avail_kib": 26950892, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057151.466, "mem_avail_kib": 26957240, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057152.504, "mem_avail_kib": 26969276, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057153.541, "mem_avail_kib": 26974864, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057154.579, "mem_avail_kib": 26975064, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057155.620, "mem_avail_kib": 26978340, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057156.659, "mem_avail_kib": 26989320, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057157.696, "mem_avail_kib": 27002780, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057158.734, "mem_avail_kib": 27006948, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057159.772, "mem_avail_kib": 27007132, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057160.810, "mem_avail_kib": 27013664, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057161.849, "mem_avail_kib": 27018108, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057162.887, "mem_avail_kib": 27028380, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057163.925, "mem_avail_kib": 27033712, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057164.965, "mem_avail_kib": 27038304, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057166.003, "mem_avail_kib": 27031064, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057167.042, "mem_avail_kib": 27036896, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057168.080, "mem_avail_kib": 27036708, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057169.119, "mem_avail_kib": 27038844, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057170.157, "mem_avail_kib": 27040860, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057171.196, "mem_avail_kib": 27045052, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057172.233, "mem_avail_kib": 27035660, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057173.271, "mem_avail_kib": 27033884, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057174.314, "mem_avail_kib": 27033848, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057175.353, "mem_avail_kib": 27034968, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057176.393, "mem_avail_kib": 27048860, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057177.431, "mem_avail_kib": 27053672, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057178.470, "mem_avail_kib": 27061072, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057179.510, "mem_avail_kib": 27055232, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057180.548, "mem_avail_kib": 26960632, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057181.585, "mem_avail_kib": 26952380, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057182.621, "mem_avail_kib": 26942108, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057183.660, "mem_avail_kib": 26942236, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057184.699, "mem_avail_kib": 26951896, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057185.737, "mem_avail_kib": 26946176, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057186.772, "mem_avail_kib": 26891816, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057187.808, "mem_avail_kib": 26895856, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057188.847, "mem_avail_kib": 26900296, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057189.885, "mem_avail_kib": 26908432, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057190.924, "mem_avail_kib": 26910856, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057191.964, "mem_avail_kib": 26899196, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057193.003, "mem_avail_kib": 26909492, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057194.043, "mem_avail_kib": 26917308, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057195.082, "mem_avail_kib": 26921092, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057196.124, "mem_avail_kib": 26928576, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057197.164, "mem_avail_kib": 26934672, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057198.207, "mem_avail_kib": 26936748, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057199.246, "mem_avail_kib": 26935680, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057200.287, "mem_avail_kib": 26932848, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057201.326, "mem_avail_kib": 26925164, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057202.364, "mem_avail_kib": 26925404, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057203.402, "mem_avail_kib": 26928744, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057204.442, "mem_avail_kib": 26935124, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057205.479, "mem_avail_kib": 26928952, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057206.518, "mem_avail_kib": 26932008, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057207.559, "mem_avail_kib": 26929600, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057208.597, "mem_avail_kib": 26929688, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057209.633, "mem_avail_kib": 26924308, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057210.669, "mem_avail_kib": 26925888, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057211.706, "mem_avail_kib": 26856688, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057212.743, "mem_avail_kib": 26849444, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057213.780, "mem_avail_kib": 26856692, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057214.818, "mem_avail_kib": 26861644, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057215.854, "mem_avail_kib": 26864968, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057216.890, "mem_avail_kib": 26859064, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057217.926, "mem_avail_kib": 26808096, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057218.966, "mem_avail_kib": 26815488, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057220.005, "mem_avail_kib": 26820624, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057221.042, "mem_avail_kib": 26821724, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057222.079, "mem_avail_kib": 26819284, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057223.118, "mem_avail_kib": 26812176, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057224.157, "mem_avail_kib": 26805956, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057225.195, "mem_avail_kib": 26817024, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057226.233, "mem_avail_kib": 26813616, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057227.273, "mem_avail_kib": 26822488, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057228.312, "mem_avail_kib": 26821472, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057229.351, "mem_avail_kib": 26815252, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057230.390, "mem_avail_kib": 26819828, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057231.430, "mem_avail_kib": 26818056, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057232.468, "mem_avail_kib": 26813328, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057233.506, "mem_avail_kib": 26804028, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057234.542, "mem_avail_kib": 26810440, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057235.580, "mem_avail_kib": 26804100, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057236.619, "mem_avail_kib": 26819344, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057237.664, "mem_avail_kib": 26817396, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057238.701, "mem_avail_kib": 26819852, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057239.740, "mem_avail_kib": 26811676, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057240.778, "mem_avail_kib": 26807268, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057241.816, "mem_avail_kib": 26787380, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057242.854, "mem_avail_kib": 26788112, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057243.891, "mem_avail_kib": 26716208, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057244.927, "mem_avail_kib": 26722464, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057245.964, "mem_avail_kib": 26730984, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057247.002, "mem_avail_kib": 26734972, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057248.038, "mem_avail_kib": 26741224, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057249.078, "mem_avail_kib": 26745100, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057250.114, "mem_avail_kib": 26701952, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057251.154, "mem_avail_kib": 26694368, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057252.191, "mem_avail_kib": 26685568, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057253.230, "mem_avail_kib": 26683856, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057254.266, "mem_avail_kib": 26678140, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057255.303, "mem_avail_kib": 26674628, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057256.338, "mem_avail_kib": 26671136, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057257.373, "mem_avail_kib": 26673032, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057258.412, "mem_avail_kib": 26685356, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057259.453, "mem_avail_kib": 26675828, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057260.496, "mem_avail_kib": 26556140, "swap_free_kib": 7065896, "vram_used": [16562462720,15780495360]} +{"t": 1791057261.534, "mem_avail_kib": 29262176, "swap_free_kib": 7065896, "vram_used": [13813174272,12855922688]} diff --git a/bench/results/2026-10-03-v100-prompt-attn/README.md b/bench/results/2026-10-03-v100-prompt-attn/README.md new file mode 100644 index 0000000000..ab8b32624b --- /dev/null +++ b/bench/results/2026-10-03-v100-prompt-attn/README.md @@ -0,0 +1,104 @@ +# V100 (sm_70): a tensor-core QSA prompt attention (2026-10-03) + +On sm_70 the prompt path ran the QSA attention on the decode kernel, one query at a time with FP32 FMAs. `prompt_attn_v70_kernel` +(`src/kernels/cuda/qsa_prompt_attn.cu`) does the two matrix products on Volta's tensor cores (`mma.sync.m8n8k4`). +`STRATA_PROMPT_ATTN_OLD=1` is the old path, so every number below is the same binary with and without that switch. + +## Rig + +- Tesla V100-PCIE-32GB (sm_70, PCIe gen3 x16), driver 550.120, one card used; a second V100 and a Quadro RTX 4000 (sm_75) are in the + PC and hidden with `CUDA_VISIBLE_DEVICES`. Intel Core i9-7960X (AVX-512), 125 GB RAM, Linux 6.8, CUDA 12.8, GCC 13. +- Engine: `origin/main` at `99f3dbd` (0.1.35 + 83 commits) with this change, built with + `-DCMAKE_CUDA_ARCHITECTURES=70 -DSTRATA_EXPERIMENTAL_SM60=ON`. +- Model: Unsloth `Qwen3.8-Flash-Next-UD-IQ4_XS` (3 shards), packed with `tools/iq_pack.py --compat-bf16`, MTP head from + `tools/mtp_fetch.py`. Not one of setup's models (see the limits). `--max-context 204800`, `--kv int8`, `--spec 2`. + +## Prompt speed + +One server start per configuration (cold), a 2K warm-up request, then three prompts of random words, each answered greedily +(temperature 0, 48 tokens). The numbers are the server's own line `strata serve: prompt N tokens = 0 reused + N read in T ms`. One run per +cell; repeated starts of the same configuration gave the same seconds. + +| Prompt tokens | old path | new kernel | gain | read time, old -> new | +| ---: | ---: | ---: | ---: | ---: | +| 7,194 | 1,010 tok/s | 1,164 tok/s | +15% | 7.1 s -> 6.2 s | +| 28,650 | 1,063 tok/s | 1,251 tok/s | +18% | 26.9 s -> 22.9 s | +| 114,338 | 973 tok/s | 1,123 tok/s | +15% | 117.5 s -> 101.8 s | + +Decode is unchanged within the noise (38-51 tok/s, 48 tokens per request): it does not use this kernel. + +## Where the time went (nsys, one V100, 28,650-token prompt, GPU kernel time of the whole capture: prefill plus 128-193 decode tokens) + +`strata-1gpu-32k-*.nsys-rep`; the "before" capture is 0.1.35 (`d9ab843`), the "after" one is the same base with this change and the BF16 change +of #540 / the closed #593 (BF16 GEMMs through FP16; that one only removes `magma_sgemmEx`, which is not in this table). + +| | before | after | +| --- | ---: | ---: | +| QSA attention on the decode kernel (`attn_chunk_kernel<1>`) | 5.78 s (19.2%) | 0.18 s (decode steps only) | +| `prompt_attn_v70_kernel` | - | 2.16 s (9.0%; 12% of the prompt part) | + +## Accuracy + +`qsa_prompt_attn_parity` (synthetic, no model; build with `-DSTRATA_PARITY_PROMPT_ATTN=ON`), V100, 32K context, 2,048 queries per chunk, output scale 3.6: + +| KV | error vs FP64, old kernel | error vs FP64, new kernel | time per chunk, old -> new | +| --- | ---: | ---: | ---: | +| int8 | 2.4e-6 | 5.1e-6 | 38.1 -> 13.4 ms (2.8x) | +| fp16 | 1.9e-6 | 5.3e-6 | 37.3 -> 28.3 ms (1.3x) | + +The 1,500- and 2,100-token contexts also pass (int8: 3.2x and 2.2x). The test's Q4_0 cases are skipped below sm_80, where the dispatcher keeps the +old kernel (upstream's mode 4 is sm_80+ only). + +## Output checks + +- **Needle recall** (`tools/needle_bench.py --lengths 8k,32k,128k --depths 10,30,50,70,90`, greedy, thinking off, a fresh server per + configuration; prompts of 7.9K, 32.0K and 122.7K tokens): **15 of 15 found with the old path, 15 of 15 with the new kernel.** The read times of + the later depths are shorter because the server reuses the checkpoint of the text before the needle. +- **Greedy output** (48 tokens, the three prompts above): identical for the 28,650- and 114,338-token prompts. For the 7,194-token prompt the + answer starts the same and **differs later**: the kernel sums in another order and a greedy decode can follow a near tie elsewhere. A recall test + with a code word does not tell small shifts of the output distribution apart, and a teacher-forced top-1 comparison was not run. + +## Distribution check (teacher-forced, against the old path and against controls) + +The method of `docs/UNSLOTH_Q4.md`: `STRATA_LOGPOS= STRATA_LOGPOS_TOPK=256` on a serve engine (`--short-read 640 --adapt-every 100000`, greedy, +`--max-context 204800`). Each prompt is a chat: a long first message (8,543 / 32,906 / 99,040 tokens of this repository's docs and code, read by the +batched prompt path, i.e. by the kernel under test), a short reply, and a last message of ~600 tokens of other repository text that goes through the verify +windows, where every token is scored: 617 positions per prompt. KL is old || other over the old run's top 256 plus a bucket for the rest. + +| comparison | KL mean (8.5K / 33K / 99K) | argmax same | top-10 overlap | +| --- | --- | --- | --- | +| old path vs old path (a second run) | 0 / 0 / 0 | 100% | 100% | +| new kernel vs new kernel (a second run) | 0 / 0 / 0 | 100% | 100% | +| **old path vs new kernel** | 0.028 / 0.040 / 0.116 | 97.7 / 93.2 / 86.4% | 93.3 / 91.9 / 88.4% | +| control A: old path, BF16 GEMMs without the FP16 detour (`STRATA_BF16_VIA_F16=0`) | 0.026 / 0.051 / 0.090 | 97.2 / 91.4 / 91.2% | 92.9 / 91.4 / 88.5% | +| control B: old path, `--prefill 2048` instead of auto (8192) | 0.023 / 0.035 / 0.038 | 97.9 / 94.8 / 95.6% | 92.9 / 92.2 / 89.7% | + +Median KL old vs new: 0.0007 / 0.0088 / 0.022; p99: 0.35 / 0.39 / 1.12 (heavy-tailed). The perplexity of the true tokens moves a lot under every +change (old 365 / 127 / 24; new 346 / 192 / 12; control A 395 / 161 / 18), so it says nothing about quality here. + +Reading: the model amplifies small numeric differences, and both controls, which change nothing but the order of the rounding, move the distribution as +much as the kernel does. At 99K the kernel is somewhat above both controls; this is one document and 617 positions, so a real effect there is neither +confirmed nor excluded. + +**The kernel on the model's own data.** A temporary check (not in this PR) ran the decode kernel beside the new one on the same inputs, for every 8th batch of +32 queries, in all 12 QSA layers, on the three prompts above (positions up to 98K, ~5 million (query, head) rows of 256): ||new - old|| / ||old|| per row is +4.4e-7 on average and 4.8e-6 in the worst row; the largest absolute difference is 4e-5 against values up to 31. No trend over the layers or the context +length. That is FP32 rounding level, so the differences above come from the model's sensitivity, not from the kernel's arithmetic. + +## Limits + +- sm_70 only (`__CUDA_ARCH__` 700-749). Q4_0 KV (`--kv q4_0`) and the QSA block scores (`block_scores_kernel`, the warp kernel below sm_80) still use their + old kernels on Volta. +- One request, one card. The Unsloth IQ4_XS pack is not a setup model; the GSQ-RCO quants were not run on this rig. +- The profile above is one capture of one prompt. The distribution check is one document per length; the 99K row is the least certain. + +## Reproduce + +```sh +cmake -S . -B build -DSTRATA_ENABLE_CUDA=ON -DSTRATA_BUILD_TESTS=OFF -DSTRATA_PARITY_PROMPT_ATTN=ON \ + -DCMAKE_CUDA_ARCHITECTURES=70 -DSTRATA_EXPERIMENTAL_SM60=ON +cmake --build build --target strata qsa_prompt_attn_parity -j +CUDA_VISIBLE_DEVICES=0 ./build/qsa_prompt_attn_parity 32768 2048 5 # the accuracy table +STRATA_PROMPT_ATTN_OLD=1 # the old path; without the variable: the new kernel +python tools/needle_bench.py --url http://127.0.0.1:8081 --lengths 8k,32k,128k --depths 10,30,50,70,90 +``` diff --git a/cmake/hip_backend.cmake b/cmake/hip_backend.cmake index 255ee381f5..d59bba9a8a 100644 --- a/cmake/hip_backend.cmake +++ b/cmake/hip_backend.cmake @@ -11,7 +11,7 @@ endif() # 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 gfx1030) +set(_strata_hip_unvalidated gfx1012 gfx1102 gfx1030 gfx1031) # 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}") @@ -44,6 +44,13 @@ string(REPLACE ";" "," STRATA_HIP_ARCHS "${STRATA_HIP_ARCH_LIST}") enable_language(HIP) find_package(hip CONFIG REQUIRED) find_package(hipblas CONFIG REQUIRED) +# Older distro hipBLAS has no workspace API. The compatibility shim uses +# rocBLAS directly for that version; newer hipBLAS keeps its existing path. +set(STRATA_HIP_BLAS_TARGETS roc::hipblas) +if(hipblas_VERSION VERSION_LESS "1.0") + find_package(rocblas CONFIG REQUIRED) + list(APPEND STRATA_HIP_BLAS_TARGETS roc::rocblas) +endif() find_package(hipblaslt CONFIG QUIET) if(NOT TARGET hip::host) @@ -67,6 +74,10 @@ add_library(strata_hip_runtime INTERFACE) 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}\"") +option(STRATA_GFX1012_PORTABLE_DOT "Use the portable signed-byte dot control on gfx1012" OFF) +if(STRATA_GFX1012_PORTABLE_DOT) + target_compile_definitions(strata_hip_runtime INTERFACE STRATA_GFX1012_PORTABLE_DOT=1) +endif() target_link_libraries(strata_hip_runtime INTERFACE hip::host) # 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 diff --git a/data/draft_vocab_fr.bin b/data/draft_vocab_fr.bin new file mode 100644 index 0000000000..21fff5d0c9 Binary files /dev/null and b/data/draft_vocab_fr.bin differ diff --git a/docs/AI_SETUP.md b/docs/AI_SETUP.md index 48fd142923..3eb9dfcf15 100644 --- a/docs/AI_SETUP.md +++ b/docs/AI_SETUP.md @@ -14,7 +14,7 @@ Work through the steps in order. Tell the user what you are doing in plain words 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 +- The model download is ~70 GB (94 GB for Unsloth's UD-IQ4_XS, 111 GB for UD-Q4_K_XL). 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 @@ -44,11 +44,12 @@ 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. + slowly). GTX 10 series and older, and integrated GPUs, are not supported; Pascal / Volta cards (P40, V100) and some + older AMD cards have experimental paths the user opts into ([OLDER_GPUS.md](OLDER_GPUS.md)). - **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. +- **RAM:** 32 GB or more (see step 3). **Disk:** ~80 GB free, ideally on an NVMe SSD. **CPU:** x86-64 with AVX2. Without AVX2 (Xeon E5 v1/v2 and older) setup still installs, as an experimental and slow build it compiles on the PC (10-20 minutes): tell the user that before starting ([INSTALL.md](INSTALL.md#older-cpus-experimental)). - **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. @@ -72,7 +73,7 @@ By the PC's RAM (ask the user whether they mainly want it for code - then the Co | 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 | +| 96 GB+ | `--family qwen --model IQ3_S` | `--family unsloth --model UD-IQ4_XS` (~4-bit, 94 GB, part of the experts read from the SSD under ~80 GB of RAM); `--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). @@ -92,7 +93,7 @@ The flags (all of them: `START-HERE.bat --help`): | --- | --- | | `--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) | +| `--model Q2_0\|IQ2_XS\|IQ3_XXS\|IQ3_S\|IQ1_M\|UD-IQ4_XS\|UD-Q4_K_XL` | the size (the Coder is IQ1_M, Unsloth UD-IQ4_XS or 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) | @@ -132,7 +133,7 @@ Notes: ## 6. Start the server Setup prints the start script it wrote (`start script: run-.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`. +lower case: `run-iq2_xs`, `run-swift-iq2_xs`, `run-coder-iq1_m`, `run-unsloth-ud-iq4_xs`. ``` Windows (PowerShell): Start-Process -FilePath ".\run-iq2_xs.bat" (opens its own window) @@ -168,10 +169,13 @@ line when it is ready; the engine log is `strata-.log` in the Strata fold - **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`. +- **Codex CLI and other Responses API apps:** `http://127.0.0.1:8080/v1/responses` (stateless; Codex's + `config.toml`: [DETAILS.md](DETAILS.md#the-responses-api-and-codex-cli)). - **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). +- Strata answers one request at a time; `"parallel": N` in the model's config (or setup `--parallel N`) decodes up to + N together ([BATCHING.md](BATCHING.md)). API details: [DETAILS.md](DETAILS.md#using-it). ## 9. When something fails diff --git a/docs/AMD_HIP.md b/docs/AMD_HIP.md index e2b63fc9bf..35b0626484 100644 --- a/docs/AMD_HIP.md +++ b/docs/AMD_HIP.md @@ -5,7 +5,8 @@ covers the build on Linux (on Windows a ready-made engine, see [Windows](#window 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)); 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 +install steps for users are in [INSTALL.md](INSTALL.md#amd-cards). gfx906 (Instinct MI50 / MI60, Radeon VII; wave64) has a separate +opt-in build, see [gfx906](#gfx906-instinct-mi50--mi60-radeon-vii-wave64-built-from-source). Other AMD architectures 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 @@ -46,7 +47,9 @@ 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:** images only through the CPU encoder (`--vision cpu`, 0.1.32), no calibration. The Monitor +- **Limits for now:** images only through the CPU encoder (`--vision cpu`, 0.1.32). Setup does not offer the tuning + (calibration) on AMD yet: its controls are being checked on HIP one at a time (#566). Since 0.1.39 a tuning run by + hand (`./setup.sh --calibrate`) is saved for the AMD card it ran on and reused when setup runs again. 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. @@ -345,8 +348,71 @@ run it; the report below is from a community machine: an RX 6900 XT 16 GB (gfx10 [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. +- **gfx1031** (RX 6700 XT, #524): setup knows it (the `gfx103X-all` wheels, unvalidated); its reporter runs it daily + on one card. +- **Not validated:** gfx1032 (the same `dp4a` path, no hardware report), setup's own build path and the + `gfx103X-all` wheels on gfx1030, images, answer-quality benchmarks. RDNA1 (gfx1012, RX 5500 XT) builds by hand: + [OLDER_GPUS.md](OLDER_GPUS.md#amd-building-gfx906-and-gfx1012). + +## gfx906 (Instinct MI50 / MI60, Radeon VII): wave64, built from source + +gfx906 is wave64 and has no WMMA and no packed byte arithmetic, so the wave32 backend above refuses it. A separate opt-in build compiles the CUDA sources as HIP through a small compat layer +(`include/strata/platform/hip_compat/`), with a CUDA warp mapped to a logical half of the 64-lane wavefront +(32-wide shuffles, a ballot of its own half). The hot kernels have wave64 layouts of their own (below). Setup does +not build it yet: build by hand, and run `serve/server.py` with a config as on any other card. + +**ROCm.** AMD's current ROCm releases no longer ship gfx906 libraries. The build and the measurements below used +HIP 7.14 from the community image [`mixa3607/rocm-gfx906:7.14-complete`](https://hub.docker.com/r/mixa3607/rocm-gfx906) +(rocBLAS/hipBLAS with gfx906 kernels), on the kernel's amdgpu driver (Ubuntu 24.04, kernel 6.8). hipBLASLt is not +used. + +```sh +# inside the image, with this checkout at /src and llama.cpp at the pinned commit in third_party/llama.cpp +apt-get update && apt-get install -y cmake ninja-build git python3 build-essential +cmake -S . -B build-906 -G Ninja -DSTRATA_HIP_GFX906=ON -DCMAKE_HIP_ARCHITECTURES=gfx906 \ + -DSTRATA_GGML_DIR=/src/third_party/llama.cpp \ + -DCMAKE_C_COMPILER=/opt/rocm/llvm/bin/clang -DCMAKE_CXX_COMPILER=/opt/rocm/llvm/bin/clang++ +ninja -C build-906 strata +``` + +`STRATA_HIP_GFX906` turns on the CUDA targets and refuses `STRATA_ENABLE_HIP` (one HIP build at a time). Run the +container with `--device=/dev/kfd --device=/dev/dri --group-add video --ipc=host --ulimit memlock=-1`; on some boards +`HSA_OVERRIDE_GFX_VERSION=9.0.6` is needed for the runtime to accept the card. + +**What differs from the CUDA build** (all under `STRATA_HIP_GFX906`; each A/B switch restores the CUDA layout): + +| | gfx906 | switch | +|---|---|---| +| grouped native experts | mode 7: signs as `dp4a(g ^ m, u) - dp4a(m, u)` (no byte SIMD), grids and activations in LDS, one weight load per group; the IQ formats only (Unsloth's K-quant / Q5_1 / Q8_0 experts take the CUDA layout) | `STRATA_EXP_MODE=2` (previous AMD layout) | +| MMVQ | one wavefront per row, 64-lane butterfly, no LDS | `STRATA_MMVQ_WAVE=0` | +| router top-10 | one wavefront, register argmax | - | +| verify-window routing (256-expert router) | the window's tokens in one multi-column BF16 projection + one top-k, ~3 ms of a ~55 ms window | `STRATA_ROUTE_PER_TOKEN=1` | +| hyper-connection read | norm loads in flight, 8 lanes per row for `up` | `STRATA_GR_FAST=0` | +| `gr_down_multi`, opt-in | split along K (8 rows x 5 slices of 2048), ~3% per window; sums in another order than the single-token read, so off by default | `STRATA_GR_SPLIT=1` | +| IQ4 table lookup | llama.cpp's `v_perm_b32` sequence (HIP's `__byte_perm` is a scratch array) | - | +| QSA prompt attention | the FP32 kernel (no tensor cores / WMMA) | - | + +Every switch above was checked bitwise against the layout it replaces: the parity tests on synthetic data, +`native_expert_bench` on real GGUF rows (one mode against another), and the greedy text of a fixed request. + +**Measured** on 2x Instinct MI50 16 GB (85 W power limit each, PCIe 3.0 x16 both, peer access between them), +Xeon E5-2666 v3 (10 cores, AVX2), 32 GB DDR4, SATA SSD; Coder IQ1_M (`--native` pack), 131,072-token context, +`--kv int8 --kv-resident 32768`, `--layer-split 27`, `--prefill 4096`, MTP `--spec 4 --spec-min-p 0.5`, +`--pcie-frac 0`, `STRATA_ARENA_MMAP=1`. Measured on the port this build was cut from (builds on the 0.1.30 and 0.1.33 bases, the same +kernels, plus the layer-split weight trim and the mapped arena, which are separate pull requests): + +- all 12,288 experts held in VRAM across the two cards; +- a 17,043-token agent prompt (Claude Code's first request, tools included) read in 43.5 s (392 tok/s), decode + on it 57.7-59.3 tok/s; +- 25 GB of the 32 GB RAM available while serving; +- six repeats of that request with a 6,000-token output cap and a 900-second two-client load test run six times + (407 requests, 202,940 tokens): no stalls, no errors, no GPU faults. + +For comparison on the same machine: llama.cpp (3cf0325, ROCm) on the same Coder IQ1_M measured 26.9 tok/s `tg128`. + +**Not done:** setup.py detection and an automatic build, Windows, images (`--vision`), MI60 and Radeon VII (the +same gfx906 ISA; not run), a single-card run, the tensor-split experiment (the halves of every layer on two cards; +it works but is not part of this build). ## Tuning table diff --git a/docs/BATCHING.md b/docs/BATCHING.md new file mode 100644 index 0000000000..7672dcc586 --- /dev/null +++ b/docs/BATCHING.md @@ -0,0 +1,215 @@ +# Several requests at once (batch slots) + +By default Strata serves **one request at a time**: the others wait in the server's queue. With `"parallel": N` +(the engine's `--batch N`, also spelled `--slots N`) the engine keeps up to N conversations open and decodes them +**together**: every verify window then carries one token of each conversation, so the dense weights, the shared +expert, the head and every routed expert two conversations share are read once per window for all of them. +Combined with a layer split across several GPUs and `--batch-groups`, the cards also work on different +conversations at the same time instead of waiting for each other. + +It is opt-in and changes nothing when the options are absent (#465; the engine part is PR #559). + +## Turning it on + +One GPU: add `"parallel": 2` to the model's config (`strata-.json`) and restart, or run setup with +`--parallel 2`. Setup recommends it only where it does not cost speed (below); any number you ask for is kept as +asked, with a note when it is more than setup would recommend. + +``` +"parallel": 2 +``` + +With a layer split, the engine options go into the config's `args`: + +``` +"args": [ ..., "--batch", "8", "--batch-groups", "4", "--trim-stage-weights" ], +"layer_split": "12,24,36" +``` + +| Option | What it does | +| --- | --- | +| `"parallel": N` / `--batch N` / `--slots N` (2..8) | up to N conversations decoded together; more requests wait for a free slot. Each slot gets its own state (a session carved like the stage's own: GDN recurrence, QSA K/V and indexer, PLE history) on every GPU of the split. | +| `--batch-groups G` | with a layer split: the N slots in G groups that flow through the GPUs as a pipeline (GPU k runs one group while GPU k+1 runs another). G must divide N. 1 = all slots in one window, GPU after GPU. | +| `--trim-stage-weights` | with an **explicit** `--layer-split` (e.g. `12,24,36`, not `auto`): every GPU loads only the dense weights of its own layers instead of the whole model's (the same as `STRATA_STAGE_TRIM=1`, PR #639). The VRAM this frees goes to the expert cache. Useful without `--batch` too. | + +The engine never refuses a count it cannot run: it says so in its log and runs what it can - at most 8 slots (a +window holds 8 rows), as many as fit in VRAM, or none (one request at a time) when not two fit. The server reads +the count the engine reports (`INFO batch_slots=N`), and `GET /v1/status` says it (`concurrency.serving`). + +### What a slot costs, and what setup recommends + +Every slot's session takes VRAM that the expert cache would otherwise hold: 0.56 GiB at a 32K context with 8-bit +KV, more with a longer context unless the KV cache streams (`--kv-resident`: then only the attention's 32K window +stays in VRAM, and each slot's whole KV cache takes pinned RAM - 1.6 GB at 128K). On a card whose experts mostly run +on the CPU, a batch also reads about as many distinct experts as the requests one by one (different conversations +route to different experts), so the gain is in **latency** (nobody waits for a whole answer), and a request alone +runs slower (the smaller expert cache): 11-24 % on a 12 GB card, see the measurements below. + +So setup recommends `"parallel"` **only where the experts mostly fit in VRAM**: the expert cache (each card's VRAM +less ~5 GB, every card of a layer split counted) must still hold at least half of the model's experts beside the +slots, and the slots may take at most a fifth of it, up to 4 slots. With Q2_0 at 32K that is 3 slots on a 24 GB +card, 4 from 32 GB or on a split such as 2 x 16 GB; IQ3_S needs 32 GB or a split. Everywhere else (any 12 or 16 GB +card alone) it stays at one at a time and setup says: "parallel N reduces waiting for several users but costs +about 10-25% speed per request on this card". `--parallel N` is honoured as asked either way. + +## How the server uses the slots + +- **One request alone** runs on the usual solo path (verify windows with MTP drafts): the fastest single stream. +- **When a second request arrives**, the first is stopped (`STOP`) and continues in a batch slot with its prompt + plus what it generated so far - the engine's prompt cache holds exactly that, so nothing is read again - and the + new request is admitted next to it. A request in a slot decodes **without MTP drafts** (one token per window). +- **A request left alone in a slot** (the others finished, nobody waits) goes back to the solo path: the slot is + stopped, the engine copies its sessions back and decodes with MTP drafts again (at most twice per request; with + `--prompt-cache 0` it stays in the slot; `STRATA_PARALLEL_SOLO=0` turns it off). The draft layer's own K/V was + built for another conversation then, but measured it accepted as many drafts (140 of 172) as a draft layer that + read the conversation (140 of 173). +- **More requests than slots** wait for a free one (`/metrics` -> `live.slots` shows each slot: idle, reading or + decoding, its tokens and tok/s; `live.running` the requests in flight). +- **Each admission** reads the request's prompt through the usual prompt path (prompt cache and conversation + checkpoints included) and produces its first token there; the state is then copied into the slot. Admissions + are taken one at a time, and **the slots decode between the prompt's chunks**: after each chunk (`--prefill`, + 2048-8192 tokens) they decode for half as long as the chunk took (`STRATA_BATCH_DECODE_SHARE`, default 0.5), so + a long prompt slows the others down instead of stopping them. The chunks are the ones one uninterrupted read + takes, so the prompt's arithmetic is unchanged. +- **A long prompt gives way to a short one** (#656's cooperative preemption): when a request with a prompt under + half as long is waiting, the server sends `BYIELD`; at its next chunk boundary the long read stops, the part read + so far is copied into a slot, the short request is admitted, and the long one then goes on from its slot with the + same chunks (at most twice per request). +- **Each slot is a conversation cache.** A finished slot keeps what it holds (the prompt, the answer, and the + checkpoint at the prompt's last turn boundary); the next turn of that conversation goes to that slot and the + engine copies its state back (50-60 ms for a short conversation) instead of reading the history again - also for + a client that drops the reply's thinking from the history (the checkpoint matches up to the new turn). A new + conversation takes an empty slot, else the one used longest ago. +- Slots are assigned so that consecutive requests land in different pipeline groups (`--batch-groups`). +- A client that disconnects stops its slot (`BSTOP`); the others go on. + +## Exactness + +A batch row's arithmetic is the single-token window's, so with greedy decoding **every conversation of a batch +produces exactly the tokens it produces alone** - verified token by token for 8 concurrent conversations of 150 +tokens, with and without the pipeline (`tools/batch_test.py`), and on one RTX 5070 for 4 conversations, for a long +prompt read while two others decode, for a prompt that gave way and went on, for a next turn continued from its +slot (from all it holds, and from its turn checkpoint without the reply's thinking), and for a request stopped in +its slot and continued on the solo path (`tools/batch_interleave_test.py`). These +settings make the comparison exact: + +- `STRATA_IQ_MT_MIN=1` (the multi-token CPU expert kernels for every group, as for the solo path's own + exactness tests: by default an expert's rows round differently alone than in a group, so the output depends on + how many rows of a window share an expert - which differs between a batch and a request alone), +- `--pcie-frac 0`: the PCIe share of the missed experts is chosen per window from the window's misses, so the + same expert can run on the GPU in one window and on the CPU in another, which rounds differently, and +- `--adapt-every 1000000` (the VRAM tier fixed), and `--no-prefill-borrow` while a prompt is read beside decoding + slots: their windows then see the expert cache without the slots the prompt borrowed (those experts run on the + CPU). + +With the default settings the outputs stay coherent but drift apart after some tokens, as two solo runs whose +windows differ can. Measured on the RTX 5070 (Q2_0, 4 conversations of 200 tokens, `tools/batch_test.py` without +the settings above): one equal to its solo run, the others apart from token 0, 99 and 174 - the first token already +differed for one, because the adaptive VRAM tier had moved experts between the solo runs and the batch; all four +texts read as well as their solo runs. + +Sampled requests (temperature, top_p, top_k, min_p, seed) are drawn row by row with the solo window's +counter-based draw (Philox(seed, position)). + +## Limits (for now) + +- Batch windows carry no MTP drafts: a conversation in a slot decodes one token per window (the solo path keeps + its drafts, which is why a request alone is not put in a slot, and goes back to it when left alone). +- Repetition / frequency / presence penalties are not applied in batch windows. +- A prompt shorter than one chunk is read in one piece (the slots wait for it); a read gives way only at a chunk + boundary, and not for pictures. +- Admissions are one at a time: two new long prompts are read one after the other. +- `--batch-groups` needs every stage on its own GPU; a pipelined slot is not kept as a conversation cache. +- The slot sessions take VRAM (above) and, with KV streaming, pinned RAM. + +## Measured + +One RTX 5070 (12 GB), Ryzen 5 7600, 64 GB DDR5, Q2_0, 32K context, through the HTTP server: C different requests +sent at once (an 800-word essay each, 256 tokens per answer, greedy, thinking off), median of 3 rounds: + +| Concurrent | Setting | Total tok/s | vs one at a time | Per request tok/s | First token: median / last of the round | +| ---: | --- | ---: | ---: | ---: | ---: | +| 1 | one at a time (default; what setup recommends on this card) | 74.3 | - | 83.6 | 0.4 s / 0.4 s | +| 1 | `"parallel": 2` | 67.3 | -9 % | 74.8 | 0.4 s / 0.4 s | +| 1 | `"parallel": 4` | 57.9 | -22 % | 63.8 | 0.4 s / 0.4 s | +| 2 | one at a time | 71.6 | - | 77.3 | 2.1 s / 4.1 s | +| 2 | `"parallel": 2` | 61.0 | -15 % | 32.6 | 0.5 s / 0.9 s | +| 2 | `"parallel": 4` | 54.6 | -24 % | 28.7 | 0.6 s / 0.7 s | +| 4 | one at a time | 70.7 | - | 79.6 | 6.0 s / 11.2 s | +| 4 | `"parallel": 2` | 61.2 | -13 % | 32.2 | 4.5 s / 9.3 s | +| 4 | `"parallel": 4` | 63.1 | -11 % | 16.9 | 1.0 s / 1.8 s | + +On this card the slots buy **waiting time, not speed**: the fourth of four requests starts after 1.8 s instead of +11.2 s, but together they decode 11-24 % slower than one after the other, and a request alone loses 11 % (2 slots) +or 24 % (4 slots), because the slots' sessions (0.56 GiB each) come out of the expert cache and most experts run +on the CPU: a batch window over 4 conversations reads 24 CPU experts per layer against ~8 for one, so it costs about +what the 4 tokens cost one after the other (`strata batch:` in the engine log: 54 ms per 4-row window, ~20 ms per +1-row window). This is why setup leaves a 12 GB card at one at a time. Cards that hold most experts in VRAM, and a +layer split, are where the slots also add speed (below). + +A 4-GPU layer split (4 x 16 GB, PCIe Gen3), IQ3_S, `--batch 8 --batch-groups 4 --trim-stage-weights`, through +the HTTP server, 400 tokens per answer, temperature 0.7 (PR #559): + +| Concurrent requests | Per request | Total | +| ---: | ---: | ---: | +| 1 | 123 tok/s (solo path) | 120 tok/s | +| 2 | 57 tok/s | 113 tok/s | +| 4 | 51 tok/s | 205 tok/s | +| 8 | 45 tok/s | 360 tok/s | + +With the patches below on engine 0.1.38 and parking on, through the service: 8 requests at temperature 0 -> 369 +tok/s, at 0.7 -> 358 tok/s. + +`--trim-stage-weights` alone raised the share of experts held in VRAM on that machine from 76-85 % to 84-100 % +per card. + +## Together with conversation parking + +`--conversation-cache-mib N --conversation-cache-slots K` (DETAILS.md) works with the layer split too: a request +whose conversation was parked is restored on every stage before its admission, so an agent and its sub-agents, or +several chats that alternate, come back without reading their history again. Measured on the same 4-GPU split, +two long conversations alternating through the HTTP server: the first turns took 3.9 s and 5.7 s to the first token +(their prompts read), the follow-ups 0.53 s and 0.46 s. + +## Testing + +Four scripts drive a built engine or a running server; each exits non-zero on a failure. `serve/test_parallel.py` +tests the server's side with a scripted engine (no GPU). + +| Script | What it checks | +| --- | --- | +| `tools/batch_test.py` | the same prompts alone (`GEN`) and together in the batch slots (`BGEN`): every slot's greedy tokens equal its solo tokens; prints the aggregate rate. `--batch-groups` in `--extra` tests the pipeline, `--keys "temperature=0.7"` the sampled rows. | +| `tools/batch_interleave_test.py` | a long prompt read while two slots decode, a prompt that gives way (`BYIELD`) and goes on, and a next turn continued from its slot: each equal to its solo tokens. | +| `tools/parking_test.py` | a follow-up to a conversation decodes the same tokens whether its state stayed live or came back from the parking cache (with a layer split: every stage's image). | +| `tools/early_close_test.py` | a client that stops reading a streamed answer early (alone, and with a second request running) does not leave its tokens to the next request (server). | + +For exact comparisons pass `--pcie-frac 0 --adapt-every 1000000` (and the scripts set `STRATA_IQ_MT_MIN=1`): + +``` +python3 tools/batch_test.py --exe engine/strata --config strata-.json --batch 8 --n 8 \ + --extra "--layer-split 12,24,36 --trim-stage-weights --batch-groups 4 --pcie-frac 0 --adapt-every 1000000" +python3 tools/batch_interleave_test.py --exe engine/strata --config strata-.json \ + --extra "--pcie-frac 0 --adapt-every 1000000 --no-prefill-borrow" +python3 tools/parking_test.py --exe engine/strata --config strata-.json \ + --extra "--layer-split 12,24,36 --conversation-cache-mib 8192 --conversation-cache-slots 4 --pcie-frac 0" +STRATA_KEY= python3 tools/early_close_test.py http://127.0.0.1:8080 +``` + +## Engine protocol (`--serve`) + +On top of `GEN` / `GENI`: + +| Line | Direction | Meaning | +| --- | --- | --- | +| `BGEN [keys] ` | in | read the prompt (as `GEN 1`), then continue in `` | +| `BGENI [keys] ` | in | the same with images | +| `BADM <1/0>` | out | after the admission's `DONE`: 1 = it continues in the slot, 0 = it ended | +| `BT ` | out | a token of that slot | +| `BDONE ` | out | the slot is free again (it keeps its conversation) | +| `BSTOP ` | in | end that slot at its next window | +| `BYIELD ` | in | the prompt being read gives way at its next chunk boundary; its part read waits in `` (the admission's own, or a free slot for a solo request) | +| `YIELDED ` | out | before the `DONE cancel` of a read that gave way: the request is sent again later and goes on from there | +| `INFO ... batch_slots=N` | out | the slots the engine runs (only with `--batch`) | + +`tools/batch_test.py` drives the engine directly: the same prompts alone, then together, compared token by token, +and the aggregate rate. diff --git a/docs/COMMUNITY_BENCHMARKS.md b/docs/COMMUNITY_BENCHMARKS.md index ff3c312c55..e6903faf40 100644 --- a/docs/COMMUNITY_BENCHMARKS.md +++ b/docs/COMMUNITY_BENCHMARKS.md @@ -15,6 +15,9 @@ and their limits. Report what you actually measured and label estimates separate - [2026-09-30: RTX 5090, Core Ultra 9 285K, 64 GB RAM](../bench/results/2026-09-30-community-rtx-5090/README.md): Strata 0.1.29, original Flash-Next IQ2_XS, 131,072-token context; three runs each at 4,096, 32,768, and 128,000 prompt tokens, plus six recall checks. +- [2026-10-03: 2x AMD Instinct MI50 16 GB (gfx906), Xeon E5-2666 v3, 32 GB RAM](../bench/results/2026-10-03-community-2x-mi50/README.md): + the gfx906 build (#638) with #639 and #640, Coder IQ1_M, 131,072-token context, layer split across both cards; + three runs each at 4,096, 32,768, and 128,000 prompt tokens, plus six recall checks. ## What to record diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index e6f33e4025..d0e7e71fe4 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -79,7 +79,9 @@ The current canonical multi-GPU documents are: Concrete reference-host hardware, tuning values, benchmark numbers, and production validation records intentionally live in the separate public recipe repository: [`rhgo1749/qwen3.8-flash-next-strata-gpu-per-lane-recipe`](https://github.com/rhgo1749/qwen3.8-flash-next-strata-gpu-per-lane-recipe). -The production baseline remains independent GPU lanes with a shared host expert arena. The current promoted engine generation is **Strata 0.1.38**, integrated from upstream `99f3dbd0b21d1401b3769e0c0d963913607f380b`. The 0.1.38 sync preserves the Phase 1/2 serving-control contract and the Phase 3 leader/follower shared-arena lifecycle while absorbing upstream 0.1.35-0.1.38 prompt/decode, reliability, telemetry/security, expert-profile persistence, unbuffered loading, and optional peer-tier work. Ordinary lanes explicitly strip inherited engine-internal multi-GPU flags and shared `--expert-profile-save` writers so only a future explicit Super-Lane/profile-ownership contract can re-enable them. The bounded compatibility gate passed the full Python serving suite (256 tests, 7 skipped), a CUDA 13.4 sm_120 Release `strata` build, focused shared-arena/profile source CTests, and a live three-lane shared-arena generation smoke. A matched single-lane compatibility A/B against the still-installed 0.1.31 production binary showed higher long-prompt prefill on 0.1.38 at roughly +9.5% for ~15K tokens and +7.3% for ~30K tokens; this is a bounded sync check, not a replacement for the retained full 0.1.30/0.1.31 campaigns. Upstream layer-split/peer execution remains complementary to the fork's independent request/session lanes. Hardware-specific measurements and caveats live in the public recipe repository. +The production baseline remains independent GPU lanes with a shared host expert arena. The current promoted engine generation is **Strata 0.1.38**, integrated from upstream `99f3dbd0b21d1401b3769e0c0d963913607f380b`. The next sync candidate is **Strata 0.1.39**, merged on `sync/upstream-0.1.39` at `3727c6bdfb12ba2ffc07402b68ae86c0ed2ac27f` from upstream `6f32ec070f23ced9f50e704d854d775da52591ab`; it is validated but not promoted until main moves. The candidate preserves the Phase 1/2 serving-control contract and the Phase 3 leader/follower shared-arena lifecycle while consuming upstream 0.1.39 batching, Responses API, conversation-cache telemetry, decode/kernel, CPU-pool, expert-cache and low-RAM/file-tier work. Ordinary lanes explicitly strip inherited engine-internal multi-GPU flags plus `parallel` / `--batch` / `--slots` / `--batch-groups` / `--trim-stage-weights`, and shared `--expert-profile-save` writers, so only future explicit contracts can re-enable nested execution. The 0.1.39 bounded gate passed the full Python serving suite (**333 tests, 7 skipped**), fresh CUDA 13.4 sm_120 Release builds with tests both off and on, focused `pinned_shared_test`, `expert_profile_save_test`, and `file_expert_source_test` CTests, a live single-lane model smoke, and a live three-lane shared-arena concurrent-generation smoke; follower lanes logged that the shared population was ready and skipped the repeated source expert load. The earlier 0.1.38 matched long-prompt compatibility A/B against the installed 0.1.31 binary remains historical evidence at roughly +9.5% for ~15K tokens and +7.3% for ~30K tokens; it is not relabeled as 0.1.39 performance evidence. Upstream layer-split/peer/batch execution remains complementary to the fork's independent request/session lanes. Hardware-specific measurements and caveats live in the public recipe repository. + +The 2026-10-05 reference-host topology recheck changes one challenger conclusion without changing the promoted topology. The fixed upstream-native three-GPU layer-split pipeline (`--layer-split 18,34 --batch 3 --batch-groups 3 --trim-stage-weights`, shared expert arena) measured **147.84 ± 2.26 tok/s** at M=2 versus **143.19 ± 3.06** for two independent lanes (**+3.25%**) and **209.66 ± 4.02 tok/s** at M=3 versus **192.16 ± 4.11** for three lanes (**+9.11%**). The retained same-binary single-request layer-split control is **120.62 ± 1.24 tok/s** versus **73.63 ± 1.67** for one lane, but M=1 was not rerun under the exact batch3/groups3/trim config. Cold-prefill still crosses by workload length: ~15K x3 strongly favors lanes (**5901.34 vs 3289.19 tok/s**), while ~110K x3 narrowly favors layer split (**6028.09 vs 5822.71 tok/s**). Every retained PP request reported `cache_n=0`; the nonce text is workload/length matched rather than byte-identical across topology arms. The ordinary FIFO path is now treated only as a serial control. This evidence reopens static layer split / Super-Lane as a serious topology candidate but does not by itself change the production default. See `docs/strata-0.1.39-performance-crossover-20261005.md`. ## Template for a decision-bearing Issue diff --git a/docs/DETAILS.md b/docs/DETAILS.md index 9d2cc7041a..d9484e6528 100644 --- a/docs/DETAILS.md +++ b/docs/DETAILS.md @@ -107,6 +107,11 @@ English/code subset from before (40,525 ids, ~110 MiB less VRAM, English answers 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. +`--draft-vocab fr` (0.1.39, #597) takes the English/code subset plus the 5,686 tokens that cover 99% of a French +Wikipedia corpus (46,211 ids, `tools/draft_vocab.py --corpus`): 23.6% of French text's tokens were outside the +English/code subset, 0.7% are outside this one. Drafts accepted in French answers 0.51 -> 0.60 (IQ3_XXS, RTX 5070, +8 prompts x 2 passes; English 0.61 -> 0.63 and code 0.77 -> 0.78, no loss), and 141 -> 158 tok/s in French on an +RTX 5090 (IQ3_S, the reporter's measurement). `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 @@ -145,6 +150,13 @@ other ~18 GB), a 32 GB PC with a 12-16 GB GPU the Coder; IQ3_XXS on a 32 GB PC s 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. +**A mapped arena for small RAM (Linux, opt-in, 0.1.39, PR #640):** `STRATA_ARENA_MMAP=1` maps a native pack's expert +arena read-only from the pack's `experts.bin` instead of reading it into locked RAM, for a PC whose GPUs hold most +experts but whose RAM is small (2x 16 GB GPUs with 32 GB of RAM: ~1 GB -> 25 GB available while serving). The first +start writes `experts.bin` (when the drive has room for it), later starts map it; the pages of the experts a GPU holds +are handed back to the OS. Run it with `--pcie-frac 0` (the GPUs get no mapped alias). Without the variable nothing +changes. + **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 needs the pack's `experts.bin`: when the pack has none, the engine @@ -173,6 +185,19 @@ been warmed). The server log has the same per request (`expert tiers: GPU ... hi 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). 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). +Its `hit_rate` is the VRAM share of the experts looked up while answering: experts the GPU reads over PCIe +(`--pcie-frac`) are not in it, so a higher `--pcie-frac` raises it even when decoding gets slower. `pcie_share` +(engine 0.1.39 or newer, #588) is their share of all routed experts, and the server log and the Monitor tab show it +beside the hit rate. + +**Where a decode window's time goes (profiling, #610):** start the server with `STRATA_DECODE_TIMING=1` (and +`STRATA_VERIFY_PROFILE=1` for the GPU's side) in the environment. After each request the engine log then has one +`strata decode timing:` line - windows, tokens per window, and per window the verify time split into the wait for the +GPU, the CPU expert pool (plan, activation quantization, jobs) and the stage, plus commit and draft - and one +`strata decode GPU stages (ms/window):` line with the GPU time of each stage (GDN and QSA layers, the VRAM expert +hits, the router, the head, ...). The GPU profile times every stage with events, so it slows the decode a little: +use it to compare, not to measure speed. This works with every pack; `--gpu-stages` (a one-token replay of +per-layer graphs) refuses a native (IQ) pack, which has no such graphs. 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). @@ -208,7 +233,9 @@ card gains depends on its PCIe link (the experts stream over it), so they are st 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. A 3090's 24 GB takes most of the CPU work away. (Since 0.1.14 the expert profile ranks all 24,576 experts; before, the cache stopped at 8,000, about 10-14 GB. `tools/make_profile.py` builds a profile from -your own prompts: run the engine once with `--dump-routing trace.bin`, see the tool's help.) +your own prompts: run the engine once with `--dump-routing trace.bin`, see the tool's help. The shipped profile already +ranks every pair, so a trace only changes the order with `--reorder` (0.1.39, #587): your trace's pairs first, then the +base's, then the rest.) ## Which model? @@ -276,8 +303,8 @@ 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. 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. | +| CPU | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. Older CPUs without AVX2 are experimental and slow: [Older CPUs](INSTALL.md#older-cpus-experimental). | +| 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 pack for the fast CPU kernel (34 GB of experts in its layout, plus the dense weights). An NVMe SSD is strongly recommended. | | OS | Windows 10/11, or Linux (Ubuntu 22.04/24.04 get everything installed automatically). | What the first start installs: in this folder `.venv/`, `engine/` and `third_party/`; the model files (`models/`, @@ -414,6 +441,18 @@ 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). +**Giving part of the VRAM back while it keeps serving (#533, opt-in, one NVIDIA GPU).** With `"vram_elastic": true` +in the config (the engine flag `--vram-elastic`), the expert cache is allocated in 512 MiB segments +(`"vram_segment_mib"`), and `POST /v1/vram` with `{"reserve_mib": 8000}` shrinks it between requests until that much +VRAM is free for another program; `{"reserve_mib": null}` grows it back towards its full size, keeping the reserve +the engine started with (`--vram-reserve-mib`), and `{"reserve_mib": 0}` takes all of it back. A request that is +running finishes first. The experts of the segments given back are computed on the CPU, like any expert outside the +cache, so answers keep coming, slower; growing back puts the same experts in the same slots. Nothing resizes on its +own. Not with a layer split, the helper caches, `--peer-device` or the resident low-RAM mode. Measured on an RTX 5070 +12 GB with Q2_0 (4.8 GiB cache): `{"reserve_mib": 6000}` took 78 ms and freed 4.3 GiB (the cache keeps 0.5 GiB for +the prompt path), decode 44 -> 33 tok/s; growing back took 92 ms and the answers were token for token the ones before +the shrink. Without the flag nothing changes (the same answers as without it). + **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-.json` the engine saves that as a @@ -435,6 +474,7 @@ The server listens on `http://127.0.0.1:8080` (change with `--port` in setup, or | --- | --- | | OpenAI Chat Completions (stream and non-stream, tools) | `POST /v1/chat/completions` | | Anthropic Messages (stream and non-stream, tools) | `POST /v1/messages` | +| OpenAI Responses (stream and non-stream, tools; stateless, [below](#the-responses-api-and-codex-cli)) | `POST /v1/responses` | | Model list / health | `GET /v1/models`, `GET /models`, `GET /health` | | Model properties | `GET /props` (also accepts `?model=`) | | What the model is doing right now | `GET /status`, `GET /slots` (single slot, busy or idle) | @@ -473,6 +513,18 @@ print(r.choices[0].message.content) part of the thinking the client sees and counts as output tokens. `"reasoning_budget_tokens": N` in `strata-.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. +- **A reply stuck on one token is ended (0.1.39, #606).** When a reply repeats the same token 256 times in a row, the + server ends it there with `finish_reason` `"length"` and says so in its window: a model in a loop, or a broken + state that answers one token forever (#606 saw 36,689 tokens of `!`). `"repeat_stop_tokens": N` in + `strata-.json` sets the run length; `0` turns it off (for a request that really wants one token many times). +- **Changing the effort without re-reading the prompt (opt-in, 0.1.39, #458).** The effort's instruction is the + first thing in the prompt, so a request that only changes the effort (an agent's "think harder" switch, `none` for + a quick tool step) reads the whole conversation again. `"effort_position": "end"` in `strata-.json` renders + every request's prompt start as the default effort's and puts a `low` / `medium` effort in a short system turn + right before the answer (no thinking: the empty thinking block, as always); the engine (0.1.39+, which the server + checks) keeps its checkpoint in front of that turn, so the next request reuses the conversation whatever its + effort. The default (`"start"`) prompt is unchanged. The model sees a level that is not the default in another + place than it was trained with; how well it follows it there is not measured yet. - **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-.json` renders such a request without thinking - Anthropic's own rule, and what Claude Code's short @@ -520,6 +572,7 @@ print(r.choices[0].message.content) - **Claude Code** (Strata 0.1.17 or newer): set `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` and `ANTHROPIC_MODEL` to a Claude model name it knows (it refuses names it doesn't; Strata ignores the name), plus any `ANTHROPIC_AUTH_TOKEN` (or your `api_key`, if you set one). +- **Codex CLI** (0.1.39): see [the Responses API](#the-responses-api-and-codex-cli) below. - **Context.** Chosen in setup (8K-262K). Requests longer than that are refused, never silently cut. A request whose `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-.json` (or pass @@ -527,6 +580,14 @@ print(r.choices[0].message.content) - **Model aliases** (0.1.32). `"aliases": ["qwen", "local-model"]` in `strata-.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. +- **Model settings in the web page (0.1.39, #564).** The About tab's Model settings card shows and changes a few of + the keys above in `strata-.json`: the `sampling` defaults (temperature, top_p, top_k, min_p), + `reasoning_budget_tokens`, `fit_max_tokens`, `anthropic_thinking`, `effort_position`, `aliases`, `idle_unload_s`, + `lazy_load`, `engine_silence_s`, `api_monitor`, `open_browser` and `--vram-reserve-mib`. An empty field removes the + key (its default). Every other key of the file stays as it is, the earlier file is kept as + `strata-.json.bak`, and the model uses the change from its next start. Only Strata's own page can save + (JSON, the API key when one is set, as for the Chat settings); the network, key, MCP and program keys are not + editable there. - **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-.json`). The server window then prints this PC's addresses @@ -581,13 +642,20 @@ in a bounded 8 GiB host-RAM cache. This preserves controller/worker histories wh their requests alternate; it does not execute requests concurrently. No client session ID is required: only exact token/image prefixes with matching steering mode are reused. The default budget is 0 (disabled); `--prompt-cache 0` also disables parking. -The initial shared-core integration supports a single session GPU: combining -enabled parking with `--layer-split` is rejected before model loading. Ordinary -upstream layer-split checkpoints remain available with parking disabled. FP16, +With `--layer-split`, a parked conversation holds one image per stage (each later stage's +session and its share of the conversation checkpoints), every image is validated before +anything is overwritten, and each is restored on its own GPU. Verified on a 4-GPU split: +a 9,276-token conversation restored in 51 ms, and its follow-up decodes exactly the tokens +it decodes when the conversation never left (`tools/parking_test.py`). FP16, INT8, Q4_0 and identity-layout K8V4 snapshots are supported; the K8V4 draft ring remains INT8, as in upstream. Windows/HIP and multi-GPU runtime coverage must be reported separately from Linux/CUDA evidence. +The web page's Monitor tab has a **Conversation cache** card (0.1.39, #596): the parked conversations against the +slots and the RAM budget, how many were parked, restored and evicted, and the last switch (read from the engine's +log), and for every setup how many prompt tokens the cache gave back - in the last request and since the start. +`/metrics` has the same under `"conversation_cache"`. + Snapshots contain running state, checkpoints, used K/V pages, and draft-layer K/V. They add host RAM, not another model or VRAM allocation. The byte budget also counts an incoming snapshot during a switch. After a restore, unchanged K/V pages can be @@ -615,8 +683,9 @@ sizes and K/V bytes reused during capture. `STRATA_SNAPSHOT_FULL_CAPTURE=1` disa retention for diagnostic comparisons. Snapshots are not persisted across restarts. -**Current limits (v1):** one request at a time, and one conversation cached at a time (switching between two chats -re-reads the other one unless the opt-in cache above is enabled); images only when set up with them (below); no video. **Temperature / top_p / top_k / min_p / +**Current limits (v1):** one request at a time unless `"parallel": N` is set (opt-in batch slots, up to N requests +decoded together: [BATCHING.md](BATCHING.md)), and one conversation cached at a time (switching between two chats +re-reads the other one unless the opt-in cache above is enabled, or each conversation keeps its own batch slot); images only when set up with them (below); no video. **Temperature / top_p / top_k / min_p / seed** are honored per request (OpenAI and Anthropic fields); with the default adaptive expert tier a sampled result is not reproducible run to run - for seed-reproducible output add `--adapt-every 100000` (static residency) to the engine arguments. The run config's optional `sampling` block sets the defaults for requests that leave the fields out @@ -631,6 +700,65 @@ without penalties, so more of its guesses are now rejected. Requests without pen --- +### The Responses API and Codex CLI + +`POST /v1/responses` (0.1.39, #451) speaks OpenAI's newer Responses API, which Codex CLI uses (it no longer +speaks Chat Completions). It runs on the same path as `/v1/chat/completions`, so the thinking levels, the thinking +budget, the conversation cache and the same API key, Host and Origin checks apply. + +It is **stateless**: nothing is stored, so the client sends the whole conversation in `input` every time (Codex does, +with `store: false`). `previous_response_id`, `conversation`, `background` and the retrieve/delete/cancel endpoints +are refused with an error that says so. + +| Request | What Strata does | +| --- | --- | +| `input` as a string, or as items | `message` items (`user`, `assistant`, `system`, `developer`; text and images), `reasoning`, `function_call`, `function_call_output`, `custom_tool_call(_output)` | +| `instructions` | The system message (with leading `developer` messages; later ones become user messages, as on the chat path) | +| `tools` | `function` tools, `namespace` tools (the model sees `namespace.name`; calls come back with `namespace` and `name`), `custom` tools (one free-form `input` string). Hosted tools (`web_search`, `file_search`, ...) are left out: the model cannot run them | +| `tool_choice` | `"none"` hides the tools; anything else lets the model choose (it cannot be forced) | +| `reasoning.effort` | `none`/`minimal`, `low`, `medium`, `high`/`xhigh`; without it the model's default (high) | +| `max_output_tokens` | The output cap (thinking included). Running out ends the response `incomplete` (`max_output_tokens`) | +| `text.format` | `json_schema` and `json_object` use the [JSON response formats](#json-response-formats) (checked, not constrained) | +| `temperature`, `top_p`, `reasoning_budget_tokens`, ... | As on the chat path | + +The model's thinking comes back as a `reasoning` output item with `reasoning_text` content (streamed as +`response.reasoning_text.delta`). This model writes no separate summaries, so `summary` is empty. With +`"include": ["reasoning.encrypted_content"]` the item also carries `encrypted_content`: an opaque string (base64, not +encrypted; the client already holds the text). Send the reasoning items back with the rest of the conversation, as +Codex does: their thinking goes back into the prompt, so it matches what the model wrote and the conversation cache +is reused. + +With `"stream": true` the events are the official ones, in order: `response.created`, `response.in_progress`, then +for each output item `response.output_item.added`, its deltas (`response.reasoning_text.delta`, +`response.output_text.delta`, `response.function_call_arguments.delta`), its `...done` events and +`response.output_item.done`, and at the end `response.completed`, `response.incomplete` or `response.failed`. +While a long prompt is read, a `response.in_progress` event goes out every 15 s, which Codex counts as activity. +Errors before the answer starts have the Responses form (`{"error": {"message", "type", "param", "code"}}`); after +it started they arrive as `response.failed`. + +**Codex CLI.** In `~/.codex/config.toml` (Windows: `%USERPROFILE%\.codex\config.toml`): + +```toml +model = "strata" # any name; Strata answers with its model +model_provider = "strata" +model_context_window = 32768 # the context you chose in setup: Codex compacts before it gets there +show_raw_agent_reasoning = true # show the model's thinking (it writes no summaries) +# model_reasoning_effort = "medium" # none, low, medium or high; default: the model's (high) + +[model_providers.strata] +name = "Strata (local)" +base_url = "http://127.0.0.1:8080/v1" +wire_api = "responses" +stream_idle_timeout_ms = 600000 # a first, long prompt can take minutes to read +# env_key = "STRATA_API_KEY" # only if the server has an api_key: the variable holding it +``` + +Then run `codex` (or `codex exec "..."`) as usual. Codex warns `Model metadata for ... not found` for a local model +name; that is expected. Measured with Codex CLI 0.160.0 and Q2_0 on an RTX 5070: its first prompt (instructions and +tool descriptions) was 9,443 tokens, read in 10 s; in a tool loop, each later turn reused about 96% of the prompt from +the cache and read only the new part in 1-2 s. On Windows, Codex's sandbox rejected every shell command in that test +until it was started with `-c 'windows.sandbox="unelevated"'` (a Codex setting, not Strata's). + ## Tools from MCP servers The chat page can give the model tools from [MCP](https://modelcontextprotocol.io) servers, as LM Studio and Claude @@ -748,6 +876,17 @@ 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. +**More image tokens (0.1.39, #625):** `--vision-tokens N` at setup (`START-HERE.bat --setup --vision cpu +--vision-tokens 768`) sets the most tokens a picture becomes - `"max_tokens"` in the `"vision"` section of +`strata-.json`, which you can also edit by hand. More tokens keep more detail (small text, charts, screenshots) +and take longer to encode, on the CPU most of all; a setup run again keeps the value. + +**A Q8_0 encoder (#625):** `"mmproj"` in the `"vision"` section can point to another mmproj file of this model, for +example a Q8_0 one (llama.cpp's `convert_hf_to_gguf.py --mmproj --outtype q8_0` makes one): the encoder's library +reads quantized weights, the file is half the size, and on the CPU it can encode faster than BF16. Setup downloads +the BF16 file, and a setup run again keeps a file of your own that still exists. We have not measured its accuracy +against BF16 yet; numbers are welcome in #625. + **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-.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 @@ -872,6 +1011,7 @@ the document, +0.4% on the chat. Details: `bench/results/2026-09-27-esp/`. | Pictures are slow (10-30 s) | The encoder runs on the CPU: run setup again with `--vision gpu` (needs ~1.4 GB of VRAM). | | A request never finishes: "reading the prompt", GPU "100%" at low power | The GPU ran out of VRAM (engines before 0.1.9 could end with ~30 MiB free at large contexts). Run `START-HERE.bat` once to get engine 0.1.9 or newer; the log then says `... MiB of VRAM free with everything loaded` (a few hundred) and names the `--vram-reserve-mib` to add if it is low. | | Generation stops mid-answer, GPU "100%", one CPU core busy | Fixed in engine 0.1.12 (issue #29, a race in the CPU expert pool on big-VRAM cards). Since then a request that stops moving ends with an error instead of hanging (after 2 minutes; 1 minute from 0.1.13): the log says `no progress for ... s ... (issue #29)` with where it stopped, and the next request starts the engine again. If you see that line, please open an issue with it. Engine 0.1.13 adds a stall report under it (what every expert-pool thread and the GPU handshake were doing, memory and page faults) and, on Windows, a `strata-stall-.dmp` file with every thread's stack: attach both. (`STRATA_WATCHDOG_S` sets the time in seconds; 0 turns it off.) Engine 0.1.14 fixes the stall those reports found (issue #31: with the IQ packs the host could wait forever inside the NVIDIA driver while copying experts in a verify window; the experts are now copied by a GPU kernel, `--pcie-mode dma` restores the old way). | +| `no progress for 60 s ... reading the prompt` on Linux, and the stall report says `threads waiting on the disk (state D): 16 ...` | The engine waits for the drive, not a deadlock: the n-gram table is read at random (`--ple-io direct`), which a rotational disk cannot keep up with (#605). The engine warns at start when the table is on one; `--ple-io ram` (Linux, needs RAM for the table) or the model on an SSD fixes it. Setup adds `--ple-io ram` itself on a rotational disk when the RAM holds the table (0.1.39). | | `the engine said nothing for ... s during the request` or `... did not finish the request after it was stopped (STOP)` | Issue #481: the engine and the server lost step (the engine waits for its next command, the server for the request's end; GPU at 0 %, nothing in the log). The server ends the engine after 300 s without a line from it during a request (while a prompt is read: each chunk may take three times the previous one's time, the first one up to its tokens at 50 tok/s more), the request ends with an error and the next request starts the engine again. `"engine_silence_s": 600` in `strata-.json` sets the time (0 = wait forever, as before). If you see it, please add the end of the engine log to #481. | | `out of memory: cudaFuncSetAttribute` in the log (IQ3_XXS, long prompt) | Fixed in engine 0.1.15: CUDA loaded a kernel's code when it was first needed, and mid-prompt there was no VRAM left for it. Run `START-HERE.bat` (Windows) or `./setup.sh` (Linux) once to update. | | Anything else | The engine log is `strata-.log` in this folder. | @@ -887,12 +1027,26 @@ the document, +0.4% on the chat. Details: `bench/results/2026-09-27-esp/`. the conversation while you chat). - **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). -- **SSD:** the 28.8 GB n-gram table, read a few rows per token through the OS cache. +- **SSD:** the 28.8 GB n-gram table, a few rows per token, read unbuffered past the OS cache (`--ple-io direct`, the + default, made for SSDs; on a rotational disk `--ple-io ram` keeps the table in RAM, #605). - **Speculation:** 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** (engine 0.1.7) 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. -- **Prompts** are processed in 2,048-token chunks with the experts streamed to the GPU over PCIe. +- **Prompts** are processed in chunks of up to 8,192 tokens (`--prefill auto`; 32,768 opt-in) with the experts + streamed to the GPU over PCIe. Since engine 0.1.39b (#583) the streamed ring is sized in bytes for the pack (a + native pack with bigger blobs gets fewer ring slots and keeps more cache slots) and `--prefill auto` picks the + largest chunk that keeps that ring full. It only gives up ring slots where 0.1.39's rule held the chunk at 4,096 + tokens or less; from 6,144 on it keeps 0.1.39's ring and only looks for a bigger chunk beside it (shrinking the + ring there measured slower: the Coder -12%), and a prompt that fits 0.1.39's chunk keeps 0.1.39's ring (one + chunk: a smaller ring only slowed it). RTX 5070, 32K prompts against `STRATA_RING_BYTES=0`, 2-3 interleaved + rounds: IQ3_XXS +18.5% at a 1,500-slot cache and +9% in another memory state, the Coder +3%, IQ3_S unchanged; 4K + prompts unchanged (-0.7% to +1.1%, bit-identical). Q2_0 (a fixed `--prefill 2048`) is not affected. It changes + the bits of a long prompt against 0.1.39 (the experts go through a different + mix of cached and streamed groups). Measured quality, teacher-forced over the next 2,001 tokens of a long document + against the FP16 prompt path (IQ3_XXS, `--prefill auto`): 8K prompt KL 0.042 (0.1.39: 0.054), top-1 agreement + 93.5% (92.2%); 32K prompt KL 0.020 (0.019), top-1 95.3% (95.0%) - the same band as before. `STRATA_RING_BYTES=0` + restores 0.1.39's ring, loan and chunk choice. The full story, with measurements, bottlenecks and what comes next: **[docs/paper/Strata-Paper.pdf](paper/Strata-Paper.pdf)**. diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 7af3a3e8bf..4ab28fb12c 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -5,9 +5,9 @@ AMD graphics card. The short version is in the [README](../README.md#install); a 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) +> [AMD cards](#amd-cards) · [Several cards](#two-or-three-cards) · [Docker](#docker-linux) · +> [Older CPUs](#older-cpus-experimental) · [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 @@ -15,7 +15,7 @@ this for you with [AI_SETUP.md](AI_SETUP.md). | --- | --- | | **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. | +| **CPU** | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. Older CPUs without AVX2 are experimental and slow: [Older CPUs](#older-cpus-experimental). | | **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)). | @@ -121,6 +121,35 @@ The same idea, in a container (NVIDIA cards). 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. +## Older CPUs (experimental) + +Strata's ready-made engine needs AVX2 (Intel Haswell 2013, AMD Zen 2017 or newer). Since 0.1.39 an older CPU - AVX +only (Sandy Bridge / Ivy Bridge, Xeon E5 v1/v2, AMD Bulldozer) or SSE4.2 only (Nehalem / Westmere, Xeon X5600) - runs +as an **experimental** build that setup compiles on that PC. Run setup as usual: it says the CPU has no AVX2, warns +that this is experimental and slow, and compiles the engine (10-20 minutes, once; Windows needs the Visual Studio Build +Tools and the CUDA Toolkit, which setup offers to install). It does not stop. The ready-made engine still refuses such +a CPU, and says so. + +What differs in that build (`STRATA_ISA_FLOOR=avx` or `none`, chosen from the CPU): + +- ggml-cpu is compiled for AVX or SSE4.2 instead of AVX2, and the CPU's share of the experts runs on its kernels. + Strata's own AVX2 / AVX-512 kernels stay in the program but are used only on a CPU that has them. +- Only the i-quant models' packs run (every model setup installs on such a CPU); the AVX-512 Q2_0 pack does not. +- Speed: the GPU part is unchanged and the CPU part is slower, so a GPU with more VRAM (more experts cached there) + helps most. With the older paths forced on a Ryzen 5 7600 + RTX 5070 and the expert cache held at 1500 slots, greedy + decode went from 26 to 11-17 tok/s with the AVX build and to 3.5-3.8 tok/s with the SSE4.2 build (Coder and IQ3_XXS, + 10 prompts each, one run). An old Xeon with DDR3 will be slower than these. +- Untested on a real old CPU by us. It was checked here by forcing the older paths on a Ryzen 5 7600 + (`STRATA_FORCE_ISA=avx` or `sse`): the answers were sane and passed the engine's checks, but not the same token for + token as the normal build (ggml-cpu rounds the CPU experts differently, as the AVX2 kernel does from the AVX-512 + one). Contributors ran earlier versions of the same approach on a Xeon E5-2680 (2.87 tok/s with a GTX 1080 Ti) + and on Xeon E5-2687W / X5690 machines. +- AMD cards: Linux only (setup compiles the AMD engine there anyway); NVIDIA on Windows and Linux. +- Two-socket Xeons: memory on the other socket is slow; keep the RAM on one CPU if you can. + +By hand: `cmake ... -DSTRATA_ISA_FLOOR=avx` (or `none`) builds it; `STRATA_ISA_FLOOR=avx` set for setup builds it on +any PC (to try it). Problems and results on real hardware are welcome as GitHub issues. + ## Updating **`UPDATE.bat`** (Linux: `./update.sh`) updates Strata without starting the model - for when the GPU is busy with @@ -145,12 +174,19 @@ says so and starts the engine you had. kept, only their names. Another browser or a private window starts empty; clearing the site's data deletes them. - **How the model starts:** `strata-.json` in the Strata folder (context, GPUs, host, API key, ...), written by setup; next to it `run-.bat` / `.sh`, the log `strata-.log` and, when you use "Use for other - apps too", `strata-.shared-settings.json`. + apps too", `strata-.shared-settings.json`. Running setup again for the same model (another context, say) + rewrites the keys setup writes (`exe`, `args`, `port`, `gpu`, `host`, `api_key`, `vision`, ...) and keeps the + ones you added (`sampling`, `mcp_servers`, `mcp`, `cors_origins`, ...); the earlier file is kept as + `strata-.json.bak` (0.1.39). Engine options you added to `"args"` by hand are not carried over: setup names + them, and you add them again. - **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. +- **Where github.com cannot be reached:** a build from source downloads llama.cpp's source (ggml, gguf-py) from + GitHub at the commit `LLAMA_CPP_COMMIT` in `setup.py` names. Put that commit's tree in `third_party/llama.cpp` + yourself (from a mirror or another PC) and setup uses it instead (#585). ## Setup's questions @@ -187,6 +223,8 @@ 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 --vram-reserve-mib 2048 leave 2 GB of VRAM free for other programs (remembered) +START-HERE.bat --no-browser do not open the chat page when the model is ready (remembered; + --browser undoes it) 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 reachable from other devices, with a key START-HERE.bat --calibrate tune the engine for this PC (about 5-10 minutes), then start diff --git a/docs/INTEL.md b/docs/INTEL.md new file mode 100644 index 0000000000..38b331eed3 --- /dev/null +++ b/docs/INTEL.md @@ -0,0 +1,652 @@ +# Strata on an Intel Arc + +Strata's engine is CUDA. On an Intel Arc it runs as **Strata's own engine, ported to SYCL** (`sycl/`, the +section "The engine itself on Intel" below), behind the same Strata server: OpenAI and Anthropic APIs, streaming, +tool calls, MCP and the web app all unchanged. llama.cpp's SYCL backend is the comparison point in the tables (it +runs the same GGUF at about a third of the speed). + +Everything Intel-specific lives in `sycl/`; no shared file of upstream's is changed, so upstream merges stay clean. +`sycl/setup_intel.py` and `sycl/serve/server_intel.py` wrap upstream's `setup.py` and `serve/server.py` from +outside. + +Written for and measured on an **Arc Pro B70 (32 GB)** running the Coder (IQ1_M) on Ubuntu 24.04, in a +PCIe 3.0 x8 slot (the card trains at Gen3 x8 there; it can do Gen5 x16). + +## What you get (2026-10-01) + +| | NVIDIA (Strata engine) | Intel Arc, Strata SYCL port | Intel Arc, llama.cpp | +|---|---|---|---| +| models | all four | the Coder IQ1_M; the original IQ2_XS too (experts beyond VRAM in the host mirror: 58-64 tok/s) | the Coder (fits VRAM) | +| model files | the same GGUFs | the same GGUFs + a native pack | the same GGUFs | +| where the model lives | experts in RAM, hot ones on the card | every expert in VRAM (`--stream-experts`: no host copy), shard 2's lookup table read from the SSD by row | all of shard 1 on the card, shard 2 paged from disk | +| RAM needed | 32-64 GB | little (23 GB is fine) | little (23 GB is fine) | +| decode, Coder IQ1_M | 44-51 tok/s on an RTX 5070 | **78.2 tok/s** (19-token prompt, 256 greedy tokens), **75.7** after a 2,184-token prompt, 69 after 40K, 66.6 after 128K, 55.9 after 256K; 48-72 through the API | 23-25 tok/s | +| prompt reading | ~1,870 tok/s | **790 tok/s** at 2,184 tokens, 1,117 at 40K, 888 at 128K, 757 at 256K | ~150 tok/s (424 at 105K) | +| speculative decoding (MTP) | yes | yes (the base checkpoint's draft layer; 70-85% of drafts accepted on code) | no | +| images | yes | not yet | not yet | +| context | up to 262K | 256K measured, 56 tok/s decode there (`--kv-resident`: the KV in pinned host memory, the attended window in VRAM) | 131K measured ceiling | + +Decode speed with speculative decoding depends on the text: code drafts well, prose less so (see "Speed depends +on the text"). The SYCL numbers are greedy runs of the engine (as in "How to run it by hand" below, 256 new +tokens) on engine 0.1.31-sycl (2026-10-01; 0.1.32-0.1.38-sycl reproduce them exactly); +"Decode round 2" below lists what each change bought. + +## Setup + +Build the engine and its runtime image first ("How to build it" below), then: + + python3 sycl/setup_intel.py [setup.py's options, e.g. --model IQ2_XS --context 32768 --port 8085] + +This is upstream's `setup.py`, run with the Intel steps swapped in (it imports setup.py and replaces those steps; +setup.py itself is unchanged). The model choice, download, pack, tokenizer, MTP draft layer and the context and KV +questions are setup's own. What changes: + +- **GPU check:** the Arc is found in sysfs (vendor 8086 under `xe` or `i915`) and offered through setup's AMD + path. That is the path that builds locally and has no images. +- **Engine step:** it uses the SYCL build (`build-sycl-aot/strata`, run in the `strata-sycl-dev` image by + `sycl/serve/strata-sycl.sh`) instead of compiling CUDA or HIP. +- **RAM rule:** this does not apply. The CUDA engine keeps every expert in RAM; the port streams them from the + GGUF into VRAM (`--stream-experts`), so RAM only decides the KV streaming. `--check` lists what fits by VRAM. +- **The config:** it uses the container's paths and `"backend": "sycl"`. The VRAM reserve is 1,024 MiB up to + 32K and 2,048 MiB with 4,096-token prompt chunks above that. KV streaming (`--kv-resident 32768`) is on from + 64K up when the RAM holds the KV. A `model_switcher` or `sampling` block from an earlier config is kept. + `run-.sh` starts `sycl/serve/server_intel.py`. + +If a future `setup.py` drops a step this relies on, it stops with a message instead of writing a wrong config. +The models, packs and checkout must sit under the folder `strata-sycl.sh` mounts at `/work` (the one above the +checkout, or `STRATA_SYCL_ROOT`). + +Then `run-.sh` (or `sycl/setup_intel.py` again) starts the model. The first start of a 30 GB model +takes about two minutes. `--port N` and `--host 0.0.0.0` work as in upstream's setup. + +## Things that matter on this GPU + +- **`SYCL_CACHE_PERSISTENT` must be 0.** The persistent JIT cache segfaults on Xe2 during the first + compile. The start script sets it; if you run llama-server by hand, do too. +- **The whole model goes on the card** (`--n-gpu-layers 999`), except `per_layer_token_embd.weight`, + the single 28.8 GB tensor of shard 2. `--override-tensor per_layer_token_embd=CPU` keeps it in host + memory, mmapped and paged from the SSD by row, which is how the model's authors serve it. Host RSS + stays around 2 GB. +- **Thinking.** The model reasons before it answers. Strata's web app has the setting; for an API + client, put `{"reasoning_effort": "none"}` in `strata-.shared-settings.json` next to the + config, or send `chat_template_kwargs: {"enable_thinking": false}` per request. Without one of + those a short `max_tokens` is spent entirely inside the think block and the answer looks empty. +- **`/health` says 503 while loading**; the server polls `/props` instead. + +## How much context fits + +The architecture keeps this small: only every fourth layer is full attention (12 of 48); the other +36 are gated-delta-net layers with a fixed-size recurrent state that does not grow with context. +Per token, the 12 attention layers keep 2 KV heads x 256 x (K + V) at q8_0, plus the +sparse-attention indexer's keys. **Measured: about 20 KiB per token** (VRAM grows 0.6 GB per 32k +of context with everything else unchanged). + +| context | VRAM in use, model loaded | headroom on 32 GB | status | +|---|---|---|---| +| 32,768 | 28.4 GB | 3.5 GB | measured, the default | +| 65,536 | 29.0 GB | 2.9 GB | measured, loads | +| 98,304 | 29.6 GB | 2.3 GB | measured, loads | +| 131,072 | 30.3 GB | 1.5 GB | **measured, the practical ceiling**: a 104,798-token prompt (79% of the window) read in 247 s at 424 tok/s, then answered | +| 163,840 | ~31.0 GB | ~0.9 GB | not attempted: under the 1.2 GB safety margin | +| 262,144 | ~32.6 GB | none | **does not fit - asking for it took the host down** | + +**Do not ask for more than fits.** On this driver a GPU allocation past VRAM does not fail: the xe +driver evicts buffers into host RAM, the kernel runs out of memory, and the machine livelocks until +its hardware watchdog resets it. A 262K request did exactly that on 2026-09-29, and llama.cpp's own +"failed to fit params" check fired too late to prevent it. Compute the KV size first and leave +1.5 GB free. + +Prompt reading gets faster with size, because the work batches better: ~150 tok/s on a 2.7k-token +prompt, 424 tok/s on a 105k-token one. So a full 128k window costs about five minutes to read, +not fifteen. + +## The engine itself on Intel: the SYCL port (`sycl/`) + +Everything above runs the model through llama.cpp. This is the other half: Strata's own engine - the 50 CUDA +kernels and the host code that drives them (streams, events, graph capture, pinned memory) - built for the +Arc with oneAPI. It is a migration of the tree, not a new backend: the engine has no backend seam to slot +into. + +**Layout.** `sycl/src` and `sycl/include` mirror the tree and hold only the files the port changes; the +SYCL build (`sycl/CMakeLists.txt`) takes every other source from the original location. No upstream file +is edited. `sycl/include/dpct/` is the vendored SYCLomatic helper library, so the port builds without the +migration tool. + +**How it was made, so it can be redone.** + +1. `sycl/tools/Dockerfile` - the dev image: the llama.cpp SYCL image plus SYCLomatic (`dpct` 2025.3), + ninja, and the CUDA 12.8 headers that `sycl/tools/get-cuda-headers.sh` pulls out of NVIDIA's pip wheels + (dpct parses CUDA; it needs the headers, not the toolkit). +2. `sycl/tools/migrate.sh` - writes a compilation database for the 86 CUDA-touching translation units and + runs dpct over them. 85 migrate; dpct reports no line it could not migrate, and about 1,400 advisory + notes. +3. `sycl/tools/fixups.py` - what dpct got wrong or could not do, as an idempotent script with a reason per + item. The ones that mattered: + - CUDA's null stream means the default stream; dpct turned it into a null `sycl::queue*`. Every + stream cast now goes through `strata::q_of()` (`sycl/include/strata/sycl_queue.hpp`). + - `__ldg((const float*) p)` came out as `*p`, reading one byte of a float scale (two sites, s_gemv). + - `__fadd_rn(a, b ? c : d)` lost its parentheses (two sites). + - the ggml lookup tables were threaded through kernel parameters with the wrong table per template; + they are plain `static const` arrays read from device code, as ggml-sycl does. + - helper headers renamed since dpct 2025.3 (`entangle`, `chunked_partition`), graph introspection and + `cudaGraphUpload` (no SYCL equivalents), `%globaltimer` (the stage profiler reads zeros). +4. `sycl/tools/build.sh` - configure + build with icpx inside the image. Two compiler flags are load-bearing: + `-fp-model=precise` (icpx defaults to a fast FP model) and `-cl-fp32-correctly-rounded-divide-sqrt` + for the device compiler - the Arc's fp32 divide is not correctly rounded by default (OpenCL allows + 2.5 ulp), and Strata's quantizers are byte-exact against ggml through `amax / 127`. Measured: without + it `quantize_act_parity` has 303k mismatches, with it none. + +**Where it stands (2026-09-29).** The whole tree builds and links: the `strata` binary and 22 kernel +parity tests. On the B70, 19 of the 22 pass byte-for-byte or within their tolerances; the other three +need model fixtures (`iq_parity`, `ple_parity`) or the tensor-core kernel below (`qsa_prompt_attn`). + +| parity test | result | +|---|---| +| bf16_gemv, cvec, dequant_s2, elementwise, gdn, gr, kv_q4, kv_q8, kv_stream, qsa, quantize_act, rope, router_top10, s2_gemv, s2_gemv_q8, s_gemv, s_gemv_q8k, sampler, shared_expert | pass | +| iq_parity, ple_parity | need fixtures (a `logs/iq_fixture` directory, a Q2_0 shard) | +| qsa_prompt_attn_parity | needs the tensor-core kernel (below) | +| kv_hybrid_parity (0.1.27, hybrid K8V4 KV) | appends, gathers and attention pass; its last step is that same tensor-core kernel | + +**The engine end to end (2026-09-29, later the same day).** It generates. Prompt "Write a Python function +that returns the n-th Fibonacci number", greedy, 64 tokens: + +> `` The user wants a Python function that returns the n-th Fibonacci number. Let me consider the +> options: 1. **Recursive approach** - Simple but O(2^n) time complexity, very slow for large n. 2. +> **Iterative approach** - O(n) time, O(1) ... + +Measured on the same card, same 2,185-token prompt, 64 greedy tokens (AOT build, `--spec 2`, +`--no-prefill-borrow`), against llama.cpp's SYCL build serving the same GGUF: + +| | llama.cpp SYCL | Strata SYCL port | +|---|---|---| +| prompt reading | 138-319 tok/s | **560 tok/s** (693 at 2,000 tokens, 180 at 300) | +| decode, suffix drafter only (`--spec 2`) | 24.2-26.0 tok/s | 20.2-20.8 tok/s | +| decode with the MTP draft layer (`--spec 4 --mtp`) | - | **42.7 tok/s** at the 2,185-token context (81% accepted, 2.9 tokens/round); 45.3 on a short prompt | + +Prompt reading is where Strata's design pays (oneMKL GEMM over dequantised experts, the whole model on +the card). Decode is at ~80%: a speculative round costs ~53 ms whatever its size and the suffix drafter +is accepted 9% of the time, so nearly every round yields one token; `--spec 4` (16.9 tok/s) was worse +than `--spec 2` (20.8) for that reason. The kernels themselves run at 130-160 GB/s of weights on a card +that streams 600 GB/s (`sycl/probe/bw.cpp`, incompressible data) - the same class llama.cpp reaches - +and five variants of the hot Q6_K matvec (lanes per row, rows per warp, unroll, 16-byte loads, software +pipelining; `mmvq_bench`) all landed in that band. The draft source was the lever: the base +Qwen3.8-Flash-Next checkpoint's MTP layer (`tools/mtp_fetch.py fetch`, `mtp_pack.py --experts q2_0`, +`mtp_rt.py`; 4.9 GB downloaded, 809 MiB of VRAM) drafts for the Coder fine-tune at 87% acceptance: +3.4 tokens per 53 ms round, 45 tok/s, the same greedy tokens. The `mtp.cpp` path ran on the first try +under the same flags. + +Without AOT the runtime JIT-compiles every kernel on first use, ~47 s the first time a process runs; +`SYCL_CACHE_PERSISTENT=1 SYCL_CACHE_DIR=` keeps that across runs (15 MB). + +How to run it by hand (a greedy test run, the way every number in this section was measured; inside the +`strata-sycl-dev` image, AOT build in `build-sycl-aot/`): + +``` +STRATA_VERIFY_DEVICE_PLAN=1 STRATA_VERIFY_NO_HOST=1 \ +build-sycl/strata --pack --native --ple-gguf \ + --expert-profile data/expert-profile-coder.bin --expert-cache auto --stream-experts \ + --prefill auto --spec 4 --spec-min-p 0.5 --max-context 8192 --tokens --max-new 64 --greedy +``` + +- `--stream-experts` (this port): no resident host copy of the experts; every one of the 12,288 goes from + the GGUF into the VRAM cache through a small staging ring (`GgufExpertSource`). Upstream needs 32 GB of + RAM for this model; with the flag the engine runs on 23 GiB. +- `STRATA_VERIFY_DEVICE_PLAN=1`: the GPU plans each layer itself (upstream's E-6; off by default there). +- `STRATA_VERIFY_NO_HOST=1` (this port): the host waits for the whole window graph instead of per-layer + rings. Only valid with every expert resident, which is the case on a 32 GB card. +- `--no-prefill-borrow`: the prompt path must not lend expert slots (a lent expert is served from the host + behind a flag the GPU does not see reliably here; long prompts hung without it). + +What the port had to get right beyond compiling (each is an entry in `sycl/tools/fixups.py` or a flag): + +- **Host-mapped flags.** Strata's decode is a GPU/CPU handshake through host-mapped memory. `volatile` + device loads do not bypass the caches on Intel; system-scope atomics do, and they are the only thing + measured to work (`sycl/probe/doorbell.cpp`, six variants). Host-to-device visibility *during* a kernel + stays unreliable on this platform, which is why the all-resident path waits for the window instead. +- **Bounded spins.** A device spin that never sees its flag is not a hang of one process: the xe driver + times the queue out and resets the GT node by node - a window graph has 2,400 - and the card stays + wedged until a reboot (twice). Every spin is capped (`kSpinMax`). +- **32-lane sub-groups.** The kernels are written for warps; dpct pinned 134 of 289 launches, the rest + would run at Xe2's default 16 (`-fsycl-default-sub-group-size=32`). +- **Synchronous copies.** `cudaMemcpy` blocks; dpct's default-queue `memcpy` did not wait. With the + streaming ring that filled the expert cache from overwritten buffers: non-deterministic residuals, NaN by + layer 5. Found with the per-layer residual ladder (`STRATA_VERIFY_DEBUG=1` prints R after every layer). +- `native_expert_parity` hand-ported: the GPU native expert kernel matches ggml's float reference on real + IQ1_M rows (rel 1.1e-2, the same class as the CPU path). + +Profiling: `sycl/tools/Dockerfile.unitrace` builds the dev image with Intel's unitrace; `unitrace -d` around +the engine plus `sycl/rank_kernels.py` gives device time per kernel. `mmvq_bench` and `native_expert_parity +NATIVE_BENCH=1` time the two hot kernel families in isolation (warm the clocks first: a 5 ms run measures +the ramp, not the kernel). + +**After the 0.1.25-0.1.27 merge (2026-09-30, same card, same flags, AOT).** Fibonacci prompt (19 tokens), +64 greedy tokens: decode 46.5 tok/s (85% of drafts accepted). The 2,184-token prompt: 566 tok/s prompt, +38.3 tok/s decode (77% accepted on that text); the draft layer's prompt pass is now upstream's batched one +(38.6 ms for 2,184 tokens, from 105 ms for 19 before it). Parity: 19 of 22, byte-exact `quantize_act` back. + +**Speed work, 2026-09-30 (after the 0.1.27 merge).** What moved the numbers, measured on the B70 with the +same prompts as above (outputs identical before and after each change): + +| change | effect | +|---|---| +| every window graph and the drafter's graphs captured at load (`STRATA_WARM_GRAPHS=0`: as before) | the first request no longer pays 250-290 ms of captures | +| the commit graph left running while the drafter's round runs on its own queue (`Verifier::commit(wait=false)` + `commit_finish`) | decode 38.3 -> 43.0 tok/s at the 2,184-token context, 46.5 -> 49.8 short | +| upstream 0.1.29 merged (GDN recurrence pipelining, the block-scores read, sampler) | 2,184-token prompt 765 -> 792 tok/s, decode at that context 43.1 -> 45.2 | +| the draft layer's batched prompt pass takes prompts under 64 rows (it fell back to per-6-token graphs) | its prompt cost on 19 tokens 106 -> 16 ms | +| one device module per kernel (`-fsycl-device-code-split=per_kernel`) | the first launch in the prompt path (the embedding gather) 245 -> 1 ms | +| the expert dequant writes each thread's run of FP16 values as one vector store (it wrote them one 2-byte store at a time) | dequant per expert 0.085 -> 0.030 ms; prompt 496 -> 575 tok/s at 2,184 tokens, 720 -> 841 at 8,000 | +| SWAR sign compare/subtract in the expert dots, a local-memory resident-plan kernel, a split-K fused down kernel (bit-identical to the single-token one) | parity-clean, no measurable decode change; kept | +| the PLE row reader's default queue depth 16 -> 64 blocking O_DIRECT threads (`STRATA_IO_THREADS`; 128 and 256 change nothing: the drive tops out near 85k IOPS on the 27k random 4 KB reads a 2,184-token prompt needs) | the rows of that prompt 466 -> 330 ms | +| a short first prompt chunk (`STRATA_PREFILL_FIRST`, default 256 tokens) so the GPU starts while the remaining rows are still being read | 2,184 tokens: 711 -> 765 tok/s, 8,000: 857 -> 986; time to first token about 150 ms less. The chunk boundary moves rounding in the expert GEMMs, so a long greedy continuation can diverge late (token 45 of 64 on the test prompt) | + +Draft policy sweep (2,184-token prompt, 128 tokens): windows of 6 (`--spec 4`) at 41.6-43.5 tok/s; `--spec 2` +33.6, `--spec 6` 35-37; `--spec-min-p` 0.3-0.7 within noise. Profiles (unitrace, `strata-sycl-dev:metrics` with +Intel's metrics libraries and `dev.xe.observation_paranoid=0`): the expert dot kernels are ALU-bound (77% XVE +active), the dense projections memory-latency bound (92-128 GB/s at 84-93% occupancy), a decode round is 80-85% +kernel time and ~5 us of launch gap per node over 2,400-2,600 nodes. + +**XMX.** oneMKL's FP16 GEMMs already run on the XMX units (30-60 TFLOP/s in `xmx_gemm_bench`); the prompt path is +bound by the dequant that feeds them, not by the products. Two joint_matrix kernels were written and are correct +but lose to the existing paths on this card, so both stay opt-in: `xmx_gemm_iq` (a fused dequant + GEMM straight +from the quantized rows, 4-5x slower than dequant + oneMKL) and `qsa_prompt_attn_xmx` (the port of the mma.sync +prompt attention; `qsa_prompt_attn_parity` passes at 1e-6 of scale, 3x slower than the FP32 fallback per chunk; +`STRATA_PROMPT_ATTN_XMX=1`). Where the prompt time goes now (2,184 / 8,000 tokens): dequant 28% / 27%, GEMMs +22% / 20%, the per-layer host grouping 9.5% / 9%, attention 7% / 14%, embeddings + PLE rows 13% / 5%. + +A trap worth knowing: with a native pack `--prefill-until N` does not feed the rest of the prompt through the +token loop (it is skipped for native packs), so the tokens after N are dropped and the model free-runs. Compare +output tokens between paths, never only timings. + +**An 80,000-token prompt (2026-09-30).** Where the time goes changes completely at this scale: + +- **The VRAM plan.** `--expert-cache auto` fills the card down to `--vram-reserve-mib` *before* the KV state + (about 1 GB in INT8 at 81,920 cells) and the prompt chunk buffers (about 2 GB at `--prefill 8192`) exist. With + the 1,536 MiB default that put 30.95 of the B70's 32.6 GB in use, the driver started migrating buffers and the + run never finished. `--max-context 81920 --kv int8 --vram-reserve-mib 3072 --prefill 4096` peaks at 28.8 GB. + (80k ids also exceed Linux's 128 KB single-argument limit: `--tokens-file`.) +- **Streamed experts.** The reserve costs cache slots (10,348 of 12,288 at 3 GB), and the prompt path streams the + missing experts from the GGUF for every chunk. Two port bugs sat on this path and are fixed: the stream plan + held `GgufExpertSource::blob()` pointers across ~1,900 reads of a 512-slot ring (so blobs were overwritten + before they were copied: the 75 s / 1,065 tok/s measured first was computed partly with the wrong experts), and + the stream-all walk hangs on this card in its first large chunk (the copy engine stops on a barrier). The stager + threads now read the blobs themselves, and the port uses the per-layer routed-only walk (`STRATA_PREFILL_RING=8` + upstream, the port's default; `STRATA_PREFILL_STREAM_ALL=1` restores the other for debugging). +- **Correct result: 80,000 tokens in 101 s = 790 tok/s.** GPU time: expert down GEMM 26.0 s, attention 15.4 s, + dequant 12.7 s, QSA block selection 9.3 s (0.2 s at 8k: it scans every block of the context per query), gate/up + GEMM 8.9 s, gather 6.1 s, the per-layer grouping sync 5.2 s, GDN recurrence 4.8 s. The PLE rows are free at this + scale (97.6% row-cache hits). +- **Decode after such a prompt, and the fix.** Without lending cache slots to the prompt path (`--no-prefill-borrow`, + which the port used from the start) the reserve evicts ~1,900 experts for good and decode after the prompt runs at + 2 tok/s. Borrowing lends ~940 slots to the prompt path and refills them in about a second afterwards: 80,000 + tokens in 75 s = **1,062 tok/s, decode 35-40 tok/s** after it, at `--vram-reserve-mib 2048 --prefill 4096`. + It costs ~1 s on short prompts (2,184 tokens: 610 vs 792 tok/s), so the port borrows by default only above a + 32K context (`--prefill-borrow` / `--no-prefill-borrow` decide explicitly). + +**Long contexts (2026-10-01, engine 0.1.32-sycl, setup's flags: `--kv-resident 32768 --vram-reserve-mib 2048 +--prefill 4096`, the prompt path borrowing cache slots - the default above 32K).** Same text repeated to length, +then 256 greedy tokens: + +| context | KV | prompt | decode after | peak VRAM | experts in the host mirror | +|---|---|---|---|---|---| +| 128K | int8 | 888 tok/s (144 s) | **66.6 tok/s** | 29.9 GB | none | +| 128K | q4_0 | 848 tok/s (151 s) | 62.8 tok/s | 29.7 GB | none | +| 128K | k8v4, KV in VRAM | **1,006 tok/s** (127 s) | **66.8 tok/s** | 30.7 GB | 103 (0.2 GB) | +| 256K | int8 | 757 tok/s (338 s) | **55.9 tok/s** | 30.2 GB | none | +| 256K | q4_0 | 737 tok/s (347 s) | 51.7 tok/s | 30.0 GB | none | +| 256K | k8v4, KV in VRAM | 823 tok/s (311 s) | 52.2 tok/s | 30.9 GB | 865 (1.7 GB) | + +Borrowing keeps every expert in VRAM (the prompt buffers live in lent cache slots, refilled after the prompt). +Without it (`--no-prefill-borrow`) the buffers keep their own VRAM and ~1,000-2,300 experts move to the pinned host +mirror: the prompt is 2-5% faster, decode after it 10-23% slower (0.1.31 without borrowing: 128K int8 934 / 59.5, +q4_0 887 / 53.4, k8v4 1,026 / 54.4; 256K int8 784 / 48.7, q4_0 763 / 43.2, k8v4 838 / 42.3 tok/s). + +KV streaming (`--kv-resident`) keeps the whole KV in pinned host memory and only the attended window in VRAM, so +the KV pushes no experts out; setup.py turns it on from 64K up and keeps INT8 (the faster of the +two at every size here). Without it the KV pushes experts out and decode after the prompt fell to 4-9 tok/s at +128K-256K (2026-09-30). `--kv k8v4` does not support streaming yet: setup keeps its KV in VRAM, which pushes up to +more experts out; those come from the pinned host mirror over PCIe (planned item 2), so decode holds at 42 tok/s at +256K (it was 3.8 before the mirror). k8v4 reads prompts fastest (no streaming copies) and works through the FP32 +attention fallback (its dedicated prompt kernel is the XMX one below). + +**XMX prompt attention v2.** 64-cell chunks, vector-packed K^T and V, hi+lo Q in one accumulator per scale group, +one accumulator update per chunk: 1.4-1.5x faster than v1 and correct in all modes (`qsa_prompt_attn_parity`, +and `kv_hybrid_parity` passes completely with it), but still ~2x slower than the FP32 fallback (13.4 vs 5.5 ms per +chunk, INT8, 32K context). This attention is gather-bound: each query position selects its own ~2,000 cells, so +the K/V fetch dominates and only 12 of the 16 matrix rows are real heads. Opt-in: `STRATA_PROMPT_ATTN_XMX=1` +(64-cell chunks, 120 KB of local memory) or `=32`. + +**XMX v2 in the full matrix, and why a grouped kernel is not next (2026-09-30).** With `STRATA_PROMPT_ATTN_XMX=1` +(stage timing on, ~8-10% overhead) the long prompts run 23-33% slower than with the FP32 attention: 128K int8 666 vs +960 tok/s, k8v4 596 vs 983; 256K k8v4 506 vs 752, int8 536 vs 718 (q4_0 does not use the XMX kernel). The obvious +fix, gathering the union of neighbouring positions' cells once, depends on how much their selections overlap. +Measured on the last chunk of an 80K prompt (first QSA layer, `STRATA_DUMP_SEL=`): the union of 8 consecutive +positions is 3.3x one position's 2,051 cells (16: 5.1x), and only 12% of a selection is shared by all 8. Grouping +would cut the K/V gather to ~40% but multiply the arithmetic by 3-5x: at best 5-8 s of an 80K prompt. Not built. + +**Serving the port (2026-09-30).** `serve/server.py --engine strata` runs the SYCL engine unchanged through +`sycl/serve/strata-sycl.sh`, an `exe` that starts the binary inside the oneAPI runtime image with the serve pipes +attached (paths in the config's `args` are the container's, the data root mounted at `/work`). A config: + +```json +{"engine": "strata", "exe": "/sycl/serve/strata-sycl.sh", + "args": ["--pack", "/work/pack", "--native", "", "--ple-gguf", "", + "--expert-profile", "data/expert-profile-coder.bin", "--expert-cache", "auto", "--stream-experts", + "--prefill", "auto", "--spec", "4", "--spec-min-p", "0.5", "--mtp", "/work/mtp/rt", + "--max-context", "32768", "--kv", "int8", "--vram-reserve-mib", "1024"], + "sampling": {"temperature": 0.6, "top_p": 0.95, "top_k": 20, "repetition_penalty": 1.05}, ...} +``` + +Measured through the OpenAI API with those sampling defaults: 36 tok/s decode on a first turn, 33 on a follow-up +(which reuses the conversation's cached prompt), against 24-26 for llama.cpp on the same card. After the decode work +(2026-10-01 build): 48-72 tok/s on a ~190-token answer, prompt included (the first request after a start is the slower +one); 55-59 tok/s over 5,000-18,000-token answers on the 2026-09-30 build. The reserve matters: +with `--stream-experts` there is no host copy of the experts, so any expert left out of VRAM is read from the SSD +and computed on the CPU whenever it is routed. At 1,536 MiB the cache came up 128 experts short and decode fell to +5-10 tok/s; 1,024 MiB fits all 12,288 with 2 GB of VRAM still free. + +**Logprobs (2026-10-02).** `/v1/chat/completions` takes OpenAI's `"logprobs": true` and `"top_logprobs": 0..20`, +streamed or not, through `sycl/serve/server_intel.py`; the reply carries `choices[0].logprobs.content[]` (`token`, +`logprob`, `bytes`, `top_logprobs`), and with thinking on the thinking tokens under `reasoning_content`. The engine +(the port only) gets `logprobs=K` on its GEN line and writes an `LP logprob id:logprob ...` line after each `T` +line, from the verify window's head logits - the distribution the token was taken from, before temperature, +penalties and sampling. Upstream's server ignores the lines, and an engine never asked writes none. Each token +costs one 1 MB logits row copy when asked. Measured (IQ2_XS, "capital of France"): "Paris" at -0.0019 (p 0.998), the +same answer and no measurable time without it. Not on `/v1/completions` or the Anthropic endpoint yet. + +**The Monitor tab on Intel.** `sycl/serve/server_intel.py` is `serve/server.py` with two additions made at run +time: + +- **GPU readings.** When NVML has no card, it plugs `sycl/serve/xe_telemetry.py` into `serve/telemetry.py`'s + `gpu_reader`. That reads the Arc through sysfs (temperature, power and its cap, the PCIe link the card + trained at). Load and VRAM come from `/run/gpustat.json`: xe reports them per client in `/proc/*/fdinfo`, + which only root can read, so a small root sampler writes them (`sycl/tools/gpustat.py`; install it with its + `gpustat.service` as its header says). +- **A Model menu.** A run config may name a `model_switcher` (an RPC taking `{"mode": m}`) for a host that swaps + models on one card. The web app's header then gets a Model menu (`sycl/serve/web/switcher.js`, injected into + the page). + +**Speed depends on the text.** Decode with speculative decoding tracks how often the draft layer guesses right. +The served engine at 32K context: 45-51 tok/s on the test prompt (continuing a Fibonacci function, 77-85% +of drafts accepted), 30-39 tok/s on a chat answer with prose (55-72%); sampling and the repetition penalty cost +nothing measurable. The engine used to abort at exit in serve mode (a queue wait in a destructor after the +runtime's teardown began); it now exits directly once its requests are done. + +**Planned (2026-09-30), in order.** Ranked by payoff on this card; the first three were taken first. + +1. Expert dot products on XMX in integer mode: a decode window (up to 6 tokens) fits one INT8 DPAS (1-8 rows), the + i-quant grids decode to small integers, the activations are already INT8. Today: scalar dp4a, ALU-bound (77%). + **Parked**: three `joint_matrix` versions (opt-in `STRATA_EXPERT_XMX=1`, do not enable) were 1.4x and 2-3x slower + than dp4a, and the third (B filled in place with `joint_matrix_apply`) hung the GPU. At 1-6 rows the grid decode + and the packed-B layout cost more than the DPAS saves. +2. Experts missing from VRAM read from pinned host memory over PCIe instead of the SSD (the 256K decode collapse). + **Done**: at start-up every expert without a VRAM slot is read into a pinned host mirror (`STRATA_MIRROR_MIB`, by + default free RAM less 4 GiB), and the device-built verify plan points the expert kernels straight at it. Before, + that plan dropped non-resident experts, so output with misses was wrong. Forced test (`--expert-cache 8000`, + 1,879 experts / 3.6 GiB out of VRAM): decode 2.6 -> 40.9 tok/s (full cache 43.3), prompt 451 -> 640 tok/s + (776), output tokens identical to the full-cache run. About 3.6 GiB of VRAM can go to context for ~6% of decode. +3. KV streaming (`--kv-resident`) from 64K up: only the attended window of the KV in VRAM. **Done**: 256K decodes at + 31 tok/s (from 4-5). +4. QSA block selection on XMX: every query against every pooled block, a dense product that grows with the context. +5. The hot decode kernels re-tuned for Xe2's native 16-wide sub-groups (twice the registers per thread). + **Tried, no gain**: SIMD16 builds of the multi-column mmvq and the wide Q6_K kernel (`STRATA_MMVQ_SG`: 16 all, + 1 IQ4_XS only, 2 short outputs only; default 32). Alone (`mmvq_sg_bench`) SIMD16 is up to 1.45x faster on IQ4_XS + and 1.3x on the 640-row shared-expert projections, 10-25% slower on the large K-quant ones; in the engine no + setting beats SIMD32 beyond run-to-run noise (SIMD32 itself lands at 39 or 45 tok/s). Output tokens identical. + The same bench shows the real headroom: the dense decode kernels stream 100-280 GB/s of a 608 GB/s card + (Q6_K, the most common type, ~146 GB/s at 2-4 columns). + **Found and fixed: misaligned loads.** A Q6_K block is 210 bytes, so every block's `ql`/`qh` runs start only + 2-byte aligned, and the B70 splits a misaligned 16-byte load into pieces. The same weights repacked at a + 224-byte stride ran 2.3-4.7x faster (`q6k_align_bench`). Without changing the layout, the wide Q6_K kernel now + does two aligned 16-byte loads and a shift (`load16_a2`; both stay inside the block) and takes every Q6_K + shape and window width (it used to be gated to n_out >= 4096, 1-4 columns): 2560->10240 at 1/2/4/6 columns + 160/145/141/101 -> 493/432/331/273 GB/s, the 248K-row head 150 -> 407 GB/s at 1 column. Output tokens + identical. **Coder decode 44.9 -> 54 tok/s** (2k prompt, greedy). `STRATA_MMVQ_A2=0` restores the old path. + Still misaligned: IQ4_XS (136 B, 8-aligned, ~100 GB/s), IQ4_NL (18 B), Q8_0 (34 B); Q4_K/Q5_K are aligned + but use 4-byte loads in the multi kernel (230-280 GB/s). + +**Decode round 2 (2026-09-30), from a unitrace of 256 decoded tokens.** Each step measured on its own, 19-token prompt, +256 tokens, two runs each, output tokens identical throughout: + +| | decode | +|---|---| +| after the Q6_K alignment fix | 53.9 tok/s | +| + `resident_plan` grouping in parallel (thread 0 alone took 73 us per layer, ~3.5 ms a round) | 57.1 | +| + the GR down kernel reads the activations directly (the 60 KB SLM tile per group capped occupancy) | 58.7 | +| + wide 16-byte-load kernels for Q4_K, Q5_K, IQ4_XS (codebook in registers) | 62.6 | +| + wide kernels for Q8_0 (MTP dense, shexp down of layer 0) and IQ4_NL (shexp down); Q6_K's activation loads aligned | 65.6 | +| + the GR down projection sliced by columns (each group stages one 128-column slice of xn, 4 lanes per row) + a fixed-order reduce | 70.6 | +| + IQ4_NL expert dots (the down projection in 39 of 48 layers): codebook in registers, weights as aligned 4-byte loads, bitwise the same ints (2026-10-01) | 76.1 | +| + GR norm per (token, stream), bitwise the same; aligned weight loads in the IQ3_XXS/IQ3_S/IQ2_S gate/up dots | **77.9** | + +The last step changes the output: identical for 146 tokens, then a near-tie after a comma goes the other way (the new +kernels sum in a different order). Q8_0 kernels 4-6x (189 -> 31 us at 2560 x 10240, 2 columns), IQ4_NL 640 -> 2560 +28 -> 6.6 us. The aligned-load helper only loads its second chunk when the address is unaligned, so it never reads a +16-byte chunk without a needed byte and cannot cross a page at the end of an allocation. `STRATA_MMVQ_WIDE_32=0` +restores the old Q8_0/IQ4_NL kernels. The sliced GR down kernel (GR read 108.6 -> 76.5 us at 6 tokens, sums equal to +~1e-7) flips that near-tie back: its output is the original one. `STRATA_GR_DOWN_SLICED=0` restores the direct kernel. + +Kernel level: IQ4_XS 107 -> 323 GB/s at 2 columns (61 -> 212 at 6), Q4_K/Q5_K 1.3-1.6x; the GR read 135 -> 110 us at +6 tokens (bitwise equal). Switches to the old paths: `STRATA_PLAN_PARALLEL=0`, `STRATA_GR_DOWN_DIRECT=0`, +`STRATA_MMVQ_WIDE_K=0`. (`iq_parity` reports 10 "missing fixture" failures with and without the change: the oracle +fixtures are not in the port's tree.) + +**Two-speed runs, explained (2026-09-30).** Identical greedy runs decode at either ~45 or ~39 tok/s. A per-gather +trace of the PLE reader (`STRATA_PLE_TRACE=1`) shows the slow runs pay one 226 ms PLE read stall in the first +decode round, after the window graphs are captured; every other read and round matches the fast runs. Prompt time +plus decode time is the same in both modes (4.95-5.17 s): the stall lands either in the prompt's PLE wait or in the +first decode round, so it is a once-per-process cost, not lost throughput. Ruled out: NVMe APST, the I/O scheduler +(`none`), CPU starvation (93% idle during decode), I/O thread count. Compare runs on time to first token + decode. +6. INT8 prompt GEMMs: experts dequantized to INT8, oneMKL/oneDNN INT8 on XMX (half the dequant bytes, 2x rate). +7. Fewer graph nodes per decode round (~2,500 at ~5 us): norm+rope, scores+top-k, gate+quantize fused. +8. Wider speculation (two draft branches per verify window): the kernels are latency-bound, so it is nearly free. +9. A model bigger than VRAM: the original Qwen3.8-Flash-Next IQ2_XS (35.5 GB of experts; the same path suits Q2_0 + and Swift 1.5) on the B70 with 23 GB of RAM. ~24 GB of experts in VRAM, the rest (~10-12 GB) in the pinned host + mirror the device plan reads over PCIe (item 2). The port has the IQ2_XS/IQ3_XXS/Q2_0 expert kernels. Open: whether + a 10+ GB pinned mirror fits beside everything else in 23 GB, and decode with that share of experts on a Gen3 x8 + link (3.6 GB mirrored measured 40.9 tok/s; expect less). Needs the 68 GB download. + **Done (2026-10-01)**: shard 1 downloaded (39.2 GB; shard 2 is byte-identical to the Coder's, so a hard link), + a native pack (`tools/iq_pack.py`, 6 s), the original model's expert profile, the same MTP draft layer. 18,329 of + 24,576 experts in VRAM (24.6 GiB), 6,247 in the pinned host mirror (8.4 GiB, 9 s to fill), host RAM never below + 12 GB free. **Decode 50.8 tok/s** on the 19-token prompt and **60.6** after 2,184 tokens, prompt 549 tok/s; + coherent, correct answers on both. Q2_0 and Swift 1.5 (similar size) should behave the same; IQ3_XXS/IQ3_S + (43-50 GB of experts) would need 19-26 GB mirrored, more than 23 GB of RAM allows. + +**Keeping up with upstream.** A merge of upstream `main` into `b70` leaves the copies in `sycl/` behind +wherever upstream touched a file they mirror. They are refreshed by re-migration, not by hand (done for +0.1.25-0.1.27, 2026-09-30): + +1. Merge upstream into `b70`. No shared file should conflict: the Intel code is all in `sycl/`. Then check that + `sycl/setup_intel.py --check` and `sycl/serve/server_intel.py --engine mock` still run against the new + setup.py and server.py. +2. Migrate the *old* upstream tree (a `git archive` of the pre-merge commit) with `migrate.sh` into + `sycl-base`, and the merged tree into `sycl-new`; about 3 minutes each in the dev image. +3. `sycl/tools/normalize.sh ` on both: dpct's file names, the unchanged files, the + message serials, then `fixups.py`. Two runs of dpct on the same source now differ only in the kernel + name hashes it generates. +4. For every file present in both: `git merge-file sycl/F sycl-base/F sycl-new/F`. Files upstream did not + change are left alone. Copies dpct never produced (`verify.cpp`, `mtp.cpp`, `remote_experts.cpp` + include no CUDA header directly) take upstream's diff by hand; new parity tests are copied in and + listed in `sycl/CMakeLists.txt`. +5. A fixup whose pattern upstream changed shows up as a compile error (the PTX gate became an `#elif` + under upstream's `__HIPCC__` guard); extend the fixup, re-run it, rebuild. + +**The 0.1.31 merge (2026-10-01).** Upstream 0.1.29 -> 0.1.31 (132 commits) by the steps above: 45 migrated files +changed for real (27 more only in dpct's kernel-name hashes), 38 conflicts resolved, verify.cpp/mtp.cpp/ +conversation_state.cpp ported by hand. A symbol audit (the port's feature identifiers counted before and after) is +part of the procedure now: it caught a dropped mirror hook and four doorbell waits that had lost their spin bound +(upstream renamed the loops; `fixups.py` covers both spellings). What the merge needed beyond conflicts: + +- **The expert kernels.** Upstream sends the i-quant experts to new multi kernels (one warp per row, 8 rows a + group); the port's grid is sized for its own kernels (8 lanes a row, 32 rows a group), so only a quarter of each + expert's rows were written and the output was end-of-text tokens. The port's kernels stay the default (they also + serve upstream's new Q4_K/Q5_K/Q5_1/Q8_0 experts); `STRATA_EXPERT_SPLIT=1` runs upstream's, with their grid. +- **Prompt-slot borrowing hung** in the first chunk of any prompt with it on (40K, 128K; GPU busy) - fixed after + the 0.1.32 merge, see "Prompt-slot borrowing: the hang" below. +- **Upstream's new tests found three dpct mistranslations from the first migration.** dpct writes `__fadd_rn(a, b)` + as `a + b` without parentheses, so `__fadd_rn(sum, c ? x : y)` became `sum + c ? x : y` (replaced, not added): in + the native QSA indexer's per-token pooled key (the decode path: blocks completed while generating got wrong + pooled keys; prompt blocks come from the batched kernel and were right), in the native QSA scores (an opt-in + path) and in the s2 activation rounding (every value 0; the Q2_0 s2 pack, unused here). Fixed, and `fixups.py` + now parenthesises. `qsa_parity`'s batch-vs-sequential test passes; decode output after long prompts changed. +- **Upstream's new kernels against the port's on the B70** (decode tok/s, Coder 19 / 2,184-token prompts, IQ2_XS + 19): port defaults 78.1 / 75.7 / 58.6; `STRATA_EXPERT_SPLIT=1` 65.6-69.5 / 67.2 / 49.1 (-11 to -16%); + `STRATA_GR_V3=1` 73.2 / 72.4 (-4 to -6%). Both stay opt-in. +- **Lanes per row of the port's expert kernels** (`STRATA_GU_LANES` / `STRATA_DOWN_LANES`, 4/8/16/32 at run time): + gate/up 8 or 16 x down 4 or 8 all decode in 37.7-38.6 ms per verify round (2 runs each, Coder, both prompts); tok/s + differences between them are draft acceptance (each split rounds differently). The default (8 / 8) stays. +- **Speed after the merge:** Coder 78.2 / 75.7 tok/s (as before), IQ2_XS 58.6 / 64.2 (from 50.8 / 60.6). +- **Still failing:** (`iq_multi_parity` on IQ2_XS: fixed later, see "Two model-specific bugs"), `s2_expert_grouped_parity` (the s2 path), `kv_hybrid_parity`'s last step + (needs the unported tensor-core prompt kernel). `ple_parity` and `native_expert_parity` need model files. + +**The 0.1.32 merge (2026-10-01).** Upstream 0.1.31 -> 0.1.32 (89 commits) by the same steps; the hash-only +differences are now canonicalized before `git merge-file` (the port's kernel-name hashes are kept), which left 10 +files with real conflicts. What it needed: + +- **Upstream's async commit** (`set_commit_async` / `wait_commit`) maps onto the port's own deferred commit + (`commit(n, err, false)` + `commit_finish`); `wait_commit` is `commit_finish`. +- **Upstream's new hyper-connection read variants** (split / staged, chosen per card by a bit-for-bit self-test at + start) are not used by the port, which keeps its sliced down / split norm read; the self-test segfaulted on the + B70, so on SYCL it runs only with `STRATA_HC_CHECK=1` (open). The port's split-norm kernel was renamed + (`gr_norm_split_port_kernel`): upstream now has one of the same name. +- Two kernel names collided after hash canonicalization (renamed), and fused_gr's per-block shared-memory query is + a fixup now. +- **Result:** every output identical to 0.1.31 (Coder 19 / 2,184-token prompts and IQ2_XS, 256 greedy tokens), + same speeds (Coder 77.7-78.1 / 75.7 tok/s, IQ2_XS 58.5), 40K prompt 1,201 tok/s then 65.1 tok/s decode. + `kv_hybrid_parity` and `qsa_prompt_attn_parity` pass (the tests now turn on the XMX prompt attention they + check); `iq_multi_parity` (IQ2_XS) and `s2_expert_grouped_parity` as before. + +**The 0.1.33 merge (2026-10-01).** Upstream 0.1.32 -> 0.1.33 (17 commits): no shared file conflicted (the Intel +code is all in `sycl/`); six engine files changed upstream and were merged by the same re-migration (only those six: +the other files dpct produced differently were left alone). Two conflicts: `--resident-cpu-experts` (upstream's new +`resident_cpu_explicit` beside the port's `--stream-experts`) and the prompt attention's compute-capability check +(the port keeps its XMX dispatch). `sycl/setup_intel.py` and `sycl/serve/server_intel.py` ran unchanged against the +new setup.py and server.py. Every output identical (Coder, IQ2_XS, the 40K prompt with borrowing); `gr_parity` and +`qsa_prompt_attn_parity` pass. + +**Prompt-slot borrowing: the hang (fixed 2026-10-01).** From 0.1.31 on, a prompt that borrowed cache slots stopped +in its first full chunk with the GPU at 100% and the host waiting on the compute queue. It was never a borrowing +bug: borrowing is just the only way the Coder streams experts during a prompt (every expert is resident otherwise), +and the streamed path's stager deadlocked. Its threads read experts into a ring of 16 page-locked buffers and, to +reuse one, waited on the copy queue's event for the DMA that last read it (the CUDA form, `cudaEventSynchronize`). +Under the Level Zero v2 adapter that event, once a host thread had waited on it, no longer released the other +queue's barrier that also listed it, and the GPU waited forever - only once a layer streamed more than 16 experts +(the ring wrapped). The v1 adapter (`SYCL_UR_USE_LEVEL_ZERO_V2=0`) and a 256-buffer ring both ran; so did a sync +after every phase (`STRATA_PREFILL_SYNC=1`, kept as a debug switch). The fix: the copy queue writes a sequence +number into page-locked host memory after each DMA and the stager polls it - no host thread waits on a queue's +event. Output is bit-identical to the run without borrowing; borrowing is the default above 32K again. + +**Two model-specific bugs found with Swift 1.5 (fixed 2026-10-01).** UkisAI's Swift 1.5 IQ2_XS (setup's `swift` +family, the same GSQ-RCO layout) decoded token 0 forever: NaN logits from layer 13 on. + +- **`--stream-experts` read the wrong shard.** Since upstream 0.1.31, `ExpertLayout::gguf_file` is per layer AND role + (`3 * layer + role`; a shard boundary can fall inside a layer). The port's `GgufExpertSource` still indexed it by + layer, so a layer in shard 2 was read from shard 1 at shard 2's offsets: garbage IQ1_M scales, infinities after the + fp16 dequantization. The original model keeps every expert in shard 1 and never noticed; Swift's layers 13-47 are + in shard 2. `STRATA_DBG_NAN=1` now also reports the experts' fp16 GEMM inputs (activations, dequantized gate/up). +- **IQ2_XS products were garbage** (the long-failing `iq_multi_parity` IQ2_XS case): ggml's `vec_dot_iq2_xs` reads + its four 16-bit codes through a `uint16_t*` to an `int2`, type punning the SYCL device compiler does not honour; + the dequantizer, which reads the codes directly, was exact. The codes are extracted by shifts now (the dot and the + expert kernels' `Split<17>`); the test checks against a CPU decode from ggml's tables too and passes. No model here + uses IQ2_XS tensors, Swift included - this was not Swift's bug, only found on the way. + +Swift 1.5 IQ2_XS after both: correct answers, 51-118 words of thinking on short questions, 48 tok/s raw decode. + +**Load time (2026-10-02).** Starting a model is mostly the expert cache's fill from the GGUF (`--stream-experts`): +the Coder's 23.4 GiB of experts took 64-76 s, the IQ2_XS's 24.9 GiB 91 s, because the fill read each expert's +three slices, copied the blob and waited for the copy before the next read. It is a pipeline now: the slots are +admitted in profile order first (the same placement), the reads go in file order by up to 8 threads into +page-locked batches of 64, and a batch's copies run while the next one is read. Cold page cache: 76.2 s -> 18.8 s +(0.33 -> 1.34 GB/s); a whole Coder start from the engine's launch to its first token 82 s -> 26 s, the IQ2_XS +120 s -> 41 s (its 8.2 GB host mirror is ~9 s of the rest). Output identical. `STRATA_FILL_SERIAL=1` is the old fill. + +**The stream-all prompt walk is back (2026-10-03).** It copies every expert the VRAM cache does not hold, layer by +layer ahead of the compute, instead of only the routed ones. It hung on the B70 until the stager and PLE fixes of +2026-10-02 and runs since. It now runs by default when the VRAM holds more than 90% of the (layer, expert) pairs; +past that it would copy several times the routed experts (the IQ2_XS keeps a quarter of them in the RAM mirror: a +2,184-token prompt fell from ~560 to 254 tok/s with it). `STRATA_PREFILL_STREAM_ALL=1` / `=0` force it on or off. +Coder, outputs identical either way: + +| prompt | routed-only | stream-all | +|---|---|---| +| 40K int8 | 1,106 tok/s | 1,160 | +| 128K int8, KV streaming | 897 | 920 | +| 128K k8v4 | 1,006 | 1,041 | +| 256K int8, KV streaming | 760 | 780 | + +Measured alongside: the 2,184-token prompt reads 784-799 tok/s on both the 0.1.35+PR and the 0.1.38 build (three +runs each), so the 825 seen once on 0.1.35 was session variance, not a regression. The prompt path at 40K (Coder, +GPU timeline): expert dequant 15.7%, expert GEMMs 23.4%, host grouping 6.5%, gather 5.7%, combine 2.4%, prompt +attention 19.2%, QSA select 6.8%, DeltaNet recurrence 5.8%, hyper-connection reads 4.6%. Upstream's sm_90 cluster +kernels (greedy sampler, QSA top-k) would save next to nothing here: the sampler reads ~6 MB a round against a 13 ms +token. Q4_0 tensor-core prompt attention (#452) is not worth porting: the port's XMX prompt attention is 2-3x slower +than the plain kernel it uses for every KV format. + +**The 0.1.38 merge (2026-10-03).** Upstream 0.1.35 -> 0.1.38 (83 commits) merged the seven PRs the port had taken +early, so their header forks in `sycl/include` are gone; the merge base for the 3-way merge was the port's own "main + +PRs" migration. New in the port with it: the DeltaNet output norm without its dead FP32 store, two prompt-path +fixes (the fused layout's buffers, the streamed walk's resident lookup), `STRATA_GR_DOWN_MAX4=1` (opt-in; neutral on +the B70: 2,184-token prompt 793 vs 788 tok/s, decode 76.2 vs 76.3). Stubbed in the port: `--peer-device` (a second +GPU as an expert-cache tier: upstream's code is CUDA calls; `open` refuses with a message) and the fused int8 prompt +kernels (#136, part of the MMQ library the port does not build). Off on SYCL: the sm_90 thread-block-cluster greedy +sampler and QSA top-k (no SYCL counterpart; the callers take the plain kernels). `STRATA_GDN_KEYHEAD=1` stays opt-in +(still 8% slower here: 727 vs 788 tok/s). `qsa_prompt_attn_parity` now also tests upstream's Q4_0 tensor-core mode +(#452), which the port does not have: those cases report "refused" (the int8 and fp16 XMX cases pass); a `--kv +q4_0` prompt takes the older kernel, as before. Outputs identical (Coder, IQ2_XS, 40K); 128K int8 KV streaming +892 / 66.8 tok/s. + +**0.1.35 and seven open upstream PRs (2026-10-02).** Upstream main 0.1.33 -> 0.1.35 merged as before, then seven open +PRs ported into `sycl/` ahead of upstream (one dpct run of main + all of them, 3-way merged into only the files they +touch; `sycl/tools/merge_upstream.py`). Their header changes live in `sycl/include` until upstream merges them. + +| PR | what | on the B70 | +|---|---|---| +| #385 (sergqwer) | stager: a buffer's first job of a generation waits for the previous DMA from it | race fix; same output | +| #463 (constantindjonkam) | decode waits for an adaptive swap before reading the residency table | determinism fix | +| #453 (architectds) | the drafter's batched K/V on the KV-streaming ring | 128K int8: 888 -> 895 tok/s prompt, 66.6 -> 67.0 decode | +| #374 (sergqwer) | the first chunk's PLE rows read beside layer 0 | part of the 2,184-token prompt's 784 -> 825 tok/s | +| #363 (BlueKingMuch) | the PCIe expert call: group stride, fused SwiGLU + q8_1 | IQ2_XS decode +1.7%; re-done on the port's lane kernels | +| #407 (sergqwer) | `--adapt-tuned` (opt-in) | neutral here; stays opt-in | +| #413 (BlueKingMuch) | DeltaNet recurrence per key head | bit-identical but 8% slower prompt here: off (`STRATA_GDN_KEYHEAD=1`) | + +#374 needed two port fixes: its next-chunk read assumed every chunk is the full chunk length (the port's first chunk +is 256 tokens: the second chunk's rows were read from the wrong place), and its host wait on the PLE upload's event, +now inside the layer loop, deadlocked the prompt past ~4K tokens under the Level Zero v2 adapter (the stager's bug +again): the upload is marked by a polled sequence number now. #413's gate is an NVIDIA SM-count rule and its parity +test an SM-holding NVIDIA bench (not built). Outputs identical to 0.1.33 (Coder 19 / 2,184 tokens, IQ2_XS, 40K). + +**Not ported yet (2026-10-01).** + +- Three kernels carry inline PTX (`mma.sync` tensor-core matrix ops, `ldmatrix`, `cp.async`): + `qsa_prompt_attn`, `qsa_select`'s block scores, `native_qsa_score`. The SYCL build takes the "older card" + fallback the CUDA build uses below sm_80. `qsa_prompt_attn` also has an XMX version (`joint_matrix`, opt-in + `STRATA_PROMPT_ATTN_XMX=1`): correct, but slower than the fallback (see "XMX prompt attention v2"). +- The ggml MMQ prefill path (`moe_mmq.cu`) needs llama.cpp's ggml-cuda sources; not built. +- AOT device code is what runs: `AOT=bmg-g31 BUILD_DIR=.../build-sycl-aot` (the JIT build costs ~47 s of + compiling on the first window). + +## Not done + +- Images, on both Intel engines. Strata's vision path encodes with `strata-vision` into embeddings the CUDA + engine reads; neither the SYCL port nor the llama.cpp config wires it yet. +- Speculative decoding on the llama.cpp path (the GGUF carries no draft layer llama.cpp can use). The SYCL + port has it (MTP draft layer, `--mtp`). + +## Measured, 2026-10-01, Arc Pro B70, Coder IQ1_M, 32K context, INT8 KV: the SYCL port (engine 0.1.31-sycl) + +| | | +|---|---| +| decode | 78.2 tok/s on a 19-token prompt, 75.7 after a 2,184-token prompt (256 greedy tokens, MTP + suffix drafts) | +| prompt | 780 tok/s on 2,184 tokens; 934 tok/s at 128K, 784 at 256K | +| through the API | 48-72 tok/s on a ~190-token chat answer, prompt included (the first request after a start is the slower one) | +| VRAM | all 12,288 experts resident, ~1.9 GB free with everything loaded (`--vram-reserve-mib 1024`) | +| RAM | no host copy of the experts (`--stream-experts`) | + +## Measured, 2026-09-29, Arc Pro B70, Coder IQ1_M, 32K context, q8_0 KV: llama.cpp + +| | | +|---|---| +| load | ~100 s | +| VRAM | 28.4 of 32 GB | +| decode | 23-25 tok/s; GPU 92% busy at 165 W, CPU idle | +| prefill | 149 tok/s on a 2,701-token prompt; 424 tok/s on a 104,798-token prompt at 131k context | +| quality | correct code on every test; matches the NVIDIA path token for token in spirit, not measured | diff --git a/docs/INTEL_ARC.md b/docs/INTEL_ARC.md new file mode 100644 index 0000000000..7b6acfeb63 --- /dev/null +++ b/docs/INTEL_ARC.md @@ -0,0 +1,126 @@ +# Intel Arc (experimental) + +Strata 0.1.39 includes an **experimental Intel Arc engine**: Strata's own engine ported to SYCL (Intel oneAPI). +maxfridbe wrote it in [#423](https://github.com/Niko1221/Strata/pull/423), with fixes from the people testing it. +The code is in `sycl/`, and the port's own notes, measurements and maintenance procedure are in +[docs/INTEL.md](INTEL.md). This page covers what you need, how to build it, and what has been tested. + +**Experimental means:** the Strata maintainers have no Intel GPU. We compile the port and run its kernel tests, but +we have not run it on an Arc. Every result on Arc hardware below comes from the community. The NVIDIA and AMD +engines are unchanged: the Intel build is a separate CMake target, off by default. + +There is **no ready-made Intel engine** in the release zips. You build it from source on Linux. + +## What has been run, and by whom + +| | Hardware | Result | Reported in | +|---|---|---|---| +| Port author | Arc Pro B70 32 GB, Ubuntu 24.04 | Coder IQ1_M: 70-78 tok/s decode, ~790 tok/s prompt; IQ2_XS: 51-64 tok/s; 256K context measured | #423, [INTEL.md](INTEL.md) | +| Community | 2x Arc Pro B70, `--layer-split` | Flash-Next IQ3_XXS 66 tok/s decode, 394 tok/s prompt (with the `stage_room` fix that is now in 0.1.39) | #423 | +| Community | Arc Pro B50 16 GB | Coder IQ1_M ~23 tok/s, IQ2_XS ~25-27 tok/s, up to 128K | #423 | +| Community | Arc B580 12 GB, WSL2 | IQ2_XS ~21 tok/s, Coder ~15 tok/s; **device loss also seen** | #423 | +| Strata maintainers | no Arc | compile check and kernel tests on a CPU device only (below) | this release | + +The 0.1.39 port re-migrates 0.1.38's port onto the 0.1.39 engine sources (the #606 NaN fix, the #649 verify +trace, the new prompt paths). It compiles and its kernel tests run, but **nobody has run the 0.1.39 port on an Arc +yet**. The numbers in the table were measured on earlier versions. + +## What was tested here (0.1.39) + +- **Compiles:** Ubuntu 24.04 (WSL2), Intel oneAPI DPC++ 2026.1.1 + oneMKL 2026.1. The whole `sycl/` project + builds with 0 errors: the `strata` engine plus all 157 targets (the kernel parity tests and benches). The build is + SPIR-V (JIT). The AOT build (`STRATA_SYCL_AOT`) was not built here, because it needs `ocloc`. +- **Kernel parity tests on a CPU** (`ONEAPI_DEVICE_SELECTOR=opencl:cpu`, Intel's OpenCL CPU runtime, AMD Ryzen 5 7600): + 14 of 25 pass. That is the same set that PR #423's own 0.1.38 port passes on that device: the failures are tight + float tolerances on the CPU's math (rel 3e-6 against a 1e-6 limit), model fixtures that are not present, and two + tests that time out on a CPU. This is a check that the kernels compile and compute, not a test of an Arc. +- **Setup:** `--backend sycl` warns, then hands over to `sycl/setup_intel.py` on Linux (unit tests: + `tools/test_setup_sycl.py`). +- **Unchanged:** the CUDA engine's greedy output, checked byte-identical against the gated 0.1.39 build (Q2_0 and Coder). + The HIP build is not affected (the option is off by default). + +**Not tested by anyone yet:** Windows (no native build path; see below), Arc on WSL2 for the 0.1.39 port, the Alchemist +A-series, integrated Arc GPUs (the B390 / Panther Lake in #515; the shared-memory planning does not exist yet), and +images (not wired on Intel). + +## What you need + +- **Linux**, e.g. Ubuntu 24.04, with Intel's GPU driver (the `xe` or `i915` kernel driver plus the compute runtime / + Level Zero; on Ubuntu, `intel-opencl-icd libze1 libze-intel-gpu1`, or Intel's + [client GPU guide](https://dgpu-docs.intel.com/driver/client/overview.html)). +- **Intel oneAPI**: the DPC++ compiler (`icpx`, 2025.3 or newer; 2026.1 is what was built here) and **oneMKL**. About 5 GB. +- `cmake` 3.24+, `ninja`, `git` (the build fetches ggml unless you point `STRATA_GGML_DIR` at a llama.cpp checkout), + Python 3. +- For `setup --backend sycl` today: **Docker**. `sycl/setup_intel.py` runs the engine in the `strata-sycl-dev` + image built from `sycl/tools/Dockerfile`. Note that the Dockerfile starts from a community llama.cpp SYCL image + (`ghcr.io/snailium/...`), not an Intel or Strata image. +- VRAM: the port keeps the experts on the card (`--stream-experts`). A 32 GB card holds the Coder IQ1_M or IQ2_XS. + Smaller cards mirror part of the experts in RAM and are slower. + +## Build (Linux) + +Install oneAPI from Intel's apt repository (this is what was used here): + +```sh +wget -qO- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB \ + | sudo gpg --dearmor -o /usr/share/keyrings/oneapi-archive-keyring.gpg +echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" \ + | sudo tee /etc/apt/sources.list.d/oneAPI.list +sudo apt update +sudo apt install intel-oneapi-compiler-dpcpp-cpp intel-oneapi-mkl-devel ninja-build cmake git +``` + +Then build the engine. Either command works: the first goes through the top-level CMake option, the second +configures the `sycl/` project directly. + +```sh +source /opt/intel/oneapi/setvars.sh +cmake -S . -B build-sycl -G Ninja -DCMAKE_C_COMPILER=icx -DCMAKE_CXX_COMPILER=icpx -DSTRATA_ENABLE_SYCL=ON +# or: cmake -S sycl -B build-sycl -G Ninja -DCMAKE_C_COMPILER=icx -DCMAKE_CXX_COMPILER=icpx +cmake --build build-sycl --target strata +``` + +Options: + +- `-DSTRATA_SYCL_AOT=bmg-g31` (Arc Pro B70) or `bmg-g21` (B580 / B570 / Pro B60) compiles the GPU code ahead of + time. This needs `ocloc` (Intel's `intel-ocloc` package). Without it, the first start JIT-compiles every kernel, + which takes about 47 s. +- `-DSTRATA_SYCL_PARITY=OFF` skips the kernel tests (on by default). Run them with + `ctest --test-dir build-sycl` on the card. + +`setup_intel.py` looks for `build-sycl-aot/strata` or `build-sycl/strata` in the checkout. With the +top-level option the engine is at `build-sycl/sycl/strata`, so either use `-S sycl` or set `STRATA_SYCL_BIN`. + +## Setup and running + +```sh +./setup.sh --backend sycl [setup's usual options, e.g. --model IQ2_XS --context 32768] +``` + +This prints the experimental warning and continues with `sycl/setup_intel.py`. That script finds the Arc in sysfs, +uses the SYCL engine you built, and writes the config and `run-.sh`. It still downloads and packs the model +the usual way. To run the engine by hand (no Docker), see "How to run it by hand" in [INTEL.md](INTEL.md). + +Things that matter on an Arc (details in INTEL.md): + +- **Do not ask for more VRAM than the card has.** On the `xe` driver, an allocation past VRAM can push buffers into + RAM until the machine runs out of memory and stalls. Leave about 1.5 GB free. +- `SYCL_CACHE_PERSISTENT=0`: the persistent JIT cache crashed on Xe2 during the first compile. +- Two cards: `ONEAPI_DEVICE_SELECTOR=level_zero:*` (the image pins `level_zero:0`; `strata-sycl.sh` now passes the + variable through) and `--layer-split`. +- `STRATA_VERIFY_NO_HOST=1` (set by `strata-sycl.sh`) is only valid when every expert is in VRAM. On smaller cards + that path is the one that has hung, and #667 found the likely reason: the GPU does not see the CPU's flag + stores without a system fence. + +## Windows + +There is no Windows path yet. `setup --backend sycl` on Windows stops and points here. oneAPI exists for Windows, +but `sycl/CMakeLists.txt` uses GCC-style flags (`-mavx512f`, `-fp-model=precise`, `-qmkl`) and the runner is a +bash/Docker script, so a native Windows build would need work. Nobody has tried it. WSL2 with an Arc has been +used by one tester (the B580 row above), but setup cannot detect the card there, because it reads `/sys/class/drm`, +which WSL2 does not have. + +## Reporting a problem + +Open an issue with: the card, the driver version, `sycl-ls` output, the oneAPI version, the model and flags, and the +engine's stderr. Results from real cards are what move this from experimental to supported. diff --git a/docs/MCP_SERVER.md b/docs/MCP_SERVER.md index b72c659c24..4fc053697f 100644 --- a/docs/MCP_SERVER.md +++ b/docs/MCP_SERVER.md @@ -111,7 +111,7 @@ The command is `python /tools/strata_mcp.py`. The transport is st | --- | --- | --- | | `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_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-IQ4_XS, 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) | diff --git a/docs/MODELS.md b/docs/MODELS.md index 95a954d146..1ffc87f5aa 100644 --- a/docs/MODELS.md +++ b/docs/MODELS.md @@ -14,7 +14,7 @@ The installer recommends one for your PC; this page explains the choice. Back to | **32 GB** | **Coder** | it fits 32 GB; best at code, weaker at everything else ([why](#coder)). With a 24 GB card, Q2_0 and IQ2_XS run too ([low-RAM mode](#a-big-graphics-card-and-little-ram)) and are the better pick for general use | | **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 | +| **96 GB or more** | **IQ3_S**, or [Unsloth's UD-IQ4_XS](#unsloth-ud-iq4_xs) (~4-bit) | room for the largest sizes | Not sure? Take **IQ2_XS**. The **Coder** is the one that fits a 32 GB PC, but it keeps only 256 of the 512 experts, chosen on code data, so it is weaker outside code and in languages other than English. For general use, or whenever @@ -71,8 +71,9 @@ fetches the MTP draft layer (~6 GB, +1 GB with images). 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 experts + about 10 GB** for Windows and your other programs - the experts are most of shard 1: 34 GB for Q2_0, +35.5 for IQ2_XS, 43 for IQ3_XXS, 50 for IQ3_S, 23 for the Coder (the dense weights in shard 1 go to the graphics +card). 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 @@ -116,9 +117,22 @@ More: [details](DETAILS.md#or-swift-15-a-fine-tune-that-thinks-shorter). START-HERE.bat --setup --family swift --model IQ2_XS ``` +### Unsloth UD-IQ4_XS + +**Unsloth's UD-IQ4_XS** (~4-bit) is the fourth version in setup's menu (`--family unsloth`, its first size), a +regular choice from 0.1.39: a 94 GB download with 59.5 GB of experts (IQ3_S and IQ4_NL), between IQ3_S and UD-Q4_K_XL +in quality. Strata keeps your RAM minus 24 GB of the experts in RAM and reads the rest from the SSD while it answers: +on a 64 GB PC part of them come from the SSD (slower; an NVMe SSD helps), from ~80 GB of RAM all of them stay in RAM. +It needs 48 GB of RAM or more and engine 0.1.38 or newer; images are an option, as with the other models. +Details: [UD-IQ4_XS](UNSLOTH_Q4.md#ud-iq4_xs-setup-from-0139-621). + +``` +START-HERE.bat --setup --family unsloth --model UD-IQ4_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 +**Unsloth's 4-bit UD-Q4_K_XL** (experimental) is the second size of the same family (`--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 diff --git a/docs/MULTI_GPU.md b/docs/MULTI_GPU.md index 6e0b0d9687..1270125b3a 100644 --- a/docs/MULTI_GPU.md +++ b/docs/MULTI_GPU.md @@ -56,6 +56,11 @@ Or edit an existing config (`strata-*.json`), then restart: "layer_split": "auto" ``` +`"layer_split"` is `"auto"` (placed by each card's free VRAM) or the **first layer of each later card**: one rising +number per card after the first, not a count of layers per card. With 4 cards and a 48-layer model, `"24,36,42"` +(or `[24, 36, 42]`) puts layers 0-23 on the first card, 24-35 on the second, 36-41 on the third and 42-47 on the last. +The server checks it before the start and says what is wrong (0.1.39, #644). + **Skip the split when the first card holds everything** (opt-in, 0.1.31): `"split_skip_if_fits": true` in the config (engine flag `--split-skip-if-fits`, with `--layer-split auto`) runs on the first card alone when it holds every profiled expert plus the context's KV, the draft layer and the reserve, and says so in the log; otherwise the split @@ -71,10 +76,32 @@ VRAM keep its own prompt buffers - the same output as 0.1.31, measured on an R97 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 idle card can help one-chunk prompts (opt-in, `STRATA_PREFILL_HELP=1`).** A prompt that fits one chunk runs the stages one after the other, so while +one card reads its layers the other idles. With it on, each stage hands a share of its streamed experts to the idle card: it +streams them over its own PCIe link into its own (lent) prompt buffers, computes their rows on the MMQ path and sends +them back - `--peer-device`'s peer streaming, without P2P (the activations and the rows go through mapped host memory, +read by copy kernels, so they do not queue behind the expert blobs on either card's copy engine). The share falls with +the prompt (0.41 of the streamed experts at 1.5K tokens, 0.32 at 3K) and is off from ~3.3K tokens, where the stages +overlap anyway and no share paid. Measured on 2x RTX 3090 (UD-Q4_K_XL, no P2P), prompt tok/s without / with: 1.1K 496 / +723, 1.5K 674 / 833, 2K 878 / 1,128, 2.5K 1,017 / 1,296, 3K 1,260 / 1,440; 4K and 8K unchanged, decode unchanged. It +costs no VRAM (the idle card's own prompt buffers) and ~110 KB of mapped host memory per token of the largest chunk it +helped (~360 MB at 3.3K tokens). The rows it computes round like a different MMQ grouping, so the output is not +bit-identical to the default's (it is repeatable: same prompt, same output), which is why it is **opt-in**: +`STRATA_PREFILL_HELP=1` turns it on. Native packs on the MMQ prompt path only (not with the fused prompt kernels, +`STRATA_PF_FUSED=1`, nor with `--peer-device`). With it on, `STRATA_PREFILL_HELP_FRAC=f` fixes the share. + 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. +**Each card loads only its own layers' dense weights** (0.1.39, PR #639) with explicit split points (`--layer-split +27`, not `auto`): every card used to keep a full copy (~3.4 GB for the Coder) though its stage reads only its own +layers, and the VRAM it frees goes to that card's expert cache (2x MI50 16 GB, Coder: 8,819 -> 10,626 experts in +VRAM, decode 39.2 -> 41.7 tok/s). Opt-in for now, on AMD and NVIDIA alike: `STRATA_STAGE_TRIM=1` (please report how +it goes). +A card holding more experts can change which experts run on the GPU, so the output can differ slightly from a run +without it. + **auto** tries every placement (all of them for two or three cards; proportional to the free VRAM beyond that) and keeps the one whose caches would hold the most of the expert profile, hottest pairs weighted most; ties go to the placement that leaves the fullest card the most room. The startup log prints the choice: @@ -140,3 +167,8 @@ The Coder on an RTX 5080 + RTX 3090 (Ryzen 9 9950X3D), 32K context; details in - Leave out a much slower card when two already hold the model. An RTX 2080 Ti as a third card made the 5080 + 3090 pair slower (68 / 90 tok/s decode): every extra card costs its own round per window. - More cards pay off when the model's routed experts do not fit the faster ones. + +## Several conversations at once + +With a layer split, `--batch N --batch-groups G --trim-stage-weights` decodes several conversations together and +pipelines them through the cards: see [BATCHING.md](BATCHING.md). diff --git a/docs/NVIDIA_V100.md b/docs/NVIDIA_V100.md new file mode 100644 index 0000000000..7e8b0c45fe --- /dev/null +++ b/docs/NVIDIA_V100.md @@ -0,0 +1,71 @@ +# NVIDIA Tesla V100 / Titan V (Volta, sm_70): the community build + +Strata's ready-made engine and the support matrix start at compute capability 7.5 (RTX 20). A V100 (7.0) runs the same +engine compiled for it, through the experimental build that issue #236 added. It is **community-tested, not part of the +supported cards**: no ready-made engine is published for it and the numbers below come from one machine. + +## Build + +CUDA 13 dropped Volta, so the build needs a **CUDA 12.x** toolkit (12.8 was used) and a host GCC that toolkit accepts. + +```sh +cmake -S . -B build -DSTRATA_ENABLE_CUDA=ON -DSTRATA_BUILD_TESTS=OFF \ + -DCMAKE_CUDA_ARCHITECTURES=70 -DSTRATA_EXPERIMENTAL_SM60=ON +cmake --build build --target strata -j +``` + +Setup does the same by itself for a model on a V100 (the experimental CUDA 12 engine, in `engine-cuda12/`: ready-made +on Windows, compiled with a CUDA 12.x toolkit on Linux; [OLDER_GPUS.md](OLDER_GPUS.md)). For a PC with a V100 and a newer card, a build for both architectures (`-DCMAKE_CUDA_ARCHITECTURES="70;86"`) is the +natural choice (a community report in issue #509 ran an RTX 3080 beside a V100 that way; it was not repeated here). On the +measuring PC a Quadro RTX 4000 (sm_75) sits beside the V100s and was hidden with `CUDA_VISIBLE_DEVICES` because the sm_70-only +build has no code for it. + +## What runs on Volta + +The flag only lowers the floor (CMake and the runtime device check) and replaces two intrinsics Volta lacks. It does not +select "old" kernels everywhere: which kernel runs is decided per architecture when the engine is built. + +| Part of the prompt path | On sm_70 | +| --- | --- | +| MoE experts (ggml MMQ) | `dp4a` kernels (Volta has no int8 tensor cores) | +| Dense projections (dequantized weights) | FP16 tensor-core GEMMs (cuBLAS / CUTLASS `s884`) | +| BF16 projections (hyper-connection, router, indexer, ...) | converted to FP16 and run on the FP16 tensor cores (#655, #540; `STRATA_BF16_TC=0`: cuBLAS BF16, an FP32 SIMT kernel on Volta) | +| QSA attention for decode and verify windows (and prompts with `STRATA_PROMPT_ATTN_OLD=1`) | #540's kernel (fewer shuffles, bit-exact; `STRATA_ATTN_PRE75=0`: the one other cards run) | +| QSA prompt attention, int8 / FP16 / K8V4 KV | `prompt_attn_v70_kernel` on `mma.m8n8k4` (`STRATA_PROMPT_ATTN_OLD=1`: the decode kernel, one query at a time) | +| QSA prompt attention, Q4_0 KV | the decode kernel | +| QSA block scores | the warp kernel (the tensor-core scorer needs sm_80) | + +## Measured + +One V100-PCIE-32GB, i9-7960X, Unsloth `UD-IQ4_XS` (not a setup model), int8 KV, MTP `--spec 2`; prompt read speed of random-word prompts, tokens/s, +with and without the Volta attention kernel (`STRATA_PROMPT_ATTN_OLD=1`): + +| Prompt tokens | old attention path | `prompt_attn_v70_kernel` | +| --- | ---: | ---: | +| 7,194 | 1,010 | 1,164 | +| 28,650 | 1,063 | 1,251 | +| 114,338 | 973 | 1,123 | + +Decode was 38-51 tok/s in every run and is not affected. The profile, the parity numbers, the needle test and the limits are in +[bench/results/2026-10-03-v100-prompt-attn](../bench/results/2026-10-03-v100-prompt-attn/README.md). + +What the kernel does and does not guarantee: against an FP64 reference its error is about 5e-6 (output scale 3.6; the FP32 kernel it replaces: 2e-6); 15 of 15 +needles were found; greedy output was identical on the 28,650- and 114,338-token prompts and differed late in the answer on the 7,194-token one (another +summation order). + +## Checking and benchmarking the Volta kernels + +```sh +cmake -S . -B build -DSTRATA_PARITY_PROMPT_ATTN=ON ... && cmake --build build --target qsa_prompt_attn_parity +CUDA_VISIBLE_DEVICES=0 ./build/qsa_prompt_attn_parity 32768 2048 5 # synthetic, no model; exit 0 = PASS +``` + +It compares the tensor-core attention with the FP32 kernel and an FP64 host reference and times both. The Q4_0 cases are skipped below +sm_80. + +## Limits + +- A layer split over two V100 was not measured with this kernel. The cards work in turn on a single request, so a split adds expert-cache room, not prompt speed. +- sm_60 (Pascal) is covered by the same flag but was not measured here. +- Volta's tensor cores take FP16 only: a model's BF16 weights are converted, and a weight outside FP16's range would saturate (none did in + the check above). diff --git a/docs/OLDER_GPUS.md b/docs/OLDER_GPUS.md new file mode 100644 index 0000000000..85479834d4 --- /dev/null +++ b/docs/OLDER_GPUS.md @@ -0,0 +1,111 @@ +# Older GPUs (experimental) + +Strata's supported cards are NVIDIA RTX 20 / 30 / 40 / 50 and the AMD cards in [AMD_HIP.md](AMD_HIP.md). The cards +below run through **opt-in, experimental** paths that community members wrote and measured on their own machines. The +maintainers have none of these cards: each path is compile-checked and unit-tested here, and the ready-made engines +and their output stay exactly as they were. Numbers are the reporters' own, on one machine each. + +## Support matrix + +### NVIDIA + +| Cards | Compute capability | How it runs | What is different on it | Reported | +| --- | --- | --- | --- | --- | +| Tesla P100 | 6.0 | the CUDA 12 engine | `__dp4a` emulated (bit-exact); BF16 projections through fp32 | not measured | +| Tesla P40 / P4, GTX 10 series | 6.1 | the CUDA 12 engine | BF16 projections through fp32 (cuBLAS has no BF16 GEMM there, #395) | P40, IQ3_S, engine 0.1.30: prompt 217-374 tok/s, decode 30-33 tok/s (#395) | +| Tesla V100, Titan V | 7.0 | the CUDA 12 engine | BF16 projections on the FP16 tensor cores (#655, #540); the prompt attention on `mma.m8n8k4` (#600); a leaner attention kernel (#540) | V100-PCIE-32GB, UD-IQ4_XS: prompt 1,123-1,251 tok/s (#600); V100 32GB, IQ2_XS: prompt +22% from #540 | +| RTX 20 (Turing) | 7.5 | **supported**, the ready-made engine | opt-in: `STRATA_BF16_TC=1` runs the BF16 projections on the FP16 tensor cores | RTX 2080 Ti, Q2_0: prompt +15-18% (#655) | + +A card needs enough VRAM to be useful: 12 GB or more is recommended, as for every card (a 2 GB GT 1030 is compute +capability 6.1 too, and cannot hold any of the model). + +### AMD + +| Cards | Architecture | How it runs | Reported | +| --- | --- | --- | --- | +| RX 6800 / 6900 series | gfx1030 | setup (`--backend hip`), unvalidated | [AMD_HIP.md](AMD_HIP.md#rdna2-gfx1030) | +| RX 6700 XT | gfx1031 | setup (`--backend hip`), unvalidated (#524) | used daily by its reporter, one card | +| RX 5500 XT (RDNA1) | gfx1012 | built by hand: `-DCMAKE_HIP_ARCHITECTURES=gfx1012` (HIP 5.7 or 7) | 8 GB card, IQ3_S, 8K prompt: 15.3 tok/s decode (#442) | +| Instinct MI50 / MI60, Radeon VII | gfx906 (wave64) | built by hand: `-DSTRATA_HIP_GFX906=ON` | 2x MI50, Coder IQ1_M, 128K context: decode 50.1 / 47.8 / 45.7 tok/s at 4K / 32K / 128K prompt tokens, prompt ~520 tok/s (#677) | + +## NVIDIA: the CUDA 12 engine + +CUDA 13 dropped Pascal and Volta: it cannot compile for them. Setup therefore keeps a **second engine**, built with +CUDA 12.9 and `-DSTRATA_EXPERIMENTAL_SM60=ON`, in its own folder (`engine-cuda12\`, beside `engine\`). One engine runs +per model, so the choice is made per model, by the oldest card that model runs on: + +- **Every card the model uses is RTX 20 or newer:** the ready-made CUDA 13 engine, as always. +- **A card is Pascal or Volta:** the CUDA 12 engine. Setup says so (`CUDA 12: sm_70 is older than CUDA 13 supports + ...`). On Windows it downloads `strata-windows-x64-cuda12.zip` with NVIDIA's CUDA 12 libraries (from pip, like the + CUDA 13 ones); on Linux, or with `--build`, it compiles the engine with a CUDA 12.x toolkit. + +### Opting in + +An older card is used only when you choose it; a PC with a newer card keeps recommending the newer one. + +| You | Setup | +| --- | --- | +| have only Pascal / Volta NVIDIA cards (and no AMD card it can use) | uses them, with the CUDA 12 engine | +| name the card: `START-HERE.bat --setup --gpu 1`, or `--gpus 0,1` with a newer card | uses it; the model gets the CUDA 12 engine | +| `--cuda 12` (or `STRATA_CUDA=12`) | the CUDA 12 engine for this model, on any card | +| `--cuda 13` | the CUDA 13 engine even with an older card (a warning: it has no code for that card) | +| `STRATA_EXPERIMENTAL_SM60=1` | the older cards are listed as usable (the setting from #295 still works) | + +The choice is kept in the model's config (`"cuda": 12`): its starts and `UPDATE.bat` keep it, and other models keep +their own engine. Setting the model up again chooses again (by its cards; add `--cuda 12` to keep a forced choice). A Pascal / Volta card added to a model at a start (`--gpus`) moves that model to the +CUDA 12 engine. + +`--cuda 12` is also the way to run Strata with an NVIDIA driver older than 580: CUDA 12 needs 528 or newer on Windows +(527.41, NVIDIA's minor-version compatibility) and 525 on Linux; setup's "driver too old" stop says so. Such old +drivers were not tested here. + +### Mixed cards + +A model that shares a V100 with an RTX 30 / 40 card runs both on the CUDA 12 engine (it has code for sm_60 to sm_89, +plus PTX). An RTX 50 card (sm_120) in a CUDA 12 engine is a warning, not a stop: CUDA 12.8 and newer compile for it, +but engines built with 12.8 crashed on long prompts there (#220, #224). Keep the RTX 50 card on its own model (`--gpu +N`), where it runs the CUDA 13 engine. + +### Building it yourself + +```sh +# Linux (Windows: the same with the toolkit's nvcc.exe) +cmake -S . -B build-cuda12 -DSTRATA_ENABLE_CUDA=ON -DSTRATA_BUILD_TESTS=OFF -DSTRATA_EXPERIMENTAL_SM60=ON \ + -DCMAKE_CUDA_ARCHITECTURES="61;70" -DCMAKE_CUDA_COMPILER=/usr/local/cuda-12.9/bin/nvcc +cmake --build build-cuda12 --target strata -j +``` + +Setup does the same when it compiles: it looks for the newest CUDA 12.x toolkit (`STRATA_NVCC=` picks +one, #601; on glibc 2.43 use 12.8, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md)). + +### What the flag changes, and what it does not + +`-DSTRATA_EXPERIMENTAL_SM60=ON` lowers the runtime floor to compute capability 6.0 and compiles the older cards' code +paths into that build only: the Volta prompt attention (#600), #540's attention kernel (used below sm_75), and the +Pascal BF16 path. The ready-made CUDA 13 engine has none of them, so its kernels and its output are unchanged. The +FP16 path for BF16 projections is in every build but runs by default only below sm_75; RTX 20 owners can try it with +`STRATA_BF16_TC=1` (`=0` turns it off on a V100). It is not bitwise the same as cuBLAS's BF16 kernel (FP16 tensor-core +sums round differently): #540 measured a mean KL of 8.4e-3 on the next-token distribution of 24 code prompts on a V100 +(the same top-1 in 23), the size of other summation-order changes; #655 a worst relative difference of 3.5e-5 per +product on an RTX 2080 Ti. + +A/B switches: `STRATA_BF16_TC=0|1`, `STRATA_PROMPT_ATTN_OLD=1` (the decode kernel for prompts), `STRATA_ATTN_PRE75=0` +(#540's kernel off). [NVIDIA_V100.md](NVIDIA_V100.md) has the V100 build, its measurements and the parity test. + +## AMD: building gfx906 and gfx1012 + +Setup does not build these; build by hand and run `serve/server.py` with a config, as on any other card. + +- **gfx906** (MI50 / MI60 / Radeon VII, wave64): a separate opt-in build, `-DSTRATA_HIP_GFX906=ON + -DCMAKE_HIP_ARCHITECTURES=gfx906` (not `STRATA_ENABLE_HIP`). Current ROCm no longer ships gfx906 libraries; the + reporter used a community ROCm 7.14 image. Recipe, kernels and measurements: [AMD_HIP.md](AMD_HIP.md#gfx906-instinct-mi50--mi60-radeon-vii-wave64-built-from-source). +- **gfx1012** (RX 5500 XT): the wave32 backend, `-DSTRATA_ENABLE_HIP=ON -DCMAKE_HIP_ARCHITECTURES=gfx1012`. HIP 5.7 + (Ubuntu's packages) works: older hipBLAS (0.x) is used through rocBLAS, the legacy HIP names and the missing + `__syncwarp` are version-gated, and RDNA1's missing signed dot4 uses llama.cpp's SDWA sequence + (`-DSTRATA_GFX1012_PORTABLE_DOT=ON`: the portable one). + +## Reports welcome + +These paths stay experimental until more people run them. A report with the card, the driver / ROCm version, the +model and the engine log (`strata-.log`) in an issue helps; [COMMUNITY_BENCHMARKS.md](COMMUNITY_BENCHMARKS.md) +has the format for measurements. diff --git a/docs/SECOND_GPU.md b/docs/SECOND_GPU.md index 179b9b3fa1..8dd82b25ff 100644 --- a/docs/SECOND_GPU.md +++ b/docs/SECOND_GPU.md @@ -84,6 +84,44 @@ The existing warning about the CUDA0 expert-cache GPU hit path still applies: its outputs diverge from cache-off runs. Treat performance as experimental until the generated tokens have been validated. +## Optional helper decode optimization + +`--remote-expert-opt` (`--serve` only) optimizes the CUDA1-3 helper caches +above. The engine's default is off; since 0.1.39b setup adds it to a config on +two or more GPUs (`--gpus`, or "use both" at start). It acts only when a helper +cache is configured; a layer split runs exactly as before. To leave it out: +setup's `--no-remote-expert-opt`, or `"remote_expert_opt": false` in the +model's `strata-*.json` (kept when setup runs again). Measured by the PR's +author: dual RTX 4090 +63% mixed / +132% code decode over the plain helper +path; RTX 5090 + 4090 +28% / +63%. The primary cache avoids admitting experts already held +by a helper, and helpers replace cold experts with frequently routed CPU +misses using their existing same-layer slots. Each helper reduces its expert +outputs to a weighted partial sum on its GPU before returning one vector per +token. Tokens with no CPU expert work skip CPU activation quantization. + +For an existing server configuration with CUDA0 as the primary and CUDA1 as a +helper, use these engine arguments alongside the model and profile arguments: + +```text +--expert-cache auto --expert-cache-device1 auto --remote-expert-opt +``` + +`--expert-cache-device1`, `--expert-cache-device2` and +`--expert-cache-device3` now also accept `auto`: fill each helper from its +assigned ranking using actual aligned expert bytes and free VRAM, retaining +the existing 512 MiB allowance. Explicit numeric budgets still work. This is +startup capacity sizing, not throughput balancing; with several helpers a +card's truncated candidate tail is not redistributed to another card. + +The optimization uses the existing host scheduling and pinned-host transport, +not P2P or tensor parallelism. Each layer still waits for its participating +helpers. It changes floating-point summation order, so enabled output is not +claimed to be bitwise identical. Only two-card CUDA operation has been measured; +three/four cards and HIP have not been validated. Without the switch, the +existing decode path remains in use. This does not optimize the separate +`--peer-device` path below or change its existing incompatibility with helper +caches. + ## Peer tier (`--peer-device`) `--peer-device N` puts a second adaptive expert cache on CUDA device N. It diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 2d040b09db..7287409e45 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -25,6 +25,14 @@ Strata is already running. Look for its window. Or another program uses the port **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. +**Linux: the engine does not compile (`unsupported GNU version`, or `exception specification is incompatible` for +`cospi`/`sinpi`/`rsqrt`).** Two known mismatches between the CUDA toolkit and a new Linux (#601): +- gcc newer than 14 (Ubuntu 26.04's default 15): CUDA 12.x and 13.0 refuse it. Install g++-14 and run + `CXX=g++-14 CUDAHOSTCXX=g++-14 ./setup.sh`. +- glibc 2.43 with CUDA 12.9: the toolkit's math headers clash with glibc's, already in CMake's first test compile. + Use CUDA 12.8 or 13.x instead. Setup takes the newest toolkit it finds; `STRATA_NVCC=/usr/local/cuda-12.8/bin/nvcc + ./setup.sh` makes it use that one (only that one). + **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). diff --git a/docs/UNSLOTH_Q4.md b/docs/UNSLOTH_Q4.md index 12d4d1997f..e275dec7cc 100644 --- a/docs/UNSLOTH_Q4.md +++ b/docs/UNSLOTH_Q4.md @@ -221,10 +221,37 @@ engine with `--short-read` covering the positions to compare; llama.cpp's side w | `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 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). | +## UD-IQ4_XS (setup from 0.1.39, #621) + +Unsloth's `UD-IQ4_XS` at the same revision is in setup too, as a regular choice (not experimental; the Unsloth +family's first size and its default): `START-HERE.bat --setup --family unsloth --model UD-IQ4_XS` (engine 0.1.38 or +newer). It sits between IQ3_S and UD-Q4_K_XL: its routed experts are IQ3_S gate/up (IQ4_XS +in one layer) with IQ4_NL downs (Q8_0 in five layers), 59.5 GB of them; the dense side (Q8_0 projections, the IQ4_NL +PLE table, a Q6_K head) is UD-Q4_K_XL's. Three shards, 93.7 GB: + +| File | Bytes | SHA-256 | +| --- | ---: | --- | +| `Qwen3.8-Flash-Next-UD-IQ4_XS-00001-of-00003.gguf` | 10,946,624 | `5ce89370720f8bf90890f439361282104c1aa1482d4013bb9a50923e758e71a4` | +| `Qwen3.8-Flash-Next-UD-IQ4_XS-00002-of-00003.gguf` | 49,835,229,856 | `577a38a2392b40ca2193cea502e1d92f60b8cd370675d308e0ec21885d9daaa7` | +| `Qwen3.8-Flash-Next-UD-IQ4_XS-00003-of-00003.gguf` | 43,836,407,744 | `d4634e6d84f0ebb0940be15c90d3790bf6464e3dea3a1cddc567dc0e83ad8833` | + +Setup treats it like UD-Q4_K_XL (the list above): the same RAM budget (your RAM less 24 GB; at most all 55 GiB of its +experts, so a PC with ~80 GB of RAM or more holds all of them), the pack with `--compat-bf16`, no `experts.bin`, one +GPU by default. It does not ask on AMD: its experts' formats have prompt kernels there too. Images are an option (asked, +off by default; NVIDIA): the image path has no restriction for this pack, which uses the original model's image +encoder; not yet run with images. + +What is known so far: it packs and runs on a Strix Halo (AMD gfx1151, 128 GB unified memory), where prompts read at +~820 tokens/s at 8K and 64K and the answers passed the retrieval checks at 8K and 64K. It has **not** been measured on +NVIDIA yet; please report what you see. One fidelity note that applies to every `--compat-bf16` pack (UD-Q4_K_XL too): +the pack rounds the file's Q8_0 hyper-connection projections to BF16, and on the Strix Halo that rounding put +perplexity 6-9% above llama.cpp's on the same file (teacher-forced, short context). Reading the Q8_0 values instead +closes most of that gap; it is measured on AMD only and not in this release. + ## Scope and validation -- Only UD-Q4_K_XL at revision `38bb39e` is targeted. Other Unsloth quantizations use formats this engine may not - have kernels for; the engine checks every layer's formats at start and refuses an unsupported one by name. +- UD-Q4_K_XL and UD-IQ4_XS at revision `38bb39e` are targeted. Other Unsloth quantizations use formats this engine may + not 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), `prefill_mmq_kquant_test` (with `-DSTRATA_MMQ_KQUANTS=ON`: the diff --git a/docs/multigpu-roadmap.md b/docs/multigpu-roadmap.md index 749b251e87..5916ccb465 100644 --- a/docs/multigpu-roadmap.md +++ b/docs/multigpu-roadmap.md @@ -6,7 +6,7 @@ Roadmap authority is GitHub Issue #1 and its child Issues. This document summari ## Current production baseline -The current promoted engine generation is **Strata 0.1.38** in this fork, integrated from upstream `99f3dbd0b21d1401b3769e0c0d963913607f380b`. The 0.1.38 compatibility gate preserves the independent-lane architecture while consuming upstream prompt/decode, reliability, security/status, profile-persistence, unbuffered-load, and optional peer-tier primitives. Upstream layer-split/peer execution remains a challenger/backend primitive rather than a replacement for request-level lanes. +The current promoted engine generation is **Strata 0.1.38** in this fork, integrated from upstream `99f3dbd0b21d1401b3769e0c0d963913607f380b`. A **validated 0.1.39 sync candidate** exists on `sync/upstream-0.1.39` at `3727c6bdfb12ba2ffc07402b68ae86c0ed2ac27f`, merging upstream `6f32ec070f23ced9f50e704d854d775da52591ab`; it is not promoted until main moves. The candidate preserves the independent-lane architecture while consuming upstream batching, Responses API, conversation-cache telemetry, decode/kernel, CPU-pool, expert-cache, low-RAM/file-tier, and layer-split improvements. Ordinary lanes strip inherited upstream internal batching just as they already strip inherited layer-split/peer execution, so those mechanisms remain explicit challenger/backend primitives rather than accidental replacements for request-level lanes. The architectural baseline is: @@ -156,7 +156,7 @@ Measure the crossover rather than assuming aggregation wins: single-request TTFT Do **not** begin with dynamic GPU bonding. Reopen bond/unbond policy only if the static `1+1+1` versus `2+1` experiment demonstrates a repeatable workload-dependent crossover large enough to justify topology changes. Any dynamic version must retain a clean independent-lane fallback and must not migrate an active session merely to rebalance GPUs. -**Current status on Strata 0.1.38 / the reference host: deferred, evidence-negative as an always-on topology, but intentionally left open.** The matched static probe preserved the known single-request benefit (~99 tok/s for the two-GPU Super-Lane versus ~68 tok/s for an ordinary lane), but `2+1` did not establish a useful M=2 aggregate advantage and regressed M=3 aggregate goodput by about 15% versus `1+1+1`. This does not reject upstream layer split as a primitive. Reopen with a short single-request smoke, then M=2, then M=3 only if upstream materially improves layer-split decode/prefill, handoff/staging, split-auto placement, queueing/FIFO behavior, or another relevant execution path enough to move the crossover. +**Current status on the Strata 0.1.39 sync candidate / reference host: reopened as a serious default-topology challenger; not yet promoted.** Upstream 0.1.39 materially changed the layer-split server through batch slots, pipeline groups and stage-weight trimming. The fixed three-GPU `18,34` pipeline with `--batch 3 --batch-groups 3 --trim-stage-weights` and the shared expert arena now beats independent lanes in the measured fixed decode region at both M=2 (**147.84 ± 2.26 vs 143.19 ± 3.06 tok/s, +3.25%**) and M=3 (**209.66 ± 4.02 vs 192.16 ± 4.11, +9.11%**). The retained M=1 layer-split control is also much faster than one lane, although that point was not rerun with the exact pipeline config. Cold-prefill is the important counterexample: ~15K x3 strongly favors lanes (**5901.34 vs 3289.19 tok/s**), while ~110K x3 narrowly favors the pipelined split (**6028.09 vs 5822.71 tok/s**). The next gate is therefore no longer more fixed microbenchmarks; it is mixed prompt/output lengths, session behavior, queue/tail latency, power, failure isolation and operational flexibility under the always-on three-GPU layer-split topology. Full evidence: [`strata-0.1.39-performance-crossover-20261005.md`](strata-0.1.39-performance-crossover-20261005.md). ### Elastic Super-Lane lifecycle @@ -247,10 +247,10 @@ Until measurements justify them, this roadmap does **not** assume that Strata sh ## Near-term order -1. Keep the completed Phase 1/2 serving-control and Phase 3 lifecycle gates as regression controls on the promoted 0.1.38 software baseline; retain the measured 0.1.30/0.1.31 evidence under its original engine generation. +1. Keep the completed Phase 1/2 serving-control and Phase 3 lifecycle gates as regression controls on the promoted 0.1.38 software baseline. The validated 0.1.39 sync candidate has passed the bounded compatibility gate and should remain a candidate until explicitly promoted; retain measured 0.1.30/0.1.31/0.1.38 evidence under its original engine generation. 2. Do not add another mandatory serving phase without a measured residual. -3. Keep the completed decode-assist and static `2+1` measurements as **0.1.38 evidence-negative / deferred challengers**, not permanent rejections. Do not repeat the same reference-host sweeps without a material upstream, hardware, or workload change. -4. Reopen decode assist or static Super-Lane with the smallest staged smoke that can show the crossover moved; only then repeat broader concurrency or lifecycle campaigns. Elastic Super-Lane remains gated on a successful future static crossover; cross-lane migration stays later. +3. Keep decode assist as **0.1.38 evidence-negative / deferred**, but treat static layer split / Super-Lane as **reopened on 0.1.39** because the measured batch-group pipeline now crosses independent lanes in some decode and very-long-prefill regions. Do not collapse the result into a universal winner claim. +4. Before any topology promotion, test mixed prompt/output lengths, queue/tail behavior, power, failure-domain cost and the drain/reconfigure/restore lifecycle against the independent-lane fallback. Elastic Super-Lane remains gated on this broader static crossover holding after reconfiguration cost; cross-lane migration stays later. 5. Treat upstream helper, peer, layer-split and load/unload controls as distinct execution/lifecycle primitives: helper offload is valid without P2P, peer execution is deferred on hardware without a validated P2P path, and neither automatically replaces independent lanes. The default bias remains deliberate simplicity: add coupling only when measurements show it buys something. diff --git a/docs/multigpu-shared-runtime.md b/docs/multigpu-shared-runtime.md index 18c1cae194..e77acc6691 100644 --- a/docs/multigpu-shared-runtime.md +++ b/docs/multigpu-shared-runtime.md @@ -17,7 +17,7 @@ Detailed reference-host hardware, tuning values, benchmark tables, and validatio ## Shared arena -Strata 0.1.38 retains the file-backed arena primitive introduced upstream in 0.1.30 through `--shared-expert-arena FILE` (originating from this fork's upstream PR #129). On Linux, upstream `PinnedArena` maps one shared file with a 4 KiB header containing the arena geometry and pack fingerprint; incompatible size or pack identity is refused before the arena is used. The ordinary anonymous/hugetlb path remains unchanged when the option is absent. The Linux whole-arena pinning behavior used by the lane runtime remains intact; the WDDM shared-memory cap is Windows-specific unless explicitly overridden with `STRATA_ARENA_PIN_GIB`. +The validated Strata 0.1.39 sync candidate retains the file-backed arena primitive introduced upstream in 0.1.30 through `--shared-expert-arena FILE` (originating from this fork's upstream PR #129). On Linux, upstream `PinnedArena` maps one shared file with a 4 KiB header containing the arena geometry and pack fingerprint; incompatible size or pack identity is refused before the arena is used. The ordinary anonymous/hugetlb path remains unchanged when the option is absent. The Linux whole-arena pinning behavior used by the lane runtime remains intact; the WDDM shared-memory cap is Windows-specific unless explicitly overridden with `STRATA_ARENA_PIN_GIB`. The lane supervisor composes that upstream primitive rather than intercepting `mmap`: it strips inherited shared-arena options, adds the same explicit `--shared-expert-arena` path to every shared lane, and removes the option entirely for the private-arena benchmark arm. The default backing lives under `/dev/shm`, matching upstream's tmpfs contract; an explicit `--arena-file` selects an operator-managed path. Automatically chosen backing files are removed on graceful supervisor exit, while explicit backing-file lifetime remains operator-owned. @@ -76,6 +76,18 @@ The proxy also exposes `X-Strata-Lane-Index`, `X-Strata-Admission-Rank`, `X-Stra `balanced-additive-new-prefill-retained-state-proxy-v1` is the promoted Phase-2B default, while `safe-affinity-live-state-v1` remains an explicit rollback control; either may run without persistent benchmark tracing. Other placement policies remain experimental and require `--bench-trace-jsonl` when selected through `--bench-scheduler-policy`. All policies preserve existing-session affinity, health/capability filtering, one active generation per lane, affinity reservations, vision reservation and compatible FIFO ordering; they can only choose differently among idle lanes for a new session. The trace retains the historical `placement_key` / `selected_placement_key` fields for compatibility and adds the active `placement_score` / `selected_placement_score`, policy name, scope and proxy source. Policies that use the last completed request byte footprint say `retained-state-proxy` in their names and report `retained_last_completed_request_bytes_v1`; this is routing-state evidence, not engine-truth active load or compute cost. The promoted balanced-additive policy orders new-session candidates first by remembered affinity-session count and then by additive estimated new-prefill plus retained-state cost. Persistent-supervisor validation on the reference host kept all six measured waves at 2/2/2 session placement across two independent campaigns, while the safe and unbalanced additive controls each developed 6-to-1 concentration on later waves; exact measurements are retained in the public recipe repository. +## Validated 0.1.39 sync candidate + +The `sync/upstream-0.1.39` branch merges upstream Strata **0.1.39** (`6f32ec070f23ced9f50e704d854d775da52591ab`) into the fork at merge commit `3727c6bdfb12ba2ffc07402b68ae86c0ed2ac27f`. Main remains on the promoted 0.1.38 baseline until an explicit promotion. + +The candidate keeps one ordinary whole-GPU engine per lane. Because upstream 0.1.39 adds engine-internal request batching and layer-split batch groups, the supervisor removes inherited top-level `parallel` and CLI `--batch`, `--slots`, `--batch-groups`, and `--trim-stage-weights` from ordinary lane configs, in addition to the existing layer-split/peer stripping. This prevents a copied upstream config from silently changing the Lanes concurrency unit from one request/session per independent engine into nested batching. A future hybrid-lane experiment must opt into that coupling explicitly. + +The supervisor also treats `/v1/responses` as generation work so the OpenAI Responses API follows the same admission, busy-lane exclusion, and lane selection path as Chat Completions and Anthropic Messages. Parking telemetry no longer extends the engine `DONE` record in this sync; upstream 0.1.39's native conversation-cache monitor owns that field space, avoiding the former semantic collision with upstream's appended offloaded-expert counter. + +The bounded 0.1.39 compatibility gate passed **333 Python serving tests with 7 skipped**, a fresh **CUDA 13.4 sm_120 Release `strata` build** with `STRATA_BUILD_TESTS=OFF`, a second fresh Release build with `STRATA_BUILD_TESTS=ON` that linked the full `strata` executable, and focused `pinned_shared_test`, `expert_profile_save_test`, and `file_expert_source_test` CTests. A live IQ3_XXS single-lane smoke generated successfully on GPU 0. A live three-lane 32K compatibility smoke then routed three simultaneous requests one each to GPUs 0/1/2; lane 0 populated the ~39.97 GiB shared expert arena once, while lanes 1 and 2 reported `shared population ready; source load skipped`. These are promotion/compatibility checks, not a new performance campaign. + +A later bounded reference-host topology campaign exercised the 0.1.39 execution challenger explicitly. Ordinary lanes still strip nested batch/layer-split flags by default, but a separate upstream-native three-GPU layer-split server using explicit split `18,34`, `--batch 3 --batch-groups 3 --trim-stage-weights`, and the same shared expert backing measured **147.84 ± 2.26 tok/s** at M=2 versus **143.19 ± 3.06** for two lanes and **209.66 ± 4.02 tok/s** at M=3 versus **192.16 ± 4.11** for three lanes. Cold-prefill crosses by length: lanes lead at ~15K x3 (**5901.34 vs 3289.19 tok/s**), while the pipelined split narrowly leads at ~110K x3 (**6028.09 vs 5822.71 tok/s**). This is challenger evidence, not a change to the ordinary lane contract; details and caveats are in [`strata-0.1.39-performance-crossover-20261005.md`](strata-0.1.39-performance-crossover-20261005.md). + ## Current promoted engine baseline The shared-lane runtime is based on upstream Strata **0.1.38**, integrated from upstream commit `99f3dbd0b21d1401b3769e0c0d963913607f380b`. The 3-lane reference contract remains one engine per GPU, 262144 context and 32768 resident KV per lane, disjoint CPU partitions, asymmetric per-lane PCIe tuning, and one upstream-native shared expert arena. diff --git a/docs/strata-0.1.39-performance-crossover-20261005.md b/docs/strata-0.1.39-performance-crossover-20261005.md new file mode 100644 index 0000000000..c82b6c59a8 --- /dev/null +++ b/docs/strata-0.1.39-performance-crossover-20261005.md @@ -0,0 +1,44 @@ +# Strata 0.1.39 topology crossover evidence — 2026-10-05 + +## Status + +This record belongs to the validated but not-yet-promoted Strata 0.1.39 sync candidate. It records only the measurements needed for the current topology decision; concrete raw evidence and supporting controls live in the public recipe repository. + +Measured source: `d51d7e9cbc327f90c2fd59b1e20a949033f594e2` on `sync/upstream-0.1.39`, upstream Strata `6f32ec070f23ced9f50e704d854d775da52591ab` (v0.1.39). + +## Decode concurrency + +The strongest measured three-GPU upstream-native challenger uses explicit split `18,34`, `--batch 3 --batch-groups 3 --trim-stage-weights`, and the shared expert arena. + +| Concurrent requests | Independent lanes | Three-GPU layer split | Result | +| ---: | ---: | ---: | --- | +| 1 | 73.63 ± 1.67 tok/s | **120.62 ± 1.24 tok/s**¹ | layer split +63.8% | +| 2 | 143.19 ± 3.06 tok/s | **147.84 ± 2.26 tok/s** | layer split +3.25% | +| 3 | 192.16 ± 4.11 tok/s | **209.66 ± 4.02 tok/s** | layer split +9.11% | + +¹ M=1 is the retained same-binary three-GPU layer-split single-request control. The exact batch3/groups3/trim config was not rerun at M=1. M=2 and M=3 are exact measurements of the fixed pipelined config. + +The M=2 engine remained in `strata batch (pipelined, 3 groups of 1)`; with only two active requests one group is unused. Both requests completed together at roughly 6.9 s mean E2E, not as a FIFO staircase. + +The old three-request FIFO result is therefore only a serial control, not the 0.1.39 layer-split concurrency ceiling. + +## Cold-prefill crossover + +Three cold prompts were submitted simultaneously and every retained request reported `cache_n=0`. + +| Prompt regime | Independent lanes | Pipelined layer split | Result | +| --- | ---: | ---: | --- | +| ~15K x3 | **5901.34 ± 55.16 tok/s** | 3289.19 ± 18.83 | lanes +79.4% | +| ~110K x3 | 5822.71 ± 4.49 | **6028.09 ± 14.50** | layer split +3.5% | + +The ~15K region favors independent request-level prefill. At ~110K the intra-prompt chunk pipeline is saturated enough to cross back slightly in favor of layer split. + +The 15K/110K arms are workload/length matched but not byte-identical because the unique nonce encoded the topology arm. + +## Decision impact + +The 0.1.39 evidence no longer supports treating independent lanes as the general performance winner for concurrent decode. The fixed three-GPU layer-split server is ahead in the measured M=2 and M=3 decode regions, while medium multi-cold-prefill remains a large independent-lane advantage and very-long cold prefill crosses back toward layer split. + +This reopens static layer split / Super-Lane as a serious production-topology candidate, but it does not yet change the default contract. Remaining promotion gates are workload mix, unequal prompt/output lengths, session behavior, queue/tail latency, failure isolation, power, and operational flexibility. + +Public recipe record: https://github.com/rhgo1749/qwen3.8-flash-next-strata-gpu-per-lane-recipe/blob/main/docs/strata-0.1.39-performance-crossover-20261005.md diff --git a/include/strata/core/conversation_cache.hpp b/include/strata/core/conversation_cache.hpp index 0a190c709e..b105deb60c 100644 --- a/include/strata/core/conversation_cache.hpp +++ b/include/strata/core/conversation_cache.hpp @@ -53,13 +53,72 @@ struct ConversationKvReuse { std::vector kv; // Original image extent for validation, and the earliest subsequent rewrite. int64_t captured_tokens = 0, unchanged_tokens = 0; + // With a layer split: the later stages' own retained K/V, one per stage, same extents (empty: none) + std::vector stages; size_t bytes() const { - size_t n = kv.capacity() * sizeof(ConversationKv); + size_t n = kv.capacity() * sizeof(ConversationKv) + stages.capacity() * sizeof(ConversationKvReuse); for (const auto& layer : kv) n += layer.bytes(); + for (const auto& s : stages) n += s.bytes(); return n; } }; +/// Layer-split parking: each running checkpoint holds the first stage's state and, in `stage_parts`, one part per +/// later stage; a parked conversation keeps them in one image per stage. `split` MOVES the checkpoints apart (no +/// running state is copied): the complete ones (a part for each of `stages` stages) into `stage0` and `parts[k]`, +/// every part carrying the checkpoint's ids / images / LRU stamp, the others aside; `merge` puts them back in their +/// original order. merge(split(x)) == x. +struct ConversationCheckpointSplit { + std::vector stage0; + std::vector> parts; ///< [stage][checkpoint] + std::vector rest; ///< not split (no complete set of stage parts) + std::vector complete; ///< per original position: in stage0/parts or in rest +}; + +inline ConversationCheckpointSplit conversation_checkpoints_split(std::vector&& checks, + size_t stages) { + ConversationCheckpointSplit out; + out.parts.resize(stages); + for (auto& c : checks) { + const bool whole = c.stage_parts.size() == stages; + out.complete.push_back(whole); + if (!whole) { out.rest.push_back(std::move(c)); continue; } + for (size_t k = 0; k < stages; ++k) { + ConversationCheckpoint part = std::move(c.stage_parts[k]); + part.ids = c.ids; part.imgs = c.imgs; part.used = c.used; part.stage_parts.clear(); + out.parts[k].push_back(std::move(part)); + } + c.stage_parts.clear(); + out.stage0.push_back(std::move(c)); + } + checks.clear(); + return out; +} + +/// false (and `out` empty) when the parts do not line up: a stage with another number of checkpoints, or a part +/// whose ids differ from its checkpoint's. +inline bool conversation_checkpoints_merge(ConversationCheckpointSplit&& split, std::vector& out) { + out.clear(); + for (const auto& p : split.parts) + if (p.size() != split.stage0.size()) return false; + size_t whole = 0, other = 0; + for (bool c : split.complete) (c ? whole : other) += 1; + if (!split.complete.empty() && (whole != split.stage0.size() || other != split.rest.size())) return false; + if (split.complete.empty()) split.complete.assign(split.stage0.size(), true); // a restored image: all whole + for (size_t j = 0; j < split.stage0.size(); ++j) + for (const auto& p : split.parts) + if (p[j].ids != split.stage0[j].ids) return false; + size_t w = 0, r = 0; + for (bool c : split.complete) { + if (!c) { out.push_back(std::move(split.rest[r++])); continue; } + ConversationCheckpoint cp = std::move(split.stage0[w]); + for (auto& p : split.parts) cp.stage_parts.push_back(std::move(p[w])); + ++w; + out.push_back(std::move(cp)); + } + return true; +} + struct SavedConversation { // Runtime compatibility only; NOT a model/weights identity or disk schema. std::array geometry{}; @@ -69,10 +128,13 @@ struct SavedConversation { std::vector checkpoints; std::vector kv; // main layers followed by the draft layer bool cvec = true; + // with a layer split, the later stages' own images, one per stage, in stage order + std::vector stage_images; size_t bytes() const { size_t n = live.bytes() + checkpoints.capacity() * sizeof(ConversationCheckpoint) + kv.capacity() * sizeof(ConversationKv); + for (const auto& s : stage_images) n += s.bytes(); for (const auto& c : checkpoints) n += c.bytes(); for (const auto& k : kv) n += k.bytes(); return n; @@ -110,13 +172,17 @@ class ConversationCache { // Retain only the restored K/V buffers, not duplicate running checkpoints. // This optimization never evicts a parked conversation to make itself fit. - void retain(std::vector&& kv, int64_t tokens) { + // `stage_kv`: with a layer split, the later stages' restored K/V (one per stage), retained with the first's. + void retain(std::vector&& kv, int64_t tokens, + std::vector>&& stage_kv = {}) { reuse_ = {}; - ConversationKvReuse candidate{std::move(kv), tokens, tokens}; + ConversationKvReuse candidate{std::move(kv), tokens, tokens, {}}; + for (auto& k : stage_kv) candidate.stages.push_back(ConversationKvReuse{std::move(k), tokens, tokens, {}}); if (enabled() && candidate.bytes() <= budget_ - bytes_) reuse_ = std::move(candidate); } void limit_reuse(int64_t first_dirty) { reuse_.unchanged_tokens = std::min(reuse_.unchanged_tokens, first_dirty); + for (auto& s : reuse_.stages) s.unchanged_tokens = std::min(s.unchanged_tokens, first_dirty); if (reuse_.unchanged_tokens <= 0) reuse_ = {}; } ConversationKvReuse take_reuse() { return std::exchange(reuse_, {}); } diff --git a/include/strata/core/conversation_snapshot.hpp b/include/strata/core/conversation_snapshot.hpp index a840bf2eda..1a56680df3 100644 --- a/include/strata/core/conversation_snapshot.hpp +++ b/include/strata/core/conversation_snapshot.hpp @@ -76,4 +76,21 @@ ConversationRestore conversation_snapshot_restore(const SavedConversation& image const ModelGeometry& g, const QsaState& draft, std::string& error); +// The same with `draft == nullptr`: an image WITHOUT the draft layer's K/V (kv holds the session's own QSA layers +// only). A layer split's later stages park this way; the draft ring is saved once, with the first stage's image. +// An image is validated and restored with the same kind of call it was saved with (a K/V layer count mismatch is +// rejected as invalid). +bool conversation_snapshot_bytes(const ConversationView& view, const SessionState& session, const ModelGeometry& g, + const QsaState* draft, size_t& bytes, std::string& error); +bool conversation_snapshot_capture_bytes(const ConversationKvReuse& reuse, const ConversationView& view, + const SessionState& session, const ModelGeometry& g, const QsaState* draft, + size_t& bytes, std::string& error); +bool conversation_snapshot_save(SavedConversation& image, const ConversationView& view, const SessionState& session, + const ModelGeometry& g, const QsaState* draft, std::string& error, + ConversationKvReuse reuse = {}, size_t* reused_bytes = nullptr); +bool conversation_snapshot_validate(const SavedConversation& image, const SessionState& session, + const ModelGeometry& g, const QsaState* draft, std::string& error); +ConversationRestore conversation_snapshot_restore(const SavedConversation& image, SessionState& session, + const ModelGeometry& g, const QsaState* draft, std::string& error); + } // namespace strata::core diff --git a/include/strata/core/expert_cache.hpp b/include/strata/core/expert_cache.hpp index 4a708355f9..8c031ff307 100644 --- a/include/strata/core/expert_cache.hpp +++ b/include/strata/core/expert_cache.hpp @@ -16,14 +16,10 @@ // * `h_layer` held-out is **0.0456** - only 4.6% of (layer, token) pairs have all ten experts resident - so // this cannot be a per-layer grouped kernel and the three-way split is not an optimisation. // -// **WHAT THIS FILE IS AND IS NOT, TODAY.** It is the SLOT STORAGE and the RESIDENCY TABLE: it allocates the -// VRAM, fills it from the host arena, and answers `(layer, expert) -> slot or -1`. It does **not** yet compute -// anything - `moe_hit_grouped_s2` does not exist and the hit/miss split is not wired into the graph - so with -// the cache on and nothing consuming it, **the engine is slower by the fill cost and faster by nothing.** -// That is stated here rather than discovered from a benchmark, and it is why `--expert-cache` defaults to 0. -// -// The order of work is `Memory/R4-design-note.md` §7: slots and residency first, then the split, then the -// kernel. This is that first step, and the step it unblocks is the one that can be measured. +// **WHAT THIS FILE IS.** The SLOT STORAGE and the RESIDENCY TABLE: it allocates the VRAM, fills it from the host +// arena, and answers `(layer, expert) -> slot or -1`. It computes nothing itself: the GPU computes the experts it +// holds in the verify window's grouped kernels and the prompt path (the hit/miss split), and the CPU pool the rest. +// (Written when nothing consumed the slots yet and `--expert-cache` defaulted to 0; setup's configs use `auto`.) #pragma once #include @@ -93,13 +89,44 @@ class ExpertCache { void close(); bool valid() const { return base_ != nullptr; } - int64_t slots() const { return slots_; } + /// The slots usable now: all of them, unless `shrink` gave the tail's VRAM back (#533) - then the ones wholly + /// inside the VRAM still backed, always a prefix. The prompt path's loan (the last slots) and every report + /// follow this; `full_slots()` is what `open` allocated. + int64_t slots() const { return live_slots_; } + int64_t full_slots() const { return slots_; } /// Slots actually claimed. Not the same as `slots()` - the cache does not evict, so a run that routes /// fewer distinct experts than there are slots leaves the rest empty. int64_t resident() const { return per_layer_ ? admitted_ : next_free_; } - int64_t bytes() const { return off_.empty() ? slots_ * blob_ : (int64_t) off_.back(); } + /// The bytes of `slots()` (all of the arena unless shrunk); `full_bytes()`: of every slot. + int64_t bytes() const { return slot_end(live_slots_); } + int64_t full_bytes() const { return slot_end(slots_); } double gib() const { return (double) bytes() / 1073741824.0; } + /// **#533: HOT VRAM RESIZE, OPT-IN (--vram-elastic).** With a segment size set before `open`/`open_sized`, the + /// arena is one reserved address range backed by physical segments of that size (CUDA virtual memory + /// management) instead of one cudaMalloc, so `shrink` can give the tail's VRAM back to the driver - for another + /// program - and `grow` can take it again, while every slot keeps its address (the captured graphs, the verify + /// plan's pointers and the prompt path's loan all hold addresses into it). 0 (the default): one cudaMalloc, as + /// always. CUDA only: a HIP build refuses it at open. The slots past `slots()` after a shrink must hold no + /// resident expert and no loan: the CALLER evicts them first (they become CPU misses). + void set_segment_bytes(int64_t seg_bytes) { seg_req_ = seg_bytes > 0 ? seg_bytes : 0; } + bool segmented() const { return !segs_.empty(); } + int64_t segment_bytes() const { return seg_; } + /// Bytes of the arena backed by VRAM now (a whole number of segments; the arena when not segmented). + int64_t mapped_bytes() const; + /// Unmaps every segment past the first `keep_bytes` (rounded UP to a segment boundary): `slots()` becomes the + /// slots wholly inside what stays. Waits for the device first. False with `err` when not segmented or a driver + /// call failed. + bool shrink(int64_t keep_bytes, std::string& err); + /// Maps segments again up to `want_bytes` (rounded DOWN to a segment, at most the arena; the last, shorter + /// segment only when `want_bytes` covers the arena). Stops at the first segment the driver cannot back (false, + /// `err`: what was mapped by then stays). The new slots are empty until the caller fills them. + bool grow(int64_t want_bytes, std::string& err); + /// The number of leading slots that fit wholly inside the first `bytes` bytes. + int64_t slots_within(int64_t bytes) const; + /// The bytes the first `n` slots span. + int64_t bytes_of(int64_t n) const { return slot_end(n < 0 ? 0 : n > slots_ ? slots_ : n); } + /// `(layer, expert)` -> slot index, or `kNotResident`. Bounds-checked: a bad layer or expert returns /// `kNotResident` rather than reading whatever is adjacent in the table. int32_t slot_of(int64_t layer, int64_t expert) const; @@ -108,6 +135,13 @@ class ExpertCache { /// sweep) and a placeholder policy would set the hit rate that everything downstream is then sized against. int32_t admit(int64_t layer, int64_t expert); + /// Publish a same-layer replacement after its slot copy has completed. + void replace(int64_t layer, int32_t old_expert, int32_t new_expert) { + auto& old = residency_[(size_t) layer * n_expert_ + old_expert]; + residency_[(size_t) layer * n_expert_ + new_expert] = old; + old = kNotResident; + } + /// **R4.2g: GIVE EACH LAYER ITS OWN SLOTS. ROUND 328 MEASURED WHY THE GLOBAL FORM CANNOT WORK.** /// /// `admit`'s original policy hands out slots in ARRIVAL ORDER from one counter shared by every layer @@ -164,7 +198,17 @@ class ExpertCache { uint8_t* blocking_staging_ = nullptr; std::size_t blocking_staging_bytes_ = 0; #endif + int64_t slot_end(int64_t n) const { return off_.empty() ? n * blob_ : (int64_t) off_[(size_t) n]; } + bool open_segmented(uint64_t want, std::string& err); + void release_segmented(); uint8_t* base_ = nullptr; + int64_t live_slots_ = 0; ///< #533: slots() - all of them unless shrunk + int64_t seg_req_ = 0; ///< #533: the segment size asked for (0: one cudaMalloc) + int64_t seg_ = 0; ///< #533: the segment size used (a multiple of the driver's granularity) + uint64_t reserved_ = 0; ///< #533: the reserved address range's size + std::vector segs_; ///< #533: each segment's physical handle (0: unmapped) + std::vector seg_size_; ///< #533: each segment's size (the last one may be shorter) + int64_t mapped_segs_ = 0; ///< #533: segments [0, mapped_segs_) are backed std::vector residency_; ///< [n_layers * n_expert] -> slot or kNotResident int64_t slots_ = 0; int64_t n_layers_ = 0; diff --git a/include/strata/core/expert_source.hpp b/include/strata/core/expert_source.hpp index 3b0e07dab2..fb46359bf4 100644 --- a/include/strata/core/expert_source.hpp +++ b/include/strata/core/expert_source.hpp @@ -65,6 +65,22 @@ struct CgroupMemoryStat { /// Returns false when the required memory.stat counters were unavailable. bool cgroup_available_bytes(uint64_t limit, const CgroupMemoryStat& stat, uint64_t& bytes); +/// #633: the RAM this process can get. `available`: MemAvailable (Windows: the available physical memory), lowered +/// to the room under the tightest cgroup limit; `cgroup_limit`: that tightest limit itself (v2 memory.max of the group +/// and its ancestors, or v1 memory.limit_in_bytes), ~0 when there is none - what a container can never exceed. +struct HostMemory { + uint64_t available = 0; + uint64_t cgroup_limit = ~uint64_t{0}; +}; + +/// Linux reads `meminfo`, `self_cgroup` and the cgroup tree under `cgroup_root` (the parameters are for tests; the +/// defaults are the real files). cgroup v2 as before (an unreadable limit of a group that has one fails); cgroup v1's +/// memory controller (`/memory/`: memory.limit_in_bytes - memory.usage_in_bytes); no cgroup line at all is +/// MemAvailable alone. False when the RAM cannot be determined. +bool host_available_memory(HostMemory& m, const std::string& meminfo = "/proc/meminfo", + const std::string& self_cgroup = "/proc/self/cgroup", + const std::string& cgroup_root = "/sys/fs/cgroup"); + /// Build compact offsets for experts absent from both the primary GPU cache and an optional second GPU tier. /// Kept CPU-only so selection and byte accounting can be tested without initializing a GPU. bool make_cache_complement_plan( @@ -122,6 +138,11 @@ class ExpertSource { virtual void begin_layer(int64_t layer, const int32_t* ids, int64_t k) { (void) layer; (void) ids; (void) k; } /// Plan v0.3 P6: the DEVICE address of a pinned, mapped blob (the GPU can read it over PCIe), or null. virtual const uint8_t* device_alias(int64_t layer, int64_t expert) const { (void) layer; (void) expert; return nullptr; } + /// A file-backed source: start reading this expert's pages now (it will be needed by the CPU); no-op elsewhere. + virtual void prefetch(int64_t layer, int64_t expert) { (void) layer; (void) expert; } + /// A file-backed source: this expert lives in VRAM, so its pages need not stay in RAM - hand them back to the + /// kernel (a later read re-reads the file; no result depends on it). Returns the bytes released; 0 elsewhere. + virtual uint64_t release(int64_t layer, int64_t expert) { (void) layer; (void) expert; return 0; } /// Whether the verify window may give the GPU a PCIe share of this layer's misses at all (each expert is still /// checked with `pinned`). The arena answers per layer through its expert 0; the resident RAM mode's compact /// copy has no expert 0 when the GPU cache holds it, so it answers for the whole copy. @@ -331,6 +352,9 @@ struct ExpertDispatch { GpuPlanSink* plan = nullptr; int pcie_num = 0; int64_t pcie_experts = 0; ///< distinct experts the GPU read over PCIe in verify windows + /// #588: routed (token, expert) entries the GPU computed from outside its cache in verify windows: read over PCIe + /// (--pcie-frac, kind 1) or on another GPU (kind 2). In neither cache_hits nor cache_refused. + int64_t offload_entries = 0; double ms_plan = 0, ms_actq = 0, ms_jobs = 0, ms_run = 0; ///< verify-window dispatch sections /// Plan v0.3 P6: decayed routing counts per (layer, expert) during decode (sized by the caller; empty = off), /// which the driver uses to swap the most-routed missing experts into the VRAM tier between rounds. @@ -485,8 +509,14 @@ class FileExpertSource : public ExpertSource { /// beside `ram_bytes` (the RAM budget), cached now or not (their mapped pages would land in the working set); see /// experts_unbuffered. The mapped reads' page faults are one small request each, and the pages they bring in /// take the RAM the budget was sized for. `why` says what decided. + /// #577: `ram_bytes` counts up to every expert, and only the experts outside it are what the cache must keep. bool set_unbuffered(uint64_t ram_bytes, std::string& why); + /// #577: the same decision once the RAM copy is built (pin_cache_complement), from the RAM it really holds and the + /// expert bytes outside it; switches either way (startup only, nothing reading). Returns whether unbuffered. + bool recheck_unbuffered(std::string& why); bool unbuffered() const { return !direct_.empty(); } + /// Every expert's bytes (n_layers x n_expert blobs). + uint64_t expert_bytes() const; /// #286, unbuffered: assembles the blobs of these pairs ahead of the `blob` calls that will ask for them (the /// GPU cache's fill from the profile) - one batch of reads instead of one blob at a time. At most 64 pairs. void prefetch_pairs(const std::pair* pairs, int64_t n); @@ -529,6 +559,7 @@ class FileExpertSource : public ExpertSource { /// #286: the blobs from the drive, unbuffered: every role's 4 KiB-aligned window is read at once (overlapped) /// into this thread's aligned buffer, then copied into place. False when a read fails. bool read_direct(const Fill* fills, size_t n) const; + bool open_direct(std::string& why); std::vector paths_; ///< the mapped files, as maps_ std::vector direct_; ///< #286: per file, an unbuffered overlapped handle (Windows) std::vector role_file_; ///< 3 x n_layers: index into maps_ / direct_ @@ -647,10 +678,14 @@ class ArenaExpertSource : public ExpertSource { int64_t reads() const { return reads_; } bool pinned(int64_t layer, int64_t expert) const override; const uint8_t* device_alias(int64_t layer, int64_t expert) const override; + void prefetch(int64_t layer, int64_t expert) override; + uint64_t release(int64_t layer, int64_t expert) override; /// What backing was obtained and why, for the startup print. "The engine adapts to the machine it is on" is /// only true if the engine says what it got. const std::string& note() const { return note_; } + /// #633: set by `open` when the RAM available is less than the arena needs (a recommendation; it still loads) + const std::string& ram_warning() const { return ram_warning_; } double load_gib_per_second() const { return gib_per_s_; } // Loader fix: the load, split. `load_seconds()` is the wall clock of the load loop; the other two are // sums over the reader threads (see LoadStats), so on their own they say how much of that wall was spent @@ -662,6 +697,8 @@ class ArenaExpertSource : public ExpertSource { private: void* arena_ = nullptr; ///< the PinnedArena, owned + void* map_ = nullptr; ///< STRATA_ARENA_MMAP: the arena file, mapped read-only (not the PinnedArena) + uint64_t map_bytes_ = 0; std::vector dev_slice_; ///< device alias of each registered slice (or of the whole range) uint64_t slice_bytes_ = 0; const uint8_t* base_ = nullptr; @@ -669,6 +706,7 @@ class ArenaExpertSource : public ExpertSource { int64_t n_expert_ = 0; int64_t reads_ = 0; std::string note_; + std::string ram_warning_; double gib_per_s_ = 0.0; double load_seconds_ = 0.0; double load_read_s_ = 0.0; diff --git a/include/strata/core/layer.hpp b/include/strata/core/layer.hpp index a8965da133..aec6e9eb7c 100644 --- a/include/strata/core/layer.hpp +++ b/include/strata/core/layer.hpp @@ -168,6 +168,9 @@ bool moe_layer(const WeightTable& tables, const ModelGeometry& g, int64_t layer, /// rung, so a host that sees it knows `h_x_f`/`h_ids`/`h_weights` are in place. bool moe_route(const WeightTable& tables, const ModelGeometry& g, int64_t layer, int64_t k, const MoEBuffers& b, const float* x, void* stream, std::string& err, const Doorbell* db = nullptr); +/// The verify window's n tokens routed at once (see layer.cpp); bitwise per token what `moe_route` gives. +bool moe_route_window(const WeightTable& tables, const ModelGeometry& g, int64_t layer, int64_t k, const MoEBuffers& b, + const float* x, float* logits, int32_t* ids, float* weights, int n, void* stream, std::string& err); /// The finishing half: the shared expert and the combination. /// diff --git a/include/strata/core/mtp.hpp b/include/strata/core/mtp.hpp index e2084f6d28..bfb6ee0f57 100644 --- a/include/strata/core/mtp.hpp +++ b/include/strata/core/mtp.hpp @@ -82,6 +82,7 @@ class MtpDrafter { /// Coupled mode with penalties: the history the next draft() chain starts from - what the session holds after the /// window's commit (`tail`) and the window's pick at its last accepted row (`next`, the next window's row 0). void set_draft_history(const int32_t* tail, int64_t n_tail, int32_t next); + void set_ple_session(SessionState* s) { ple_ss_ = s; } bool coupled() const { return coupled_active_; } double ms_draft = 0, ms_prefill = 0; @@ -124,6 +125,7 @@ class MtpDrafter { const ModelGeometry* g_ = nullptr; SessionState* ss_ = nullptr; + SessionState* ple_ss_ = nullptr; const WeightTable* wt_ = nullptr; const NativeHead* head_ = nullptr; const float* window_R_ = nullptr; diff --git a/include/strata/core/native_dense.hpp b/include/strata/core/native_dense.hpp index 8330fac55b..2d3cb94c68 100644 --- a/include/strata/core/native_dense.hpp +++ b/include/strata/core/native_dense.hpp @@ -19,12 +19,17 @@ class NativeDense { ~NativeDense(); NativeDense(const NativeDense&) = delete; NativeDense& operator=(const NativeDense&) = delete; + /// With layer_hi >= 0, only the `blk..` matrices with layer_lo <= l < layer_hi are uploaded (a layer + /// split's stage holds its own layers' projections, not the whole model's); the others keep data == nullptr. bool load(const std::vector& shards, WeightTable& table, std::string& err, - bool include_ple_key = false); + bool include_ple_key = false, int64_t layer_lo = 0, int64_t layer_hi = -1); /// Plan v0.3 P1: the canonical tensor names `load` would serve natively from these shards (eligible name, /// supported type, 2-D), read from the GGUF headers only - so the canonical arena can skip them. static bool served_names(const std::vector& shards, bool include_ple_key, std::set& out, std::string& err); + /// Layer split: load only blocks [lb, le) (every other `blk.N.` projection belongs to another GPU's stage; the + /// PLE tensors are loaded everywhere). Process-wide, read by the next `load`; (-1, -1) = all layers. + static void set_layer_range(int lb, int le); /// #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. diff --git a/include/strata/core/pinned.hpp b/include/strata/core/pinned.hpp index 44cf86c1e0..7d1f380c54 100644 --- a/include/strata/core/pinned.hpp +++ b/include/strata/core/pinned.hpp @@ -125,8 +125,11 @@ LoadStats load_experts_direct(const std::string& path, uint8_t* dst, const std:: /// STRATA_UNBUFFERED_LOAD=1 / 0 forces it. `why` says what decided. /// `cache_counts` false (the file tier with a RAM budget): only whether the files could be kept decides - their mapped /// pages land in the process's working set, so a partly cached file would still take the RAM the budget was sized for. +/// `read_bytes` (#577): the bytes the files are read for, when that is less than the files (the file tier reads only +/// the experts outside its RAM copy); `kAllFileBytes` = every byte of `files`. See platform::file_cache_keeps. +constexpr uint64_t kAllFileBytes = ~0ull; bool experts_unbuffered(const std::vector& files, uint64_t arena_bytes, std::string& why, - bool cache_counts = true); + bool cache_counts = true, uint64_t read_bytes = kAllFileBytes); // FNV-1a 64. Per layer, so a corrupt or short read names WHICH layer rather than just failing a whole-file // comparison - the same reason the Phase 1 tools report the first differing element. diff --git a/include/strata/core/remote_expert_opt.hpp b/include/strata/core/remote_expert_opt.hpp new file mode 100644 index 0000000000..5eb488c734 --- /dev/null +++ b/include/strata/core/remote_expert_opt.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace strata::core { +class RemoteExperts; +class ExpertSource; + +// Remote Expert Decode Optimization v1 on existing helper caches. One instance per serial engine. +class RemoteExpertOpt { +public: + RemoteExpertOpt() = default; + ~RemoteExpertOpt(); + RemoteExpertOpt(const RemoteExpertOpt&) = delete; + RemoteExpertOpt& operator=(const RemoteExpertOpt&) = delete; + + void attach(RemoteExperts& remote); // before RemoteExperts::open + bool init(std::string& err); // after all helper caches are open + bool owns(int64_t layer, int32_t expert) const; + bool adapt(const std::vector& usage, const std::vector& primary, + const std::vector>& pending, int max_swaps, + ExpertSource& source); + + // Bracket the existing pool callback; its signature and dispatch remain unchanged. + void begin(const float* weights, int token_begin, int tokens); + void end() { weights_ = nullptr; } + void copy_rows(float* dst, const float* src, int token_begin, int tokens, + const int32_t* hit_rows, const int32_t* count, void* stream) const; + void combine(float* dst, float* scratch, int token_begin, int tokens, + const uint32_t* skip, uint32_t ring, void* stream) const; + +private: + friend class RemoteExperts; + static size_t metadata_bytes(); + bool active() const { return weights_ != nullptr; } + void prepare(const RemoteExperts& remote, void* metadata) const; + bool reduce(RemoteExperts& remote, const void* metadata, std::string& err); + void accumulate(const RemoteExperts& remote); + + struct Peer { RemoteExperts* remote; float* sum = nullptr; }; + std::vector peers_; + float* h_sum_ = nullptr; + float* m_sum_ = nullptr; + int32_t* h_mask_ = nullptr; + int32_t* m_mask_ = nullptr; + const float* weights_ = nullptr; + int token_begin_ = 0, tokens_ = 0; +}; +} // namespace strata::core diff --git a/include/strata/core/remote_experts.hpp b/include/strata/core/remote_experts.hpp index 805bc950f8..224d24f5f0 100644 --- a/include/strata/core/remote_experts.hpp +++ b/include/strata/core/remote_experts.hpp @@ -11,6 +11,7 @@ #include namespace strata::core { +class RemoteExpertOpt; /// A static, profile-filled expert tier on another CUDA device. CUDA0 keeps all /// dense weights and state; results return through the existing pinned CPU rows. @@ -27,7 +28,7 @@ class RemoteExperts { bool open(int device, int slots, int64_t layers, int64_t experts, const std::vector>& ranked, const ExpertCache& primary, ExpertSource& source, - std::vector& claimed, std::string& err); + std::vector& claimed, std::string& err, bool auto_size = false); void close(); /// `kind` is the primary verifier's classification (-1 = CPU candidate), @@ -36,6 +37,7 @@ class RemoteExperts { int64_t k, const int32_t* kind, const int32_t* primary_res, std::string& err); bool owns(int64_t index) const { return owned_[(size_t) index] != 0; } + bool optimized_decode() const { return remote_opt_ != nullptr; } bool finish(float* out, std::string& err); int64_t resident() const { return cache_.resident(); } int64_t computed() const { return computed_; } @@ -48,6 +50,8 @@ class RemoteExperts { double ms_wait() const { return ms_wait_; } private: + friend class RemoteExpertOpt; + RemoteExpertOpt* remote_opt_ = nullptr; int device_ = -1; int64_t n_expert_ = 0; int32_t groups_ = 0; diff --git a/include/strata/core/session.hpp b/include/strata/core/session.hpp index 6d1becfac4..4d633205ea 100644 --- a/include/strata/core/session.hpp +++ b/include/strata/core/session.hpp @@ -16,6 +16,7 @@ #include "strata/core/hit_hook.hpp" #include "strata/core/layer.hpp" +#include "strata/kernels/cpu/pool.hpp" #include @@ -369,7 +370,7 @@ struct SessionLoopScratch { float* y_miss = nullptr; ///< pinned host staging for the pool's answer, `parts_bytes` long size_t parts_bytes = 0; cudaEvent_t probe = nullptr; - long long pinned_core = -1; ///< the affinity to restore, or -1 if the host was never pinned + strata::kernels::cpu::ThreadAffinity pinned_core{}; ///< affinity to restore, or invalid if the host was not pinned bool pinned = false; /// Allocates the buffers and pins the host thread. Call ONCE, at session setup. diff --git a/include/strata/core/verify.hpp b/include/strata/core/verify.hpp index b80d10bd26..e68f7b3a4b 100644 --- a/include/strata/core/verify.hpp +++ b/include/strata/core/verify.hpp @@ -31,6 +31,8 @@ #include #include +#include +#include #include #include #include @@ -38,6 +40,7 @@ namespace strata::core { class NativeHead; +class RemoteExpertOpt; /// The CPU pool for a window: x_f (n_tok, n_embd), ids (n_tok, k) -> out (n_tok * k, n_embd), hit rows zeroed. using PoolMultiFn = void (*)(void* user, const float* x_f, const int32_t* ids, int64_t n_tok, int64_t k, float* out, @@ -45,6 +48,7 @@ using PoolMultiFn = void (*)(void* user, const float* x_f, const int32_t* ids, i struct VerifyHits { const int32_t* d_res = nullptr; ///< device [n_layers * n_expert] slot or -1 + const int32_t* h_res = nullptr; ///< host [n_layers * n_expert] slot or -1 (when all >= 0 in stage: zero-doorbell) const uint8_t* cache_base = nullptr; ///< slot 0 of the VRAM expert arena const uint64_t* slot_off = nullptr; ///< E-6: host per-slot offsets when slots differ in size (null: slot * blob) int64_t n_slots = 0; ///< E-6: how many (for the device copy) @@ -57,9 +61,13 @@ class Verifier { ~Verifier(); Verifier(const Verifier&) = delete; Verifier& operator=(const Verifier&) = delete; + void set_remote_expert_opt(RemoteExpertOpt* opt) { remote_opt_ = opt; } // before init/capture /// The watchdog's view of the window in flight (issue #31): the layer, the GPU's sequence, the flags. void diag(std::FILE* f) const; + /// #649 (STRATA_VERIFY_TRACE=1, opt-in): the last windows' host-side handshake events and the GPU's breadcrumbs + /// (in mapped memory, so they read while the GPU hangs). Nothing when the trace is off. + void trace_dump(std::FILE* f) const; /// #267: raise every flag the window's spin kernels wait on past any ring (UINT32_MAX), so a window the GPU /// cannot finish drains instead of staying resident, then wait up to `timeout_ms` for its streams. For the @@ -119,6 +127,50 @@ class Verifier { /// 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); + + // ================================ SEVERAL SEQUENCES IN ONE WINDOW ================================ + // + // A batch window holds S INDEPENDENT sequences, one token each: row s is slot s, at slot s's own position, + // reading and writing slot s's own state (GDN recurrence and conv history, QSA K/V and indexer, PLE history), + // which lives in `slots[s]` - a session carved like this verifier's own (same layer range, same max_cells). + // Everything that is per row already (hyper-connections, dense projections, router, shared expert, the + // routed experts on the GPU and the CPU, the head) runs once over the S rows, so the weights are read once + // per window for all the sequences. Row s's arithmetic is the single-token window's, so a slot's greedy + // tokens are its solo greedy tokens (modulo the multi-token CPU kernel choice: STRATA_IQ_MT_MIN=1). + // + // Greedy only, no drafts (MTP) in a batch window. `init_slots` once after `init` (S <= max_t); a layer + // split's stages each get their own sessions, and run_slots/commit_slots continue into the next stage. + bool init_slots(const std::vector& slots, std::string& err); + int n_slots() const { return (int) slots_.size(); } + /// One batch window over slots [0, S): tokens[s] at positions pos[s]; out[s] = the greedy pick after it. + bool run_slots(int S, const int32_t* tokens, const int64_t* pos, PoolMultiFn pool, void* user, int32_t* out, + std::string& err); + /// The same over the S slots `rows` (row t is slot rows[t], any distinct slots in any order): the slots not + /// listed are not touched, so an idle slot keeps its state (a finished conversation it may continue later). + bool run_slot_rows(const int* rows, int S, const int32_t* tokens, const int64_t* pos, PoolMultiFn pool, void* user, + int32_t* out, std::string& err); + /// Keep every row of the last batch window: each slot's state advances by its one token. + bool commit_slots(std::string& err); + + // ---- The stages of a layer split as a PIPELINE. A batch window over the slot GROUP + // [base, base + S) is launched on ONE stage with its commit right behind it on the stage's stream (a batch window + // keeps every row, so the commit needs no host decision), and the host serves the rings of every stage that has + // a window in flight from one thread (batch_poll does not block). Stage k can then run group g while stage k+1 + // runs group g-1. Rows of group `base` use hand-off rows [base, base + S), so groups never share a hand-off row. + bool batch_launch(int base, int S, const int32_t* tokens, const int64_t* pos, std::string& err); + /// 1 = this stage's window and commit are done (the last stage's picks are in batch_out), 0 = still running, + /// -1 = an error (err). Serves every layer that has rung so far. + int batch_poll(PoolMultiFn pool, void* user, std::string& err); + bool batch_busy() const { return b_running_; } + /// A slot's sampling (temperature / top_p / top_k / min_p / seed; penalties are not applied in batch windows): + /// its row is drawn again on the last stage with Philox(seed, position), as a solo window draws it. Greedy by + /// default. Set on the first stage, it reaches the last. + void set_slot_sampling(int slot, const strata::kernels::SamplerParams& sp) { + if (slot >= 0 && slot < (int) slot_sp_.size()) slot_sp_[(size_t) slot] = sp; + if (next_) next_->set_slot_sampling(slot, sp); + } + const int32_t* batch_out() const { return b_out_; } + bool last_stage() const { return g_ != nullptr && le_ == g_->n_layers; } /// 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 @@ -161,7 +213,37 @@ class Verifier { std::string profile_report(); private: + RemoteExpertOpt* remote_opt_ = nullptr; bool capture(int T, std::string& err); + // batch windows (see init_slots) + std::vector slots_; + bool batch_rec_ = false; ///< record_window is capturing a batch window + int row_base_ = 0; ///< ... its hand-off rows start here (a pipeline group's own rows) + int brow_[8] = {}; ///< ... and row t is slot brow_[t] + bool last_batch_ = false; ///< the last run was a batch window (set_plan_slot: one group) + std::map exec_bm_, commit_bm_; ///< key: batch_key(rows, S, hand-off base) + int last_rows_[8] = {}; ///< the slots of the last batch window's rows + static uint64_t batch_key(const int* rows, int S, int hbase) { + uint64_t k = (uint64_t) hbase << 40 | (uint64_t) S << 32; + for (int t = 0; t < S; ++t) k |= (uint64_t) (rows[t] & 15) << (4 * t); + return k; + } + // batch_launch / batch_poll + bool b_running_ = false; + int64_t b_k_ = 0, b_steps_ = 0; + std::chrono::steady_clock::time_point b_last_; + int32_t b_out_[8] = {}; + std::vector slot_sp_; + bool sample_rows(int S, std::string& err); ///< the sampled slots' rows of the last batch window + int32_t* h_commitb_ = nullptr; int32_t* m_commitb_ = nullptr; // per slot [1, 0, pos, -1 ..], stride 2 + max_t + int32_t* commitb_ = nullptr; + float* tail_snap_b_ = nullptr; ///< per (slot, QSA layer) indexer tail snapshot + void* arena_b_ = nullptr; + int64_t last_pos_b_[8] = {}; + bool capture_batch(const int* rows, int S, int hbase, std::string& err); + void collect_profile(); ///< STRATA_VERIFY_PROFILE: add the last window's stamps to prof_sum_ + bool capture_commit_batch(const int* rows, int S, int hbase, std::string& err); + bool stage_batch(const int* rows, int S, int hbase, const int32_t* tokens, const int64_t* pos, std::string& err); strata::kernels::SamplerParams sampling_ = [] { strata::kernels::SamplerParams s; s.greedy = true; @@ -173,6 +255,7 @@ class Verifier { bool head_sampling_ = true; ///< set_head_sampling int device_ = -1; ///< the device `init` ran on: run/commit switch to it (layer split) std::atomic released_{false}; ///< #267: release_gpu_waits ran (maybe on the watchdog thread): no more windows + bool all_resident_ = false; ///< 100% of experts in [lb_, le_) resident in VRAM: zero-doorbell graph bool device_plan_ = false; ///< E-6: resident-only layers planned on the device (STRATA_VERIFY_DEVICE_PLAN) uint32_t* skip_ = nullptr; ///< E-6: per group, the ring whose plan the device built (0: the host's) unsigned long long* slot_off_d_ = nullptr; ///< E-6: the slot offsets on the device @@ -182,8 +265,17 @@ class Verifier { Verifier* next_ = nullptr; void* next_user_ = nullptr; bool ple_stage() const { return lb_ <= 1 && 1 < le_; } ///< holds layer 1, where the PLE block runs + void stage_inputs(int T, const int32_t* tokens, int64_t pos0); + bool staged_ = false; + bool copy_used_ = false; bool capture_commit(std::string& err); bool record_window(int T, cudaStream_t cs, std::string& err); + // #649: STRATA_VERIFY_TRACE=1 - a host event ring (trace_ev) and GPU breadcrumbs: the profiler's stamp points, + // per layer and token group, written to mapped memory (null when the trace is off) + void trace_ev(const char* what, int64_t step, int64_t layer, int64_t aux) const; + unsigned long long* trace_h_ = nullptr; + unsigned long long* trace_m_ = nullptr; + size_t trace_n_ = 0; 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; @@ -203,6 +295,8 @@ class Verifier { int32_t last_tokens_[8] = {}; int64_t n_vocab_ = 0; cudaStream_t cs_ = nullptr; + cudaStream_t sh_cs_ = nullptr; + cudaEvent_t ev_fork_ = nullptr, ev_join_ = nullptr; cudaGraphExec_t exec_[9] = {}; cudaGraphExec_t commit_exec_ = nullptr; @@ -243,6 +337,7 @@ class Verifier { float *ple_ = nullptr, *emb_ = nullptr, *R_ = nullptr, *mixed_ = nullptr, *bo_ = nullptr; float *inj_ = nullptr, *inj2_ = nullptr, *lo_ = nullptr, *rs_ = nullptr, *xn_ = nullptr; uint8_t* xq_ = nullptr; // T columns of q8_1 + uint8_t* sh_xq_ = nullptr; // T columns of q8_1 for shared expert branch float *qkv_L_ = nullptr, *h_L_ = nullptr, *gate_L_ = nullptr, *beta_L_ = nullptr; // per GDN layer float *z_ = nullptr, *y_ = nullptr, *y_dummy_ = nullptr; float *qfull_ = nullptr, *qcur_ = nullptr, *kcur_ = nullptr, *vcur_ = nullptr, *idx_raw_L_ = nullptr; diff --git a/include/strata/hip_compat/cublas_v2.h b/include/strata/hip_compat/cublas_v2.h index f162474bb4..879447968e 100644 --- a/include/strata/hip_compat/cublas_v2.h +++ b/include/strata/hip_compat/cublas_v2.h @@ -1,5 +1,45 @@ #pragma once #include +#include + +#if hipblasVersionMajor == 0 +// Ubuntu's hipBLAS 0.54 has no workspace API or compute-type enum. Use its +// underlying rocBLAS API directly, including ownership of the real handle, +// so the engine's explicit workspace contract is preserved. +#include +#define CUBLAS_COMPUTE_32F rocblas_datatype_f32_r +#define CUBLAS_DEFAULT_MATH 0 +#define CUBLAS_GEMM_DEFAULT rocblas_gemm_algo_standard +#define CUBLAS_OP_N rocblas_operation_none +#define CUBLAS_OP_T rocblas_operation_transpose +#define CUBLAS_STATUS_SUCCESS rocblas_status_success +#define CUDA_R_16BF rocblas_datatype_bf16_r +#define CUDA_R_16F rocblas_datatype_f16_r +#define CUDA_R_32F rocblas_datatype_f32_r +#define cublasCreate rocblas_create_handle +#define cublasDestroy rocblas_destroy_handle +#define cublasHandle_t rocblas_handle +#define cublasSetStream rocblas_set_stream +#define cublasSetWorkspace rocblas_set_workspace +#define cublasStatus_t rocblas_status + +inline rocblas_status cublasSetMathMode(rocblas_handle h, int mode) { + // This API has only its default arithmetic; GEMM below explicitly selects + // FP32 accumulation. Reject any request for a different math mode. + if (!h) return rocblas_status_invalid_handle; + return mode == CUBLAS_DEFAULT_MATH ? rocblas_status_success : rocblas_status_invalid_value; +} +inline rocblas_status cublasGemmEx(rocblas_handle h, rocblas_operation trans_a, + rocblas_operation trans_b, int m, int n, int k, const void* alpha, + const void* a, rocblas_datatype a_type, int lda, + const void* b, rocblas_datatype b_type, int ldb, const void* beta, + void* c, rocblas_datatype c_type, int ldc, + rocblas_datatype compute_type, rocblas_gemm_algo algo) { + return rocblas_gemm_ex(h, trans_a, trans_b, m, n, k, alpha, + a, a_type, lda, b, b_type, ldb, beta, c, c_type, ldc, + c, c_type, ldc, compute_type, algo, 0, 0); +} +#else #define CUBLAS_COMPUTE_32F HIPBLAS_COMPUTE_32F #define CUBLAS_DEFAULT_MATH HIPBLAS_DEFAULT_MATH #define CUBLAS_GEMM_DEFAULT HIPBLAS_GEMM_DEFAULT @@ -17,3 +57,4 @@ #define cublasSetStream hipblasSetStream #define cublasSetWorkspace hipblasSetWorkspace #define cublasStatus_t hipblasStatus_t +#endif diff --git a/include/strata/hip_compat/cuda_runtime.h b/include/strata/hip_compat/cuda_runtime.h index da2c4f4cc4..8ea907a943 100644 --- a/include/strata/hip_compat/cuda_runtime.h +++ b/include/strata/hip_compat/cuda_runtime.h @@ -10,6 +10,7 @@ #endif #endif #include +#include // Do not let HIP's legacy macro corrupt libstdc++ attribute names. #ifdef __noinline__ #undef __noinline__ @@ -39,8 +40,13 @@ #define cudaEventSynchronize hipEventSynchronize #define cudaEvent_t hipEvent_t #define cudaFree hipFree +#if HIP_VERSION_MAJOR < 7 +#define cudaFreeHost hipHostFree +#else #define cudaFreeHost hipFreeHost +#endif #define cudaFuncAttributeMaxDynamicSharedMemorySize hipFuncAttributeMaxDynamicSharedMemorySize +#define cudaFuncAttributePreferredSharedMemoryCarveout hipFuncAttributePreferredSharedMemoryCarveout #define cudaGetDevice hipGetDevice #define cudaGetDeviceCount hipGetDeviceCount #define cudaGetDeviceProperties hipGetDeviceProperties @@ -54,13 +60,20 @@ #define cudaGraphUpload hipGraphUpload #define cudaGraph_t hipGraph_t #define cudaHostAlloc hipHostMalloc +#if HIP_VERSION_MAJOR < 7 +#define cudaHostAllocDefault hipHostMallocDefault +#define cudaHostAllocMapped hipHostMallocMapped +#define cudaHostAllocPortable hipHostMallocPortable +#else #define cudaHostAllocDefault hipHostAllocDefault #define cudaHostAllocMapped hipHostAllocMapped #define cudaHostAllocPortable hipHostAllocPortable +#endif #define cudaHostGetDevicePointer hipHostGetDevicePointer #define cudaHostRegister hipHostRegister #define cudaHostRegisterMapped hipHostRegisterMapped #define cudaHostRegisterPortable hipHostRegisterPortable +#define cudaHostRegisterReadOnly hipHostRegisterReadOnly #define cudaHostUnregister hipHostUnregister #define cudaLaunchHostFunc hipLaunchHostFunc #define cudaMalloc hipMalloc diff --git a/include/strata/hip_compat/intrinsics.hpp b/include/strata/hip_compat/intrinsics.hpp index a789e38c98..3129f0492b 100644 --- a/include/strata/hip_compat/intrinsics.hpp +++ b/include/strata/hip_compat/intrinsics.hpp @@ -1,8 +1,9 @@ #pragma once -// CUDA device intrinsics used by Strata kernels that HIP does not provide on RDNA2 / RDNA3 / RDNA4 (wave32). +// CUDA device intrinsics used by Strata kernels on supported wave32 HIP targets. // This header is included only from the HIP cuda_runtime compatibility shim. #if defined(__HIPCC__) +#include #include @@ -25,6 +26,21 @@ __device__ __forceinline__ int dp4a(int a, int b, int c) { // 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); +#elif defined(__gfx1012__) && !defined(STRATA_GFX1012_PORTABLE_DOT) + // RDNA1 has no native signed dot4. SDWA selects and sign-extends byte + // operands directly, avoiding separate shift/mask/sign-extension work. + // Adapted from pinned llama.cpp ggml-cuda/common.cuh (MIT; see + // third_party/ggml/LICENSE), same modulo-2^32 accumulation contract. + int lo, hi; + asm("v_mul_i32_i24 %1, sext(%3), sext(%4) dst_sel:DWORD dst_unused:UNUSED_PAD src0_sel:BYTE_0 src1_sel:BYTE_0\n" + "v_mul_i32_i24 %2, sext(%3), sext(%4) dst_sel:DWORD dst_unused:UNUSED_PAD src0_sel:BYTE_1 src1_sel:BYTE_1\n" + "v_add3_u32 %0, %1, %2, %0\n" + "v_mul_i32_i24 %1, sext(%3), sext(%4) dst_sel:DWORD dst_unused:UNUSED_PAD src0_sel:BYTE_2 src1_sel:BYTE_2\n" + "v_mul_i32_i24 %2, sext(%3), sext(%4) dst_sel:DWORD dst_unused:UNUSED_PAD src0_sel:BYTE_3 src1_sel:BYTE_3\n" + "v_add3_u32 %0, %1, %2, %0\n" + // c changes before the high-byte reads; it must not alias a or b. + : "+&v"(c), "=&v"(lo), "=&v"(hi) : "v"(a), "v"(b)); + return c; #else const uint32_t ua = static_cast(a); const uint32_t ub = static_cast(b); @@ -84,6 +100,17 @@ __device__ __forceinline__ void require_full_wave_mask(uint32_t mask) { if (mask != 0xffffffffu) __builtin_trap(); } +// Older HIP has no __syncwarp. A wave barrier alone does not order memory. +// Release/acquire fences cover the shared-memory exchange used by attention. +#if HIP_VERSION_MAJOR < 7 +__device__ __forceinline__ void syncwarp(uint32_t mask = 0xffffffffu) { + require_full_wave_mask(mask); + __builtin_amdgcn_fence(__ATOMIC_RELEASE, "workgroup"); + __builtin_amdgcn_wave_barrier(); + __builtin_amdgcn_fence(__ATOMIC_ACQUIRE, "workgroup"); +} +#endif + template __device__ __forceinline__ T shfl_xor_sync(uint32_t mask, T value, int lane_mask, int width = 32) { require_full_wave_mask(mask); @@ -125,6 +152,9 @@ __device__ __forceinline__ unsigned ballot_sync(uint32_t mask, int predicate) { #define __shfl_up_sync(...) (::strata::hip_compat::shfl_up_sync(__VA_ARGS__)) #define __shfl_sync(...) (::strata::hip_compat::shfl_sync(__VA_ARGS__)) #define __ballot_sync(mask, predicate) (::strata::hip_compat::ballot_sync((mask), (predicate))) +#if HIP_VERSION_MAJOR < 7 +#define __syncwarp(...) (::strata::hip_compat::syncwarp(__VA_ARGS__)) +#endif // AMD's sleep instruction accepts only 0..15; the synchronization loops use it as a // backoff hint, so use its smallest portable delay independently of CUDA cycle counts. #define __nanosleep(cycles) __builtin_amdgcn_s_sleep(1) diff --git a/include/strata/hip_compat/math_constants.h b/include/strata/hip_compat/math_constants.h new file mode 100644 index 0000000000..50a0862504 --- /dev/null +++ b/include/strata/hip_compat/math_constants.h @@ -0,0 +1,4 @@ +#pragma once +// CUDA spelling used by the attention kernels; included only in HIP builds. +#include +#define CUDART_INF_F HIP_INF_F diff --git a/include/strata/kernels/bf16_gemv.hpp b/include/strata/kernels/bf16_gemv.hpp index 906a8f0b15..e3efe0ff34 100644 --- a/include/strata/kernels/bf16_gemv.hpp +++ b/include/strata/kernels/bf16_gemv.hpp @@ -44,6 +44,10 @@ void bf16_gemv_split(const uint16_t* x, const uint16_t* w, float* y, int64_t n_i /// No allocations or synchronization, including when stream is null (the CUDA default stream). void bf16_gemv_fp32_mmvf(const float* x, const uint16_t* w, float* y, int64_t n_in, int64_t n_out, void* stream); +/// `ncols` columns at once: x is [ncols][n_in], y is [ncols][n_out]; each column bitwise equal to a +/// `bf16_gemv_fp32_mmvf` call on it. +void bf16_gemv_fp32_mmvf_cols(const float* x, const uint16_t* w, float* y, int64_t n_in, int64_t n_out, int ncols, + void* stream); /// bf16_gemv_fp32_mmvf for n_tok (1..8) activation rows x[t * ldx], outputs y[t * ldy + j]; one launch, /// the weight read once, every output bit-identical to its own single-row call. void bf16_gemv_fp32_mmvf_multi(const float* x, int64_t ldx, const uint16_t* w, float* y, int64_t ldy, diff --git a/include/strata/kernels/cpu/expert_layout.hpp b/include/strata/kernels/cpu/expert_layout.hpp index 9a8b991830..0eff498dd4 100644 --- a/include/strata/kernels/cpu/expert_layout.hpp +++ b/include/strata/kernels/cpu/expert_layout.hpp @@ -44,9 +44,25 @@ struct ExpertLayout { /// Plan v0.3 P6: whether this CPU (and its OS) runs the AVX-512 kernels (F, BW, VL, VNNI, VBMI). Probed in a /// file compiled without AVX-512, so asking is safe everywhere; STRATA_FORCE_AVX2=1 answers no (for tests). bool cpu_avx512_ok(); +/// Whether this CPU runs AVX-512F + AVX-512BW + AVX-512VL + AVX-512DQ (e.g. Skylake-X i9-9920X) for iq_avx512.cpp. +bool cpu_avx512bw_ok(); /// Whether this CPU (and its OS) runs the AVX2 kernels (AVX, AVX2, FMA, F16C): the floor of every expert kernel /// (q2_avx2.cpp, iq_avx2.cpp, and ggml-cpu in the portable build). STRATA_FORCE_AVX2 does not change it. bool cpu_avx2_ok(); +/// Whether this CPU (and its OS) runs AVX (Sandy Bridge and newer): the AVX1 router dot (kq_avx1.cpp) and ggml-cpu in +/// a STRATA_ISA_FLOOR=avx build. The other expert rows then run on ggml-cpu (experimental, slow). +bool cpu_avx1_ok(); +/// SSE4.2 + POPCNT: ggml-cpu in a STRATA_ISA_FLOOR=none build. +bool cpu_sse42_ok(); +/// STRATA_FORCE_ISA=avx2|avx|sse caps the probes above (3 = no cap, 2 = AVX2, 1 = AVX, 0 = SSE4.2), so the older-CPU +/// paths run on any PC: tests and the experimental builds' checks. ggml-cpu keeps what it was compiled for. +int cpu_isa_cap(); +/// The experimental older-CPU build this engine is ("avx", "sse4.2"), or "" for the normal build (AVX2 floor). +const char* isa_floor_build(); +/// Whether a native pack's layer of this ggml type runs on Strata's own Q2_0 kernels (AVX2 / AVX-512). On a +/// CPU without AVX2 (a native, non-portable build: ggml-cpu compiled for this CPU's SSE) a Q2_0 layer takes +/// ggml-cpu's own Q2_0 vec_dot like every other native type: slower, but it runs. +inline bool q2_native_kernels(int type) { return type == 42 && cpu_avx2_ok(); } /// The CPU's brand string (CPUID 0x80000002..4), for messages; "unknown" when it has none. std::string cpu_name(); /// Q2_0 GGUF rows / activation quantizer on the kernels this CPU has. diff --git a/include/strata/kernels/cpu/kq_avx1.hpp b/include/strata/kernels/cpu/kq_avx1.hpp new file mode 100644 index 0000000000..c9afc3f83b --- /dev/null +++ b/include/strata/kernels/cpu/kq_avx1.hpp @@ -0,0 +1,18 @@ +// include/strata/kernels/cpu/kq_avx1.hpp - the AVX1 router dot for older CPUs (from the Strata_Dirigo fork). +// +// bf16_rows_dot_multi (kq_avx2.cpp) is compiled with -mavx2 and uses instructions an AVX-only CPU does +// not have (`vpmovzxwd`, `vfmadd`), so an AVX-only host cannot call it at all. This is the same +// arithmetic on AVX1 instructions: 8 float lanes per token, one weight stream per row shared by all +// `nt` tokens, one horizontal sum per (row, token). AVX1 has no FMA, so the multiply and the add stay +// separate; it has no 256-bit integer ops, so the bf16 -> f32 widening runs in 128-bit halves. +#pragma once + +#include + +namespace strata::kernels::cpu { + +/// Mirrors bf16_rows_dot_multi's arithmetic (same accumulation shape, mul+add instead of FMA). +/// Requires cols % 8 == 0 and nt <= 8, exactly like the AVX2 kernel it stands in for. +void bf16_rows_dot_multi_avx1(const uint16_t* w, int rows, int cols, const float* x, int nt, float* out); + +} // namespace strata::kernels::cpu diff --git a/include/strata/kernels/cpu/pool.hpp b/include/strata/kernels/cpu/pool.hpp index 76bda6def8..662bb03c17 100644 --- a/include/strata/kernels/cpu/pool.hpp +++ b/include/strata/kernels/cpu/pool.hpp @@ -76,7 +76,7 @@ struct CpuTopology { 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 worker_cores; ///< Ordered logical core IDs for workers (excluding host if skip_first) + std::vector worker_cores; ///< Ordered CPU IDs; on Windows, group * 64 + processor within the group int host_core = -1; ///< Logical core reserved for host thread }; @@ -100,9 +100,23 @@ std::vector physical_cores(bool skip_first, PoolAffinity affinity = PoolAff /// - exactly 5/6 of L9's 44.14 on 6 - and at **26.9 GB/s inside the host loop**, where the unpinned spinning /// host is free to land on a worker's core or its SMT sibling. That 1.35x is not the kernel. /// -/// Returns the PREVIOUS affinity mask, or -1 if the platform refused; pass it to `restore_thread_affinity`. -long long pin_current_thread(int core); -void restore_thread_affinity(long long previous); +/// The previous host placement. `valid` is false when querying or setting placement failed. +/// Windows uses a reversible CPU Set selection, leaving hard affinity (including Windows 11's implicit +/// all-group eligibility) untouched. An empty selection restores inheritance from the process defaults. +/// Linux stores the complete, dynamically sized native CPU mask, including CPU IDs above 63. +struct ThreadAffinity { +#if defined(_WIN32) + std::vector cpu_sets; +#else + std::vector mask; +#endif + bool valid = false; +}; + +/// Selects one encoded processor for the caller; Windows CPU Sets respect existing hard-affinity limits. +/// Restore on the same calling thread. Pool-owned workers use hard group affinity instead. +ThreadAffinity pin_current_thread(int core); +void restore_thread_affinity(const ThreadAffinity& previous); class ExpertPool { public: diff --git a/include/strata/kernels/dp4a.hpp b/include/strata/kernels/dp4a.hpp index cbca6a9aa5..e73be4d15f 100644 --- a/include/strata/kernels/dp4a.hpp +++ b/include/strata/kernels/dp4a.hpp @@ -19,6 +19,9 @@ #pragma once #include +#if defined(STRATA_HIP_GFX906) +#include // gfx906: the compat layer (__forceinline__, __nanosleep, __dp4a) +#endif #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ < 610 __device__ __forceinline__ int strata_dp4a(const int a, const int b, const int c) { diff --git a/include/strata/kernels/elementwise.hpp b/include/strata/kernels/elementwise.hpp index 02501beb6c..ae0973c6dc 100644 --- a/include/strata/kernels/elementwise.hpp +++ b/include/strata/kernels/elementwise.hpp @@ -1,4 +1,4 @@ -// include/strata/kernels/elementwise.hpp - the small ops a LAYER needs between its GEMVs, P2.S5. +// include/strata/kernels/elementwise.hpp - the small ops a LAYER needs between its GEMVs, P2.S5. // // These are not interesting kernels and that is why they are in one file with one parity test: they are the // glue in the per-layer graph, and glue that is written inline at each call site is glue with several @@ -114,6 +114,12 @@ void copy_rows_from_mapped(float* dst, const float* src, int64_t rows, int64_t w /// device-to-host memcpy nodes (copy-engine operations in the middle of the layer chain) and the ring kernel. void doorbell_publish(const float* x, const int32_t* ids, const float* weights, int64_t n, int64_t k, float* x_out, int32_t* ids_out, float* weights_out, uint32_t* d_seq, void* stream); +/// #649 (HIP, STRATA_DOORBELL_STORE=1): the ring stored as `value` instead of incremented over PCIe. +void doorbell_publish_value(const float* x, const int32_t* ids, const float* weights, int64_t n, int64_t k, + float* x_out, int32_t* ids_out, float* weights_out, uint32_t* d_seq, uint32_t value, + void* stream); +void doorbell_publish_res(const float* x, const int32_t* ids, const int32_t* d_res, int n_expert, int64_t n, int64_t k, + float* x_out, int32_t* ids_out, uint32_t* d_seq, void* stream); /// Plan v0.3 P3: copy `n` int32 from mapped pinned host memory into device memory with a kernel (the QSA /// per-token step and positions), instead of a host-to-device memcpy node in the middle of a layer. diff --git a/include/strata/kernels/fused_gr.hpp b/include/strata/kernels/fused_gr.hpp index 40eff30f28..b5f95fcb94 100644 --- a/include/strata/kernels/fused_gr.hpp +++ b/include/strata/kernels/fused_gr.hpp @@ -49,6 +49,8 @@ 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 bench only: the AMD latency-hidden kernels on (1) or off (0); -1 = STRATA_GR_FAST. +void fused_gr_set_fast(int on); /// 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 diff --git a/include/strata/kernels/iq_kernels.hpp b/include/strata/kernels/iq_kernels.hpp index 56c0d65e7b..2c38f6f98d 100644 --- a/include/strata/kernels/iq_kernels.hpp +++ b/include/strata/kernels/iq_kernels.hpp @@ -68,6 +68,9 @@ void native_expert_grouped(const NativeExpertLayout& L, const unsigned long long /// (native_grouped_parity checks it); kept for A/B timing. Set before graph capture; captured graphs keep theirs. void native_grouped_set_v1(bool v1); +/// The bench only: the AMD kernel layout (STRATA_EXP_MODE values; -1 = the environment's) and the phase +/// (0 all, 1 gate/up + SwiGLU + quantize, 2 down). +void native_expert_set_mode(int mode, int phase); /// `iq_mmvq` and `native_expert_grouped` decode each weight part once and apply it to every column / entry; /// true selects the older kernels that decode it again per column (STRATA_OLD_IQ_MMVQ=1 at startup). Both give /// bitwise the same results. Set before graph capture; captured graphs keep the kernels they captured. diff --git a/include/strata/kernels/kv_stream.hpp b/include/strata/kernels/kv_stream.hpp index b3ebee34d9..514474b113 100644 --- a/include/strata/kernels/kv_stream.hpp +++ b/include/strata/kernels/kv_stream.hpp @@ -85,6 +85,12 @@ void kv_ring_restore(const QsaAttnPools& slots, const KvHostPools& host, int fmt void kv_stage_from_host(const QsaAttnPools& stage, const KvHostPools& host, int fmt, int64_t n_blocks, const QsaShapes& s, void* stream); +/// #579 #613 (HIP A/B, STRATA_KV_HOST_DMA=1): the reverse of kv_stage_from_host for blocks [b0, b1) - the staging +/// pool's blocks into the host copy, by DMA, so the prompt path's append need not write host memory from a kernel. +/// Not capturable. +void kv_unstage_to_host(const QsaAttnPools& stage, const KvHostPools& host, int fmt, int64_t b0, int64_t b1, + const QsaShapes& s, void* stream); + struct KvStreamCounters { uint64_t misses = 0, lookups = 0, calls = 0; bool overflow = false; diff --git a/include/strata/kernels/mrope.hpp b/include/strata/kernels/mrope.hpp index cb1451c402..18dbb6858e 100644 --- a/include/strata/kernels/mrope.hpp +++ b/include/strata/kernels/mrope.hpp @@ -15,6 +15,9 @@ #pragma once #include +#if defined(__HIPCC__) && defined(STRATA_HIP_GFX906) // PR #638: the gfx906 compat build only +#include +#endif #include "strata/kernels/rope_scaling.hpp" diff --git a/include/strata/kernels/native_mmvq.hpp b/include/strata/kernels/native_mmvq.hpp index 9f69dd940c..a1554e5d1b 100644 --- a/include/strata/kernels/native_mmvq.hpp +++ b/include/strata/kernels/native_mmvq.hpp @@ -38,6 +38,9 @@ bool native_mmvq_multi_exact(); void native_quantize_q8_1(const float* x, void* x_q8_1, int n_in, int ncols, void* stream); +void native_swiglu_quantize_q8_1(const float* gate, const float* up, void* x_q8_1, + int n_in, int ncols, void* stream); + void native_q5_k_mmvq(const void* weights, const void* x_q8_1, float* y, int n_in, int n_out, int ncols, void* stream); diff --git a/include/strata/kernels/ngram.hpp b/include/strata/kernels/ngram.hpp index 9aae9c08c8..a321ea70f9 100644 --- a/include/strata/kernels/ngram.hpp +++ b/include/strata/kernels/ngram.hpp @@ -143,6 +143,9 @@ class PleTable { /// needs the rows. `gather` is `issue` followed by `collect`. In Mmap mode `issue` only prefetches. bool issue(const uint32_t* rows16); bool collect(float* out2560, std::string& err); + /// Non-blocking prefetch of 16 rows for one token into the reader's row cache while the GPU is busy. + void prefetch_rows(const uint32_t* rows16); + void wait_prefetches(); /// Plan v0.3 P5: the rows of `n_tokens` tokens (16 each, `rows` token-major) into `out` (2560 floats per token), /// as ONE reader request - page dedupe and sort across the whole batch, the reader's full queue depth. Not /// while a single-token `issue` is pending. The mapped mode gathers row by row. diff --git a/include/strata/kernels/ple.hpp b/include/strata/kernels/ple.hpp index fabad838a3..76ae78902b 100644 --- a/include/strata/kernels/ple.hpp +++ b/include/strata/kernels/ple.hpp @@ -33,6 +33,7 @@ namespace strata::kernels { /// Opt in to pinned CUDA BF16/F32 MMVF for the PLE value projection only (default false). /// Configure before session capture; captured graphs retain their selected projection kernels. void ple_set_native_bf16(bool enabled); +bool ple_native_bf16_enabled(); /// Opt in to pinned CUDA postprojection norms, gate, convolution and residual /// arithmetic (default false). Set before capture; existing graphs keep their @@ -107,6 +108,9 @@ uint64_t ple_block_scratch_bytes(); void ple_block(const float* emb, const float* hidden, const float* hist_rows, const PleWeights& w, PleOut& out, void* scratch, void* stream); +void ple_block_projected(const float* projected_key, const float* projected_value, const float* hidden, + const float* hist_rows, const PleWeights& w, PleOut& out, void* scratch, void* stream); + /// Advance the row-fastest normalized history by one token: hist[r,c] = old_hist[r+1,c], then append /// normalized[c] at row NG_HIST-1. One thread owns each channel, making the in-place shift well-defined. /// hist and normalized must be nonnull, disjoint device regions of NG_HIST*NG_HC_DIM and NG_HC_DIM floats. diff --git a/include/strata/kernels/q8_1_finite.hpp b/include/strata/kernels/q8_1_finite.hpp new file mode 100644 index 0000000000..7826dbc528 --- /dev/null +++ b/include/strata/kernels/q8_1_finite.hpp @@ -0,0 +1,33 @@ +// include/strata/kernels/q8_1_finite.hpp - #606: the q8_1 activation block's scale and sum, kept finite. +// +// A q8_1 block stores its scale d = amax / 127 and the sum of its 32 raw activations as fp16. A sum past 65504 (one +// massive-activation dimension among 32 values is enough) rounds to inf, and d does past amax = 8.3M; the dot +// products then read inf * 0 = NaN, and a NaN in the residual ends as one token repeated (ggml-org/llama.cpp#23606 +// is the same defect). Clamped to the largest finite half, such a block gives an approximate product instead of NaN, +// and the values are held to the int8 range they then exceed. Every block that was finite before is stored bit for +// bit as before: values in (65504, 65520) round to 65504 anyway, and a finite block's |x / d| is at most 127. A NaN +// stays NaN (the comparisons are false for it), so STRATA_DBG_NAN still sees one. +#pragma once + +#include + +namespace strata::kernels { + +constexpr float kHalfMax = 65504.0f; + +__device__ __forceinline__ float q8_1_finite(const float v) { + return fabsf(v) > kHalfMax ? copysignf(kHalfMax, v) : v; +} + +/// The block's quantized value of `xi` for the (finite) scale `d`; `amax` = 0 is an all-zero block. +__device__ __forceinline__ int8_t q8_1_quant(const float xi, const float d, const float amax) { + if (amax == 0.0f) return 0; + const float q = roundf(xi / d); + return (int8_t) (q > 127.0f ? 127.0f : q < -127.0f ? -127.0f : q); +} + +__device__ __forceinline__ half2 q8_1_ds(const float d, const float sum) { + return make_half2(__float2half(q8_1_finite(d)), __float2half(q8_1_finite(sum))); +} + +} // namespace strata::kernels diff --git a/include/strata/kernels/qsa_select.hpp b/include/strata/kernels/qsa_select.hpp index b70a4ce560..3150816511 100644 --- a/include/strata/kernels/qsa_select.hpp +++ b/include/strata/kernels/qsa_select.hpp @@ -49,7 +49,8 @@ void qsa_block_topk(const float* scores, const int32_t* steps, int64_t nq, int64 /// 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. +/// The original kernel (256 threads, one shared histogram, keys read from memory on every radix pass), for tests and +/// STRATA_TOPK_OLD=1: 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); diff --git a/include/strata/kernels/shared_expert.hpp b/include/strata/kernels/shared_expert.hpp index 8909ed9e60..44b37d8220 100644 --- a/include/strata/kernels/shared_expert.hpp +++ b/include/strata/kernels/shared_expert.hpp @@ -24,6 +24,7 @@ namespace strata::kernels { /// Configure before session capture; captured graphs retain their selected kernels. /// In this mode shared_expert requires its optional unrounded x_f32 input. void shared_expert_set_native_bf16(bool enabled); +bool shared_expert_native_bf16_enabled(); /// Optional native GGUF projections. Each supported type with nonnull data /// replaces only that canonical projection; absent or unsupported entries fall @@ -37,6 +38,7 @@ struct NativeSharedWeights { const void* up_data = nullptr; const void* down_data = nullptr; void* q8_1 = nullptr; + const void* x_q8_1 = nullptr; // optional pre-quantized Q8_1 input for gate/up in shared_expert_multi }; /// Bytes of caller-owned scratch `shared_expert` needs. **THE KERNEL USED TO `cudaMalloc` FOUR BUFFERS ON diff --git a/include/strata/platform/hip_compat/cublas_v2.h b/include/strata/platform/hip_compat/cublas_v2.h new file mode 100644 index 0000000000..fe8ea15723 --- /dev/null +++ b/include/strata/platform/hip_compat/cublas_v2.h @@ -0,0 +1,26 @@ +// cublas_v2.h for a STRATA_ENABLE_HIP build: the cuBLAS names src/prefill/gemm.cu uses, on hipBLAS. +#pragma once +#include "strata_hip.h" +#ifndef HIPBLAS_V2 +#define HIPBLAS_V2 +#endif +#include + +#define cublasHandle_t hipblasHandle_t +#define cublasStatus_t hipblasStatus_t +#define CUBLAS_STATUS_SUCCESS HIPBLAS_STATUS_SUCCESS +#define cublasCreate hipblasCreate +#define cublasDestroy hipblasDestroy +#define cublasSetStream hipblasSetStream +#define cublasGemmEx hipblasGemmEx +#define CUBLAS_OP_N HIPBLAS_OP_N +#define CUBLAS_OP_T HIPBLAS_OP_T +#define CUBLAS_GEMM_DEFAULT HIPBLAS_GEMM_DEFAULT +#define CUBLAS_COMPUTE_32F HIPBLAS_COMPUTE_32F +#define CUDA_R_32F HIP_R_32F +#define CUDA_R_16F HIP_R_16F +#define CUDA_R_16BF HIP_R_16BF +// rocBLAS picks its own math and workspace; the NVIDIA-only knobs are accepted and ignored +#define CUBLAS_DEFAULT_MATH 0 +#define cublasSetMathMode(handle, mode) HIPBLAS_STATUS_SUCCESS +#define cublasSetWorkspace(handle, ptr, bytes) HIPBLAS_STATUS_SUCCESS diff --git a/include/strata/platform/hip_compat/cuda_fp16.h b/include/strata/platform/hip_compat/cuda_fp16.h new file mode 100644 index 0000000000..4cd2d3c6a5 --- /dev/null +++ b/include/strata/platform/hip_compat/cuda_fp16.h @@ -0,0 +1,2 @@ +#pragma once +#include "strata_hip.h" diff --git a/include/strata/platform/hip_compat/cuda_runtime.h b/include/strata/platform/hip_compat/cuda_runtime.h new file mode 100644 index 0000000000..4cd2d3c6a5 --- /dev/null +++ b/include/strata/platform/hip_compat/cuda_runtime.h @@ -0,0 +1,2 @@ +#pragma once +#include "strata_hip.h" diff --git a/include/strata/platform/hip_compat/math_constants.h b/include/strata/platform/hip_compat/math_constants.h new file mode 100644 index 0000000000..b00d57b9e9 --- /dev/null +++ b/include/strata/platform/hip_compat/math_constants.h @@ -0,0 +1,4 @@ +// math_constants.h for a STRATA_ENABLE_HIP build: the CUDART_* constants the tree uses. +#pragma once +#define CUDART_INF_F __builtin_huge_valf() +#define CUDART_NAN_F __builtin_nanf("") diff --git a/include/strata/platform/hip_compat/strata_hip.h b/include/strata/platform/hip_compat/strata_hip.h new file mode 100644 index 0000000000..b0de6c4a60 --- /dev/null +++ b/include/strata/platform/hip_compat/strata_hip.h @@ -0,0 +1,295 @@ +// strata_hip.h - the CUDA runtime/cuBLAS names Strata uses, mapped onto HIP/hipBLAS for AMD GPUs (gfx906). +// +// This directory is put FIRST on the include path of a STRATA_ENABLE_HIP build only; its `cuda_runtime.h`, +// `cuda_fp16.h` and `cublas_v2.h` include this header instead of the NVIDIA ones, so the engine's sources stay +// one tree. The mapping list follows llama.cpp's ggml-cuda/vendors/hip.h (MIT) where the two overlap. +// +// The warp contract: every Strata kernel is written for a 32-lane warp. gfx906 runs 64-lane wavefronts, so +// here a CUDA warp is a LOGICAL 32-lane half of a wavefront: every shuffle defaults to width 32 (a +// `__shfl_down_sync(m, v, 1)` at lane 31 must not read lane 32), `__ballot_sync` returns the 32-bit mask of the +// caller's own half, and `__syncwarp` is a wavefront barrier plus an LDS fence. That keeps every kernel's +// arithmetic, lane mapping and reduction order exactly as on NVIDIA; wave64-native rewrites of the hot kernels +// are separate, measured changes. +#pragma once + +#ifndef HIP_DISABLE_WARP_SYNC_BUILTINS +#define HIP_DISABLE_WARP_SYNC_BUILTINS 1 +#endif +#include +#include + +#include +#include + +#define STRATA_HIP_GFX906 1 + +// ---- runtime: types and constants ------------------------------------------------------------------------ +#define cudaError_t hipError_t +#define cudaSuccess hipSuccess +#define cudaErrorNotReady hipErrorNotReady +#define cudaErrorMemoryAllocation hipErrorOutOfMemory +#define cudaErrorStreamCaptureUnsupported hipErrorStreamCaptureUnsupported +#define cudaErrorPeerAccessAlreadyEnabled hipErrorPeerAccessAlreadyEnabled +#define cudaStream_t hipStream_t +#define cudaEvent_t hipEvent_t +#define cudaGraph_t hipGraph_t +#define cudaGraphExec_t hipGraphExec_t +#define cudaGraphNode_t hipGraphNode_t +#define cudaDeviceProp hipDeviceProp_t +#define cudaMemcpyKind hipMemcpyKind +#define cudaMemcpyHostToDevice hipMemcpyHostToDevice +#define cudaMemcpyDeviceToHost hipMemcpyDeviceToHost +#define cudaMemcpyDeviceToDevice hipMemcpyDeviceToDevice +#define cudaMemcpyDefault hipMemcpyDefault +#define cudaStreamNonBlocking hipStreamNonBlocking +#define cudaStreamCaptureModeThreadLocal hipStreamCaptureModeThreadLocal +#define cudaStreamCaptureModeRelaxed hipStreamCaptureModeRelaxed +#define cudaStreamCaptureModeGlobal hipStreamCaptureModeGlobal +#define cudaStreamCaptureStatus hipStreamCaptureStatus +#define cudaStreamCaptureStatusNone hipStreamCaptureStatusNone +#define cudaStreamCaptureStatusActive hipStreamCaptureStatusActive +#define cudaEventDisableTiming hipEventDisableTiming +#define cudaEventDefault hipEventDefault +#define cudaHostAllocDefault hipHostMallocDefault +#define cudaHostAllocPortable hipHostMallocPortable +#define cudaHostAllocMapped hipHostMallocMapped +#define cudaHostAllocWriteCombined hipHostMallocWriteCombined +#define cudaHostRegisterDefault hipHostRegisterDefault +#define cudaHostRegisterPortable hipHostRegisterPortable +#define cudaHostRegisterMapped hipHostRegisterMapped +#define cudaHostRegisterReadOnly hipHostRegisterReadOnly +#define cudaDeviceScheduleSpin hipDeviceScheduleSpin +#define cudaDeviceScheduleYield hipDeviceScheduleYield +#define cudaDeviceScheduleBlockingSync hipDeviceScheduleBlockingSync +#define cudaDeviceMapHost hipDeviceMapHost +#define cudaFuncAttributeMaxDynamicSharedMemorySize hipFuncAttributeMaxDynamicSharedMemorySize +#define cudaDevAttrMultiProcessorCount hipDeviceAttributeMultiprocessorCount +#define cudaDevAttrClockRate hipDeviceAttributeClockRate +#define cudaDevAttrComputeCapabilityMajor hipDeviceAttributeComputeCapabilityMajor +#define cudaDevAttrComputeCapabilityMinor hipDeviceAttributeComputeCapabilityMinor +#define cudaDevAttrMaxSharedMemoryPerBlockOptin hipDeviceAttributeSharedMemPerBlockOptin +#define cudaDevAttrMemoryClockRate hipDeviceAttributeMemoryClockRate +#define cudaDevAttrGlobalMemoryBusWidth hipDeviceAttributeMemoryBusWidth + +// ---- runtime: functions ---------------------------------------------------------------------------------- +#define cudaGetErrorString hipGetErrorString +#define cudaGetErrorName hipGetErrorName +#define cudaGetLastError hipGetLastError +#define cudaPeekAtLastError hipPeekAtLastError +#define cudaGetDevice hipGetDevice +#define cudaSetDevice hipSetDevice +#define cudaGetDeviceCount hipGetDeviceCount +#define cudaGetDeviceProperties hipGetDeviceProperties +// The shared-memory opt-in limit: some HIP versions report 0 for it on GCN cards; the real ceiling there is +// the plain per-block limit (64 KB of LDS on gfx906). +inline hipError_t strata_device_get_attribute(int* v, hipDeviceAttribute_t a, int dev) { + hipError_t e = hipDeviceGetAttribute(v, a, dev); + if (e == hipSuccess && a == hipDeviceAttributeSharedMemPerBlockOptin && *v <= 0) + e = hipDeviceGetAttribute(v, hipDeviceAttributeMaxSharedMemoryPerBlock, dev); + return e; +} +#define cudaDeviceGetAttribute strata_device_get_attribute +#define cudaDeviceSynchronize hipDeviceSynchronize +#define cudaDeviceCanAccessPeer hipDeviceCanAccessPeer +#define cudaDeviceEnablePeerAccess hipDeviceEnablePeerAccess +#define cudaDriverGetVersion hipDriverGetVersion +#define cudaRuntimeGetVersion hipRuntimeGetVersion +#define cudaSetDeviceFlags hipSetDeviceFlags +#define cudaMalloc hipMalloc +#define cudaFree hipFree +#define cudaMemGetInfo hipMemGetInfo +#define cudaMemcpy hipMemcpy +#define cudaMemcpyAsync hipMemcpyAsync +#define cudaMemcpyPeerAsync hipMemcpyPeerAsync +#define cudaMemcpy2DAsync hipMemcpy2DAsync +#define cudaMemset hipMemset +#define cudaMemsetAsync hipMemsetAsync +// Mapped host memory is how the GPU and the CPU talk inside a running graph (the doorbells, the CPU experts' +// results, the verify flags): a kernel spins on a word the CPU writes, and the CPU on a word the kernel writes. +// On AMD that needs FINE-GRAINED (coherent) memory - coarse-grained host memory may sit in the GPU's L2 for the +// whole kernel and the spin never sees the other side. So every mapped allocation is made coherent here. +inline hipError_t strata_host_alloc(void** p, size_t bytes, unsigned int flags) { + if (flags & hipHostMallocMapped) flags |= hipHostMallocCoherent; + return hipHostMalloc(p, bytes, flags); +} +template inline hipError_t strata_host_alloc(T** p, size_t bytes, unsigned int flags) { + return strata_host_alloc(reinterpret_cast(p), bytes, flags); +} +#define cudaHostAlloc strata_host_alloc +#define cudaMallocHost(ptr, size) hipHostMalloc(ptr, size, hipHostMallocDefault) +#define cudaFreeHost hipHostFree +#define cudaHostRegister hipHostRegister +#define cudaHostUnregister hipHostUnregister +#define cudaHostGetDevicePointer hipHostGetDevicePointer +#define cudaStreamCreate hipStreamCreate +#define cudaStreamCreateWithFlags hipStreamCreateWithFlags +#define cudaStreamDestroy hipStreamDestroy +#define cudaStreamSynchronize hipStreamSynchronize +#define cudaStreamQuery hipStreamQuery +#define cudaStreamWaitEvent hipStreamWaitEvent +#define cudaStreamBeginCapture hipStreamBeginCapture +#define cudaStreamEndCapture hipStreamEndCapture +#define cudaStreamIsCapturing hipStreamIsCapturing +#define cudaLaunchHostFunc hipLaunchHostFunc +#define cudaEventCreate hipEventCreate +#define cudaEventCreateWithFlags hipEventCreateWithFlags +#define cudaEventDestroy hipEventDestroy +#define cudaEventRecord hipEventRecord +#define cudaEventQuery hipEventQuery +#define cudaEventSynchronize hipEventSynchronize +#define cudaEventElapsedTime hipEventElapsedTime +#define cudaGraphLaunch hipGraphLaunch +#define cudaGraphUpload hipGraphUpload +#define cudaGraphDestroy hipGraphDestroy +#define cudaGraphExecDestroy hipGraphExecDestroy +#define cudaGraphGetNodes hipGraphGetNodes +#define cudaGraphNodeType hipGraphNodeType +#define cudaGraphNodeGetType hipGraphNodeGetType +#define cudaGraphNodeTypeKernel hipGraphNodeTypeKernel +#define cudaGraphNodeTypeMemcpy hipGraphNodeTypeMemcpy +#define cudaGraphNodeTypeMemset hipGraphNodeTypeMemset +#define cudaKernelNodeParams hipKernelNodeParams +#define cudaGraphKernelNodeGetParams hipGraphKernelNodeGetParams +inline hipError_t cudaFuncGetName(const char** name, const void*) { *name = nullptr; return hipErrorNotSupported; } +#define cudaFuncSetAttribute(fn, attr, val) hipFuncSetAttribute(reinterpret_cast(fn), attr, val) +#define cudaMemcpyToSymbol(sym, src, ...) hipMemcpyToSymbol(HIP_SYMBOL(sym), src, __VA_ARGS__) + +// CUDA 12 has a 3-argument cudaGraphInstantiate(exec, graph, flags) and the older 5-argument one; HIP spells +// the first hipGraphInstantiateWithFlags. Both are used in the tree. +inline hipError_t cudaGraphInstantiate(hipGraphExec_t* exec, hipGraph_t graph, unsigned long long flags) { + return hipGraphInstantiateWithFlags(exec, graph, flags); +} +inline hipError_t cudaGraphInstantiate(hipGraphExec_t* exec, hipGraph_t graph, hipGraphNode_t* err_node, + char* log, size_t log_size) { + return hipGraphInstantiate(exec, graph, err_node, log, log_size); +} +// cudaInitDevice(dev, deviceFlags, flags): initialise a device and its scheduling flags WITHOUT making it the +// current device (CUDA's contract; the layer split relies on it - a changed current device put GPU 0's expert +// cache on GPU 1). +inline hipError_t cudaInitDevice(int device, unsigned int device_flags, unsigned int /*flags*/) { + int prev = 0; + hipError_t e = hipGetDevice(&prev); + if (e != hipSuccess) return e; + e = hipSetDevice(device); + if (e != hipSuccess) return e; + if (hipSetDeviceFlags(device_flags) != hipSuccess) (void) hipGetLastError(); // context already active + return hipSetDevice(prev); +} + +// ---- device side ----------------------------------------------------------------------------------------- +#if defined(__HIP_DEVICE_COMPILE__) || defined(__HIPCC__) + +// Logical 32-lane warps (see the header comment). The width argument, when given, wins. +#define __shfl_sync(mask, var, lane, ...) __shfl(var, lane, STRATA_SHFL_W(__VA_ARGS__)) +#define __shfl_up_sync(mask, var, delta, ...) __shfl_up(var, delta, STRATA_SHFL_W(__VA_ARGS__)) +#define __shfl_down_sync(mask, var, delta, ...) __shfl_down(var, delta, STRATA_SHFL_W(__VA_ARGS__)) +#define __shfl_xor_sync(mask, var, lanemask, ...) __shfl_xor(var, lanemask, STRATA_SHFL_W(__VA_ARGS__)) +// STRATA_SHFL_W() -> 32, STRATA_SHFL_W(w) -> w +#define STRATA_SHFL_W(...) STRATA_SHFL_W_I(dummy, ##__VA_ARGS__, STRATA_SHFL_W_GIVEN, STRATA_SHFL_W_DEFAULT)(__VA_ARGS__) +#define STRATA_SHFL_W_I(d, a, b, ...) b +#define STRATA_SHFL_W_GIVEN(w) (w) +#define STRATA_SHFL_W_DEFAULT(...) 32 +#define __all_sync(mask, pred) __all(pred) +#define __any_sync(mask, pred) __any(pred) + +static __device__ __forceinline__ unsigned int strata_ballot32(int pred) { + const unsigned long long b = __ballot(pred); + return (unsigned int) (b >> (__lane_id() & 32u)); +} +#define __ballot_sync(mask, pred) strata_ballot32(pred) +#define __activemask() ((unsigned int) (__ballot(1) >> (__lane_id() & 32u))) + +static __device__ __forceinline__ void strata_syncwarp() { + __builtin_amdgcn_fence(__ATOMIC_RELEASE, "wavefront"); + __builtin_amdgcn_wave_barrier(); + __builtin_amdgcn_fence(__ATOMIC_ACQUIRE, "wavefront"); +} +#define __syncwarp(...) strata_syncwarp() + +// __nanosleep(ns): s_sleep counts 64 clocks per unit (~40 ns at the 1.7 GHz shader clock); a spin loop only +// needs "back off a little", not an exact time. +static __device__ __forceinline__ void strata_nanosleep(unsigned int ns) { + if (ns >= 400) __builtin_amdgcn_s_sleep(8); + else __builtin_amdgcn_s_sleep(1); +} +#define __nanosleep(ns) strata_nanosleep(ns) +#define __trap() __builtin_trap() + +// __dp4a(a, b, c): signed 8-bit dot product plus accumulator; gfx906 has v_dot4_i32_i8. +static __device__ __forceinline__ int strata_dp4a(int a, int b, int c) { +#if defined(__gfx906__) || defined(__gfx908__) || defined(__gfx90a__) || defined(__gfx942__) || defined(__GFX11__) || defined(__GFX12__) + return __builtin_amdgcn_sdot4(a, b, c, false); +#else + const int8_t* va = reinterpret_cast(&a); + const int8_t* vb = reinterpret_cast(&b); + return c + va[0] * vb[0] + va[1] * vb[1] + va[2] * vb[2] + va[3] * vb[3]; +#endif +} +static __device__ __forceinline__ unsigned int strata_dp4a(unsigned int a, unsigned int b, unsigned int c) { + const uint8_t* va = reinterpret_cast(&a); + const uint8_t* vb = reinterpret_cast(&b); + return c + va[0] * vb[0] + va[1] * vb[1] + va[2] * vb[2] + va[3] * vb[3]; +} +#define __dp4a strata_dp4a + +// SIMD-in-a-word byte ops (CUDA semantics: __vsub4 wraps per byte, __vsubss4 saturates, __vcmp*4 give 0xff) +typedef int8_t strata_i8x4 __attribute__((ext_vector_type(4))); +typedef uint8_t strata_u8x4 __attribute__((ext_vector_type(4))); +static __device__ __forceinline__ unsigned int strata_vsub4(unsigned int a, unsigned int b) { + const strata_u8x4 va = __builtin_bit_cast(strata_u8x4, a); + const strata_u8x4 vb = __builtin_bit_cast(strata_u8x4, b); + return __builtin_bit_cast(unsigned int, (strata_u8x4) (va - vb)); +} +static __device__ __forceinline__ int strata_vsubss4(int a, int b) { + const strata_i8x4 va = __builtin_bit_cast(strata_i8x4, a); + const strata_i8x4 vb = __builtin_bit_cast(strata_i8x4, b); + return __builtin_bit_cast(int, (strata_i8x4) __builtin_elementwise_sub_sat(va, vb)); +} +static __device__ __forceinline__ unsigned int strata_vcmpeq4(unsigned int a, unsigned int b) { + const strata_u8x4 va = __builtin_bit_cast(strata_u8x4, a); + const strata_u8x4 vb = __builtin_bit_cast(strata_u8x4, b); + strata_u8x4 c; +#pragma unroll + for (int i = 0; i < 4; ++i) c[i] = va[i] == vb[i] ? 0xff : 0x00; + return __builtin_bit_cast(unsigned int, c); +} +static __device__ __forceinline__ unsigned int strata_vcmpne4(unsigned int a, unsigned int b) { + return ~strata_vcmpeq4(a, b); +} +#define __vsub4(a, b) ((int) strata_vsub4((unsigned int) (a), (unsigned int) (b))) +#define __vsubss4(a, b) strata_vsubss4((int) (a), (int) (b)) +#define __vcmpeq4(a, b) strata_vcmpeq4((unsigned int) (a), (unsigned int) (b)) +#define __vcmpne4(a, b) strata_vcmpne4((unsigned int) (a), (unsigned int) (b)) + + +// __fmul_rn / __fadd_rn / __fsub_rn: on NVIDIA these are never contracted into an FMA (that is their point - the +// engine uses them where a result must match a reference bit for bit). HIP spells them as plain operators, which +// clang's default -ffp-contract=fast-honor-pragmas may fuse. The pragma keeps each one a separate rounding. +static __device__ __forceinline__ float strata_fmul_rn(float a, float b) { +#pragma clang fp contract(off) + return a * b; +} +static __device__ __forceinline__ float strata_fadd_rn(float a, float b) { +#pragma clang fp contract(off) + return a + b; +} +static __device__ __forceinline__ float strata_fsub_rn(float a, float b) { +#pragma clang fp contract(off) + return a - b; +} +#define __fmul_rn(a, b) strata_fmul_rn((a), (b)) +#define __fadd_rn(a, b) strata_fadd_rn((a), (b)) +#define __fsub_rn(a, b) strata_fsub_rn((a), (b)) + + +// __byte_perm(x, y, s): HIP's own version builds an 8-byte array and indexes it - the array lands in SCRATCH +// memory, and every IQ4 codebook lookup went through it (a 0.9 MB IQ4_NL GEMV took 290 us, ~3 GB/s). gfx906 has +// v_perm_b32, which selects bytes of {src0:src1} (src1 low) by byte selectors 0-7: the same operation, one +// instruction. CUDA reads the low 3 bits of each of the four low selector nibbles. +static __device__ __forceinline__ unsigned int strata_byte_perm(unsigned int x, unsigned int y, unsigned int s) { + const unsigned int sel = (s & 0x7u) | ((s & 0x70u) << 4) | ((s & 0x700u) << 8) | ((s & 0x7000u) << 12); + return __builtin_amdgcn_perm(y, x, sel); +} +#define __byte_perm(x, y, s) strata_byte_perm((unsigned int) (x), (unsigned int) (y), (unsigned int) (s)) + +#endif // device side diff --git a/include/strata/platform/memory.hpp b/include/strata/platform/memory.hpp index ece6c9e4a1..202a3bf25b 100644 --- a/include/strata/platform/memory.hpp +++ b/include/strata/platform/memory.hpp @@ -32,4 +32,15 @@ bool gpu_shared_memory_budget(const void* luid, uint64_t& budget, uint64_t& usag /// The machine's physical RAM in bytes (0 when unknown). uint64_t total_physical_memory(); +/// #357/#577: whether the OS file cache could keep the `read_bytes` the expert files are read for, beside +/// `arena_bytes` of RAM held by the engine's own copy of the experts and `margin` for everything else, with `avail` +/// bytes of RAM available. The file tier passes only the expert bytes it really reads from the files (the experts +/// outside its resident RAM copy) and the RAM that copy really holds - not every shard's bytes and the requested +/// budget, which on a 96 GB PC (#577) made the file tier read unbuffered when the cache could keep its reads. +inline bool file_cache_keeps(uint64_t avail, uint64_t arena_bytes, uint64_t read_bytes, + uint64_t margin = 4ull << 30) { + const uint64_t room = avail > arena_bytes + margin ? avail - arena_bytes - margin : 0; + return room >= read_bytes; +} + } // namespace strata::platform diff --git a/include/strata/prefill/gemm.hpp b/include/strata/prefill/gemm.hpp index c9ddc6f4dc..60d1c71748 100644 --- a/include/strata/prefill/gemm.hpp +++ b/include/strata/prefill/gemm.hpp @@ -26,6 +26,8 @@ class Gemm { std::string& err); /// Y[T, N] (fp32, row stride ldy) = X[T, K] (bf16, row-major) . W[N, K]^T (bf16, row-major). `beta` = 1 adds. + /// Below sm_80 (no BF16 tensor cores): Volta converts both to FP16 and runs the FP16 tensor-core GEMM (Turing with + /// STRATA_BF16_TC=1), Pascal widens both to fp32 (cublasSgemm); see gemm.cu. void bf16(const uint16_t* X, const uint16_t* W, float* Y, int64_t T, int64_t N, int64_t K, int64_t ldy = 0, float beta = 0.0f); @@ -52,6 +54,11 @@ class Gemm { void* workspace_ = nullptr; bool external_ = false; void* hipblaslt_state_ = nullptr; + // below sm_80: FP16 (Pascal: fp32) copies of a BF16 product's weight and activation slice (Gemm::bf16) + uint16_t* tc_w_ = nullptr; + int64_t tc_w_elems_ = 0; + uint16_t* tc_x_ = nullptr; + int64_t tc_x_elems_ = 0; }; diff --git a/include/strata/prefill/kernels.hpp b/include/strata/prefill/kernels.hpp index bc4187a44f..3358bea654 100644 --- a/include/strata/prefill/kernels.hpp +++ b/include/strata/prefill/kernels.hpp @@ -60,6 +60,9 @@ void swiglu_pair(const float* g, const float* u, uint16_t* h16, int64_t n, void* /// dst[i] = src[i] for n int32s, as a kernel: either side may be mapped host memory, and the copy never waits /// behind the copy engine's queue (the prompt path's grouping tables, while the expert stream fills it). void copy_i32(int32_t* dst, const int32_t* src, int64_t n, void* stream); +/// dst[0, n) = src[0, n) in 16-byte loads (both 16-byte aligned; src may be mapped host memory: read over PCIe by +/// the kernel, not queued on a copy engine). +void copy_f32_wide(float* dst, const float* src, int64_t n, void* stream); /// Gather rows: dst16[i, :] = x16[src[i], :] (n rows of `width` BF16). void gather_rows16(const uint16_t* x16, const int32_t* src, uint16_t* dst16, int64_t n, int64_t width, void* stream); /// bo[t, :] = shared[t, :] * sigmoid(sg[t]) + sum_k w[t, k] * D[slot[t, k], :] diff --git a/include/strata/prefill/prefill.hpp b/include/strata/prefill/prefill.hpp index 038e0dd953..15df6e4e40 100644 --- a/include/strata/prefill/prefill.hpp +++ b/include/strata/prefill/prefill.hpp @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -78,10 +79,37 @@ class Prefill { /// #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); + /// #583: the auto chunk scan's byte-budget ring, for chunks above `small_max` (0.1.39's auto chunk: a prompt that + /// fits it keeps 0.1.39's ring). 0 slots = none. A layer split's set_ring_override and STRATA_PREFILL_RING win. + static void set_ring_budget(int slots, int64_t small_max); /// 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); + /// The same without the streamed ring: what the chunk's own buffers cost. The auto chunk scan sizes the chunk + /// first and hands the ring what the chunk leaves over, so it needs the chunk priced on its own. + static uint64_t bytes_needed_no_ring(const core::ModelGeometry& g, const core::SessionState& ss, int64_t chunk); + + /// The streamed ring's byte budget as a slot count for this pack (the measured slot count x Q2_0's blob, over + /// max_blob, never past ring_cap()): + /// what the auto chunk scan treats as a full ring. A slot is one whole blob, so a pack with bigger blobs than + /// Q2_0's gets fewer of them for the same bytes - 384 on Q2_0, 199 on a 2.54 MiB-blob IQ3_S pack. + static int64_t ring_max_slots(); + /// 0.1.39's ring for this PC (1024 fused / 384 pinned / 96), within ring_cap(). + static int64_t ring_default_slots(); + /// #583: the ring the auto scan keeps full, given the chunk 0.1.39's rule picked: its byte budget where that rule's + /// chunk was small (< 6144), else 0.1.39's ring (measured: shrinking it for a bigger chunk lost there). + static int64_t ring_cap_for(int64_t old_chunk); + + /// What the ring actually resolves to for a chunk of `chunk` tokens, after the override, STRATA_PREFILL_RING + /// and the pinned-share rule - the slot count `init` lays out. The engine reports it on its INFO line so the + /// Monitor tab shows the pair the run really got, not what it asked for. + static int64_t ring_slots_for(int64_t chunk); + + /// 0.1.39b (#583, the default): the ring as a byte budget, the loan's corrected count and the auto chunk scan that + /// keeps the ring full. STRATA_RING_BYTES=0: 0.1.39's ring, loan and chunk list. + static bool ring_bytes_enabled(); + /// Positions [pos0, pos0 + n) holding `tokens`; `ss.ple_prev` must be the two tokens before pos0 (oldest /// first, -1 for none) and is advanced to the last two of these. bool run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& err); @@ -120,10 +148,32 @@ class Prefill { stage_lb_ = layer_begin; stage_le_ = layer_end; next_ = next; } + /// LAYER SPLIT: `helper` is another stage's prompt path (another GPU). A prompt of one chunk runs the stages one + /// after the other, so while this stage reads it the helper's GPU idles: it then streams a share of this stage's + /// non-resident experts over its own PCIe link into its own (lent) prompt buffers, computes their rows, and sends + /// them back - the --peer-device peer's streaming, without P2P (activations and rows through mapped host memory, + /// read by copy kernels). Only on the MMQ prompt path, only for chunks of stream_all_min() tokens and more, up to + /// the size where the measured share stops paying. Opt-in: STRATA_PREFILL_HELP=1 (not bit-identical to the default). Both stages must have + /// run `init`. + bool set_stage_helper(Prefill* helper, std::string& err); + private: + // Stage-1 pipeline: intermediate stages return after handing their chunk to + // the direct successor. The public run() drains the chain once at prompt end. + bool run_impl(const int64_t* tokens, int64_t n, int64_t pos0, std::string& err); + bool drain_pipeline(std::string& err); + int64_t stage_lb_ = 0, stage_le_ = -1; Prefill* next_ = nullptr; + Prefill* helper_ = nullptr; ///< set_stage_helper + bool single_chunk_ = false; ///< a later stage: the prompt is one chunk (set by the stage before) + bool bind_stage_helper(int64_t T); // binds the helper's buffers for a one-chunk prompt of T tokens const float* hand_in_ = nullptr; ///< the previous stage's rows of the chunk being read (host, pinned) + + std::string next_err_; + std::future next_run_; + int hand_buf_ = 0; + bool carve(std::size_t T, void* alloc); // the device buffers of a chunk (prefill.cpp's Alloc) void release(); // the destructor's cleanup (also `reset`'s) struct Impl; diff --git a/serve/frontend.py b/serve/frontend.py index a156889b36..7a98ad4fa4 100644 --- a/serve/frontend.py +++ b/serve/frontend.py @@ -16,6 +16,7 @@ from __future__ import annotations import json +import re import uuid from dataclasses import dataclass, field from pathlib import Path @@ -135,6 +136,81 @@ def images_of(messages: list[dict]) -> list[str]: for item in m["content"] if item.get("type") == "image"] +# #537: a literal / inside a message's text is plain text, not the model's reasoning markers. The +# tokenizer matches those two strings as their special tokens everywhere (GGUF token type 4, as llama.cpp does), so +# a user quoting "" used to hand the model a real end-of-reasoning token. Before the template is rendered +# they are swapped for these private-use characters, and the server encodes the spans they mark as ordinary text. +THINK_TAGS = {"": "\U000F0E01", "": "\U000F0E02"} +THINK_MARKS = {v: k for k, v in THINK_TAGS.items()} + + +def _mark(text: str) -> str: + for tag, mark in THINK_TAGS.items(): + text = text.replace(tag, mark) + return text + + +def _mark_deep(v): + if isinstance(v, str): + return _mark(v) + if isinstance(v, dict): + return {k: _mark_deep(x) for k, x in v.items()} + if isinstance(v, list): + return [_mark_deep(x) for x in v] + return v + + +def _has_tag(v) -> bool: + if isinstance(v, str): + return "" in v or "" in v + if isinstance(v, dict): + return any(_has_tag(x) for x in v.values()) + if isinstance(v, list): + return any(_has_tag(x) for x in v) + return False + + +def mark_think_literals(messages: list[dict], tools: list[dict] | None): + """#537: (messages, tools) with every literal / in their text swapped for THINK_TAGS' marks, and + whether there was one (None: no change, the same objects back - a prompt without them renders as it always did). + An assistant message whose content opens with a whole ... block (clients that send the reasoning + inline) keeps that one block as the model's markers, as before.""" + if not _has_tag(messages) and not _has_tag(tools): + return messages, tools, False + out = [] + for m in messages: + m = dict(m) + content = m.get("content") + for k, v in m.items(): + if k != "role": + m[k] = _mark_deep(v) + if m.get("role") == "assistant" and isinstance(content, str) and content.lstrip().startswith("") \ + and "" in content: + i, j = content.index("") + len(""), content.index("") + m["content"] = content[:i] + _mark(content[i:j]) + "" + _mark(content[j + len(""):]) + out.append(m) + return out, _mark_deep(tools), True + + +_THINK_MARK_RE = re.compile("|".join(THINK_MARKS)) + + +def unmark_think_literals(prompt: str) -> tuple[str, list[tuple[int, int]]]: + """The rendered prompt with THINK_TAGS' marks turned back into the tags' text, and the (start, end) spans of those + tags in it: the server encodes them as ordinary text (the tokenizer's encode_plain_spans).""" + out, spans, pos, n = [], [], 0, 0 + for m in _THINK_MARK_RE.finditer(prompt): + out.append(prompt[pos:m.start()]) + n += m.start() - pos + tag = THINK_MARKS[m.group(0)] + out.append(tag) + spans.append((n, n + len(tag))) + n += len(tag) + pos = m.end() + out.append(prompt[pos:]) + return "".join(out), spans + + def _late_system_to_user(messages: list[dict]) -> list[dict]: """The chat template takes a system message only at the start ("System message must be at the beginning"). Clients also send them mid-conversation - Claude Code's hook context as {"role": "system"} after the first user @@ -160,6 +236,27 @@ def _object_list(value, name: str) -> list[dict]: return value +def _tool_list(value, wrapper: str | None) -> list[dict]: + """#592: a request's "tools" as a list of tool objects, each with a name - in the OpenAI shape + {"type": "function", "function": {"name": ...}} (`wrapper` "function"; a bare {"name": ...} is still taken), or + in the Anthropic shape {"name": ...} (`wrapper` None). A value that is not (a string such as "auto", a list of + names, an object without a name) is a ValueError - a 400 naming the field - where it used to take the request + thread down with no reply at all. No value (or an empty one) is no tools, as always.""" + if not value: + return [] + shape = ('{"type": "function", "function": {"name": ..., "parameters": {...}}}' if wrapper else + '{"name": ..., "input_schema": {...}}') + try: + tools = _object_list(value, "tools") + except ValueError: + raise ValueError(f"tools must be a list of tool objects, each {shape}") from None + for i, t in enumerate(tools): + fn = t.get(wrapper, t) if wrapper and t.get("type") == wrapper else t + if not isinstance(fn, dict) or not isinstance(fn.get("name"), str) or not fn["name"]: + raise ValueError(f"tools[{i}] has no name: each tool must be {shape}") + return tools + + def openai_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dict]: """OpenAI Chat Completions -> (template messages, template tools, template kwargs).""" messages = [] @@ -182,10 +279,10 @@ def openai_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dict]: calls.append({"function": {"name": fn.get("name"), "arguments": args or {}}}) out["tool_calls"] = calls messages.append(out) - tools = [t.get("function", t) if isinstance(t, dict) and t.get("type") == "function" else t - for t in req.get("tools") or []] or None + tools = [t.get("function", t) if t.get("type") == "function" else t + for t in _tool_list(req.get("tools"), "function")] or None - # OpenAI tool_choice compatibility. Normalize the API-specific field before rendering the chat template. + # OpenAI tool_choice compatibility. Normalize the API-specific field before rendering the chat template. choice = req.get("tool_choice") forced = None if choice is None or choice == "auto": @@ -210,7 +307,6 @@ def openai_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dict]: raise ValueError("tool_choice must be 'auto', 'none', 'required', or a function selection") if forced: messages.insert(0, {"role": "system", "content": forced}) - kwargs = {} # OpenAI Chat Completions: "reasoning_effort"; Responses style: "reasoning": {"effort": ...} reasoning = req.get("reasoning") if isinstance(req.get("reasoning"), dict) else {} @@ -260,7 +356,7 @@ def anthropic_to_messages(req: dict, think_unasked: bool = True) -> tuple[list[d out["tool_calls"] = calls messages.append(out) tools = [{"name": t["name"], "description": t.get("description", ""), "parameters": t.get("input_schema", {})} - for t in req.get("tools") or []] or None + for t in _tool_list(req.get("tools"), None)] or None kwargs = {} # Anthropic: "thinking": {"type": "disabled"} or {"type": "enabled", "budget_tokens": N}; # "output_config": {"effort": "low" | "medium" | "high"} diff --git a/serve/multigpu_server.py b/serve/multigpu_server.py index 2664a36aaa..35398e936f 100644 --- a/serve/multigpu_server.py +++ b/serve/multigpu_server.py @@ -42,7 +42,7 @@ "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade", } -GENERATE_PATHS = {"/v1/chat/completions", "/v1/messages"} +GENERATE_PATHS = {"/v1/chat/completions", "/v1/messages", "/v1/responses"} VISION_METADATA_PATHS = {"/health", "/props", "/models", "/v1/models"} AFFINITY_HEADERS = ("x-strata-session-id", "x-conversation-id", "x-session-id", "x-thread-id") AFFINITY_FIELDS = ("conversation_id", "session_id", "thread_id") @@ -94,6 +94,7 @@ def sanitize_lane_config(cfg: dict, *, allow_profile_persistence: bool = False, lane_cfg = copy.deepcopy(cfg) lane_cfg.pop("gpu", None) lane_cfg.pop("layer_split", None) + lane_cfg.pop("parallel", None) if not allow_profile_persistence: # server.py can synthesize the CLI writer flags from these top-level # keys, so stripping only cfg["args"] would still leave several lanes @@ -118,9 +119,13 @@ def sanitize_lane_config(cfg: dict, *, allow_profile_persistence: bool = False, "--expert-cache-device2", "--expert-cache-device3", "--expert-cache-remote-placement", + "--batch", + "--slots", + "--batch-groups", ): args = remove_option(args, name) args = remove_flag(args, "--split-skip-if-fits") + args = remove_flag(args, "--trim-stage-weights") # Upstream 0.1.36 can persist an adaptive expert profile. A copied # parent path would give several lane processes the same writer, so diff --git a/serve/responses.py b/serve/responses.py new file mode 100644 index 0000000000..4a017c38d8 --- /dev/null +++ b/serve/responses.py @@ -0,0 +1,538 @@ +"""serve/responses.py - #451: OpenAI's Responses API (POST /v1/responses), stateless, on the chat path. + +A Responses request becomes the same template messages, tools and kwargs a Chat Completions request does, runs through +the same Service.run, and its events (reasoning, text, tool calls) come back as Responses output items and typed SSE +events. Nothing is stored: the client sends the whole conversation in `input` every time (`store: false`, as Codex CLI +does), including the reasoning, message and function_call items of earlier answers. + +Codex CLI is the first client. What it needs, and what this does: + * `instructions` and `developer` messages -> the system message (leading ones merged, so the prompt start is stable + and the conversation cache holds across turns); + * function tools, also inside `namespace` tools (Codex's MCP and agent tools: the model sees `namespace.name`) and + `custom` tools (a free-form `input` string); hosted tools (web search, ...) are left out, the model cannot run them; + * reasoning items: the model's thinking is returned as `reasoning_text` content (no summaries: this model does not + write them, see docs/DETAILS.md) and, when `include` asks for `reasoning.encrypted_content`, also as an opaque + `encrypted_content` string (base64, not encrypted: the client already holds the text). Either form sent back is + put into the conversation again, so the prompt matches what the model wrote and the cache is reused; + * the official event order: response.created, response.in_progress, response.output_item.added, deltas, + ...done events, response.output_item.done, and response.completed / response.incomplete / response.failed. + +Based on #630 by CC-David-CC (the item/event assembler, namespace tools, call_id ordering of tool results). +""" +from __future__ import annotations + +import base64 +import copy +import json +import time +import uuid + +from serve.frontend import Event, _late_system_to_user, _parts_of, effort_kwargs + +ENCRYPTED_PREFIX = "strata.r1:" # our own reasoning replay strings; others' are ignored +HOSTED_TOOLS = ("web_search", "web_search_preview", "file_search", "computer_use_preview", "computer_use", + "code_interpreter", "image_generation", "local_shell", "mcp", "tool_search") + + +class ResponsesError(ValueError): + """A request error in the Responses format: {"error": {"message", "type", "param", "code"}}.""" + + def __init__(self, message, param=None, code=None, status=400, kind="invalid_request_error"): + super().__init__(message) + self.param, self.code, self.status, self.kind = param, code, status, kind + + def body(self): + return error_body(str(self), self.kind, self.param, self.code) + + +def error_body(message, kind="invalid_request_error", param=None, code=None) -> dict: + return {"error": {"message": message, "type": kind, "param": param, "code": code}} + + +def new_id(prefix: str) -> str: + return f"{prefix}_{uuid.uuid4().hex}" + + +# ------------------------------------------------------------------------------------------------ request -> chat +def _content(content, param): + """A message's (or a tool output's) content -> the template's form: a string, or text and image parts.""" + if content is None: + return "" + if isinstance(content, str): + return content + if not isinstance(content, list): + raise ResponsesError("expected a string or an array of content parts", param) + parts = [] + for j, part in enumerate(content): + if isinstance(part, str): + parts.append({"type": "text", "text": part}) + continue + if not isinstance(part, dict): + raise ResponsesError("expected a content part object", f"{param}[{j}]") + kind = part.get("type") + if kind in ("input_text", "output_text", "text", "summary_text", "reasoning_text"): + parts.append({"type": "text", "text": part.get("text") or ""}) + elif kind == "refusal": + parts.append({"type": "text", "text": part.get("refusal") or ""}) + elif kind in ("input_image", "image_url"): + if not part.get("image_url"): + raise ResponsesError("only images given as image_url (a data: or http(s) URL) are supported; " + "this server keeps no files", f"{param}[{j}]", "unsupported_parameter") + parts.append({"type": "input_image", "image_url": part["image_url"]}) + else: + raise ResponsesError(f"content parts of type {kind!r} are not supported (text and images are)", + f"{param}[{j}].type", "unsupported_parameter") + return _parts_of(parts) + + +def _reasoning_text(item, param) -> str: + """The thinking a reasoning item carries: its reasoning_text content, else our own encrypted_content. A summary + alone, or another server's encrypted_content, carries none that can go back into the prompt.""" + texts = [p.get("text") or "" for p in item.get("content") or [] if isinstance(p, dict) + and p.get("type") in ("reasoning_text", "text")] + if texts: + return "".join(texts) + enc = item.get("encrypted_content") + if isinstance(enc, str) and enc.startswith(ENCRYPTED_PREFIX): + try: + return json.loads(base64.b64decode(enc[len(ENCRYPTED_PREFIX):], validate=True).decode("utf-8"))["text"] + except (ValueError, KeyError, TypeError, UnicodeError): + raise ResponsesError("this reasoning item's encrypted_content is damaged", param + ".encrypted_content", + "invalid_encrypted_content") from None + return "" + + +def _arguments(raw, param) -> dict: + """A function_call's arguments (a JSON string) -> the mapping the template renders. Arguments that are not a JSON + object (a call the output ended inside) are kept as text rather than refusing the whole conversation.""" + if isinstance(raw, dict): + return raw + if raw is None or (isinstance(raw, str) and not raw.strip()): + return {} + if not isinstance(raw, str): + raise ResponsesError("function_call arguments must be a JSON string", param) + try: + value = json.loads(raw) + except ValueError: + return {"arguments": raw} + return value if isinstance(value, dict) else {"arguments": value} + + +def _tool_output(item, param): + out = item.get("output") + if isinstance(out, dict): # some clients send a JSON result as an object + return json.dumps(out, ensure_ascii=False) + return out if isinstance(out, str) else _content(out, param + ".output") + + +def input_messages(req: dict) -> list[dict]: + """`instructions` + `input` -> the chat template's messages.""" + messages = [] + instructions = req.get("instructions") + if instructions not in (None, ""): + messages.append({"role": "system", "content": _content(instructions, "instructions") + if not isinstance(instructions, str) else instructions}) + items = req.get("input") + if items is None: + raise ResponsesError("input is required: a string or an array of input items", "input", "missing_required_parameter") + if isinstance(items, str): + items = [{"type": "message", "role": "user", "content": items}] + if not isinstance(items, list): + raise ResponsesError("input must be a string or an array of input items", "input") + thinking = [] # reasoning waiting for the assistant turn it belongs to + open_turn = None # the assistant message the next function_call joins + + def turn(content=""): + nonlocal open_turn + open_turn = {"role": "assistant", "content": content} + if thinking: + open_turn["reasoning_content"] = "".join(thinking) + thinking.clear() + messages.append(open_turn) + return open_turn + + for i, item in enumerate(items): + param = f"input[{i}]" + if not isinstance(item, dict): + raise ResponsesError("expected an input item object", param) + kind = item.get("type") or ("message" if "role" in item else None) + if kind == "message": + role = item.get("role") + if role not in ("user", "assistant", "system", "developer"): + raise ResponsesError(f"unknown message role {role!r}", param + ".role") + content = _content(item.get("content"), param + ".content") + if role == "assistant": + turn(content if isinstance(content, str) else "".join( + p.get("text", "") for p in content if p.get("type") == "text")) + else: + open_turn = None + thinking.clear() + messages.append({"role": "system" if role == "developer" else role, "content": content}) + elif kind == "reasoning": + text = _reasoning_text(item, param) + if text: + if open_turn is not None and (open_turn.get("tool_calls") or open_turn["content"]): + open_turn = None # thinking starts a new model turn + thinking.append(text) + elif kind in ("function_call", "custom_tool_call"): + name = item.get("name") + if not isinstance(name, str) or not name: + raise ResponsesError("a function_call needs a name", param + ".name") + if item.get("namespace"): + name = f"{item['namespace']}.{name}" + if kind == "function_call": + args = _arguments(item.get("arguments"), param + ".arguments") + else: + args = {"input": item.get("input") if isinstance(item.get("input"), str) else ""} + target = open_turn if open_turn is not None and not thinking else turn() + target.setdefault("tool_calls", []).append({"function": {"name": name, "arguments": args}, + "_call_id": item.get("call_id")}) + elif kind in ("function_call_output", "custom_tool_call_output"): + messages.append({"role": "tool", "content": _tool_output(item, param), "_call_id": item.get("call_id")}) + open_turn = None + thinking.clear() + elif kind == "item_reference": + raise ResponsesError("item references need stored responses, and this server keeps none: send the items " + "themselves", param, "unsupported_parameter") + else: + raise ResponsesError(f"input items of type {kind!r} are not supported", param + ".type", + "unsupported_parameter") + _order_tool_results(messages) + # leading system/developer messages (Codex: instructions, then its developer message) become one system message + # at the start; later ones become user messages in place, as on the chat path + lead = 0 + while lead < len(messages) and messages[lead]["role"] == "system" and isinstance(messages[lead]["content"], str): + lead += 1 + if lead > 1: + messages[:lead] = [{"role": "system", "content": "\n\n".join(m["content"] for m in messages[:lead])}] + return _late_system_to_user(messages) + + +def _order_tool_results(messages): + """The template places tool results by position: each run of tool messages is put in the order of the calls of + the assistant turn before it (a client may send the results in the order they finished), then the private call + ids are dropped.""" + i = 0 + while i < len(messages): + if messages[i]["role"] != "tool": + i += 1 + continue + j = i + while j < len(messages) and messages[j]["role"] == "tool": + j += 1 + prev = messages[i - 1] if i else {} + order = [c.get("_call_id") for c in prev.get("tool_calls") or []] + rank = {cid: k for k, cid in enumerate(order) if cid} + messages[i:j] = sorted(messages[i:j], key=lambda m: rank.get(m.get("_call_id"), len(order))) + i = j + for m in messages: + m.pop("_call_id", None) + for c in m.get("tool_calls") or []: + c.pop("_call_id", None) + + +def request_tools(req: dict): + """-> (template tools or None, {template name: (namespace, name, kind)}, names of hosted tools left out).""" + tools, names, skipped = [], {}, [] + given = req.get("tools") or [] + if not isinstance(given, list): + raise ResponsesError("tools must be an array", "tools") + + def add(tool, param, namespace=None, ns_description=""): + kind = tool.get("type") + name = tool.get("name") + if not isinstance(name, str) or not name: + raise ResponsesError("a tool needs a name", param + ".name") + flat = f"{namespace}.{name}" if namespace else name + description = tool.get("description") or "" + if ns_description: + description = ns_description + ("\n\n" + description if description else "") + if kind == "function": + params = tool.get("parameters") or {"type": "object", "properties": {}} + else: # custom: free-form text, maybe in a grammar the model reads + fmt = tool.get("format") or {} + if isinstance(fmt, dict) and fmt.get("type") == "grammar" and fmt.get("definition"): + description += f"\n\nThe input must follow this {fmt.get('syntax', '')} grammar:\n{fmt['definition']}" + params = {"type": "object", "properties": {"input": {"type": "string", "description": "the raw input"}}, + "required": ["input"]} + tools.append({"name": flat, "description": description, "parameters": params}) + names[flat] = (namespace, name, kind) + + for i, tool in enumerate(given): + param = f"tools[{i}]" + if not isinstance(tool, dict): + raise ResponsesError("expected a tool object", param) + kind = tool.get("type") + if kind in ("function", "custom"): + add(tool, param) + elif kind == "namespace": + for j, member in enumerate(tool.get("tools") or []): + if not isinstance(member, dict) or member.get("type", "function") not in ("function", "custom"): + raise ResponsesError("a namespace holds function tools", f"{param}.tools[{j}]") + add({"type": "function", **member}, f"{param}.tools[{j}]", tool.get("name"), + tool.get("description") or "") + elif kind in HOSTED_TOOLS or isinstance(kind, str): + skipped.append(kind) # the model cannot run OpenAI's hosted tools: left out + else: + raise ResponsesError("a tool needs a type", param + ".type") + choice = req.get("tool_choice") + if choice == "none": + return None, names, skipped + return tools or None, names, skipped + + +def text_format(req: dict): + """`text.format` -> the chat path's response_format (None: plain text).""" + text = req.get("text") + if text is None: + return None + if not isinstance(text, dict): + raise ResponsesError("text must be an object", "text") + fmt = text.get("format") + if fmt is None: + return None + if not isinstance(fmt, dict): + raise ResponsesError("text.format must be an object", "text.format") + kind = fmt.get("type") + if kind in (None, "text"): + return None + if kind == "json_object": + return {"type": "json_object"} + if kind == "json_schema": + spec = {k: fmt[k] for k in ("name", "schema", "strict", "description") if k in fmt} + return {"type": "json_schema", "json_schema": spec} + raise ResponsesError("text.format.type must be text, json_object or json_schema", "text.format.type") + + +def check_request(req: dict) -> None: + """What this stateless server cannot do, refused before anything runs.""" + if req.get("previous_response_id"): + raise ResponsesError("this server keeps no responses (stateless): send the whole conversation in input " + "instead of previous_response_id", "previous_response_id", "unsupported_parameter") + if req.get("conversation"): + raise ResponsesError("this server keeps no conversations (stateless): send the whole conversation in input", + "conversation", "unsupported_parameter") + if req.get("background"): + raise ResponsesError("background responses are not supported: use stream or wait for the answer", + "background", "unsupported_parameter") + if req.get("prompt"): + raise ResponsesError("stored prompt templates are not supported: send instructions and input", + "prompt", "unsupported_parameter") + n = req.get("max_output_tokens") + if n is not None and (isinstance(n, bool) or not isinstance(n, int) or n < 1): + raise ResponsesError("max_output_tokens must be a positive whole number", "max_output_tokens") + reasoning = req.get("reasoning") + if reasoning is not None and not isinstance(reasoning, dict): + raise ResponsesError("reasoning must be an object", "reasoning") + + +def template_kwargs(req: dict, shared: dict) -> dict: + effort = (req.get("reasoning") or {}).get("effort") + if effort is None: + effort = shared.get("reasoning_effort") # the Chat settings shared with apps, as on the chat path + try: + return effort_kwargs(effort) + except ValueError as e: + raise ResponsesError(str(e), "reasoning.effort") from None + + +# ------------------------------------------------------------------------------------------------ output +class Assembler: + """One response: its output items and its typed events, built from Service.run's events. The non-streaming + answer is the `response` of the last event, so both modes are the same items.""" + + def __init__(self, req: dict, model: str, prompt_tokens: int, names: dict, encrypted: bool, + json_mode: bool = False): + self.names, self.encrypted, self.json_mode = names, encrypted, json_mode + self.prompt_tokens = prompt_tokens + self.seq = 0 + self.item = None # the open output item + self.json_text = [] # structured output: the answer, held until it is checked + reasoning = req.get("reasoning") if isinstance(req.get("reasoning"), dict) else {} + self.response = { + "id": new_id("resp"), "object": "response", "created_at": int(time.time()), "status": "in_progress", + "background": False, "error": None, "incomplete_details": None, + "instructions": req.get("instructions"), "max_output_tokens": req.get("max_output_tokens"), + "model": model, "output": [], "parallel_tool_calls": req.get("parallel_tool_calls", True), + "previous_response_id": None, + "reasoning": {"effort": reasoning.get("effort"), "summary": reasoning.get("summary")}, + "store": False, "temperature": req.get("temperature"), + "text": req.get("text") if isinstance(req.get("text"), dict) else {"format": {"type": "text"}}, + "tool_choice": req.get("tool_choice", "auto"), "tools": req.get("tools") or [], + "top_p": req.get("top_p"), "truncation": "disabled", "usage": None, "user": None, + "metadata": req.get("metadata") or {}} + + # --- events + def event(self, kind, **data) -> dict: + e = {"type": kind, "sequence_number": self.seq, **data} + self.seq += 1 + return e + + def snapshot(self) -> dict: + return copy.deepcopy(self.response) + + def start(self) -> list[dict]: + return [self.event("response.created", response=self.snapshot()), + self.event("response.in_progress", response=self.snapshot())] + + def in_progress(self) -> dict: + """A keep-alive a client counts as activity (SSE comments are not events: Codex's idle timer ignores them).""" + return self.event("response.in_progress", response=self.snapshot()) + + def _open(self, item) -> list[dict]: + self.response["output"].append(item) + self.item = item + out = [self.event("response.output_item.added", output_index=self.index, item=copy.deepcopy(item))] + if item["type"] == "message": + part = {"type": "output_text", "text": "", "annotations": [], "logprobs": []} + item["content"].append(part) + out.append(self.event("response.content_part.added", item_id=item["id"], output_index=self.index, + content_index=0, part=copy.deepcopy(part))) + return out + + @property + def index(self) -> int: + return len(self.response["output"]) - 1 + + def close(self, status="completed") -> list[dict]: + item, self.item = self.item, None + if item is None: + return [] + at, out = self.response["output"].index(item), [] + item["status"] = status + if item["type"] == "message": + part = item["content"][0] + out += [self.event("response.output_text.done", item_id=item["id"], output_index=at, content_index=0, + text=part["text"], logprobs=[]), + self.event("response.content_part.done", item_id=item["id"], output_index=at, content_index=0, + part=copy.deepcopy(part))] + elif item["type"] == "reasoning": + text = item["content"][0]["text"] + out.append(self.event("response.reasoning_text.done", item_id=item["id"], output_index=at, + content_index=0, text=text)) + if self.encrypted: + item["encrypted_content"] = ENCRYPTED_PREFIX + base64.b64encode( + json.dumps({"text": text}, ensure_ascii=False).encode("utf-8")).decode("ascii") + elif item["type"] == "function_call": + out.append(self.event("response.function_call_arguments.done", item_id=item["id"], output_index=at, + name=item["name"], arguments=item["arguments"])) + elif item["type"] == "custom_tool_call": + out.append(self.event("response.custom_tool_call_input.done", item_id=item["id"], output_index=at, + input=item["input"])) + out.append(self.event("response.output_item.done", output_index=at, item=copy.deepcopy(item))) + return out + + def feed(self, ev: Event) -> list[dict]: + out = [] + kind = ev.kind + if kind == "reasoning": + if not ev.text: + return out + if self.item is None or self.item["type"] != "reasoning": + out += self.close() + out += self._open({"id": new_id("rs"), "type": "reasoning", "status": "in_progress", "summary": [], + "content": [{"type": "reasoning_text", "text": ""}]}) + self.item["content"][0]["text"] += ev.text + out.append(self.event("response.reasoning_text.delta", item_id=self.item["id"], output_index=self.index, + content_index=0, delta=ev.text)) + elif kind == "content": + if not ev.text: + return out + if self.json_mode: # sent once it is checked (finish) + out += self.close() + self.json_text.append(ev.text) + return out + if self.item is None or self.item["type"] != "message": + out += self.close() + out += self._open({"id": new_id("msg"), "type": "message", "status": "in_progress", + "role": "assistant", "content": []}) + self.item["content"][0]["text"] += ev.text + out.append(self.event("response.output_text.delta", item_id=self.item["id"], output_index=self.index, + content_index=0, delta=ev.text, logprobs=[])) + elif kind == "tool_start": + out += self.close() + out += self._open(self._call_item(ev)) + elif kind == "tool_args": + item = self.item + if item is None or item.get("call_id") != ev.call.id: + return out + if item["type"] == "function_call": # a custom tool's input is sent whole, at its end + item["arguments"] += ev.text + out.append(self.event("response.function_call_arguments.delta", item_id=item["id"], + output_index=self.index, delta=ev.text)) + elif kind == "tool_call": + item = self.item + if item is None or item.get("call_id") != ev.call.id: # a call that was not announced while written + out += self.close() + out += self._open(self._call_item(ev)) + item = self.item + if item["type"] == "function_call": + item["arguments"] = json.dumps(ev.call.arguments, ensure_ascii=False) + out.append(self.event("response.function_call_arguments.delta", item_id=item["id"], + output_index=self.index, delta=item["arguments"])) + if item["type"] == "custom_tool_call": + value = ev.call.arguments.get("input") if isinstance(ev.call.arguments, dict) else None + item["input"] = value if isinstance(value, str) else json.dumps(ev.call.arguments, ensure_ascii=False) + out.append(self.event("response.custom_tool_call_input.delta", item_id=item["id"], + output_index=self.index, delta=item["input"])) + out += self.close() + return out + + def _call_item(self, ev: Event) -> dict: + namespace, name, kind = self.names.get(ev.call.name, (None, ev.call.name, "function")) + if kind == "custom": + item = {"id": new_id("ctc"), "type": "custom_tool_call", "status": "in_progress", "call_id": ev.call.id, + "name": name, "input": ""} + else: + item = {"id": new_id("fc"), "type": "function_call", "status": "in_progress", "call_id": ev.call.id, + "name": name, "arguments": ""} + if namespace: + item["namespace"] = namespace + return item + + def finish(self, done: dict, validate=None) -> list[dict]: + """The end: close what is open, then response.completed (or .incomplete when max_output_tokens ran out). + `validate(text, finish)` checks a structured answer and returns its text (or raises).""" + length = done.get("finish") == "length" + # a call still open was cut off (its tool_call never came, #211): incomplete, whatever the finish + unfinished = self.item is not None and self.item["type"] in ("function_call", "custom_tool_call") + out = self.close("incomplete" if length or unfinished else "completed") + if self.json_mode: + text = validate("".join(self.json_text), done.get("finish")) + out += self._open({"id": new_id("msg"), "type": "message", "status": "in_progress", "role": "assistant", + "content": []}) + self.item["content"][0]["text"] = text + out.append(self.event("response.output_text.delta", item_id=self.item["id"], output_index=self.index, + content_index=0, delta=text, logprobs=[])) + out += self.close() + n = done.get("completion_tokens", 0) + prompt = done.get("prompt_tokens", self.prompt_tokens) + self.response["usage"] = { + "input_tokens": prompt, "input_tokens_details": {"cached_tokens": min(done.get("reused") or 0, prompt)}, + "output_tokens": n, "output_tokens_details": {"reasoning_tokens": done.get("reasoning_tokens", 0)}, + "total_tokens": prompt + n} + if length: + self.response["status"] = "incomplete" + self.response["incomplete_details"] = {"reason": "max_output_tokens"} + return out + [self.event("response.incomplete", response=self.snapshot())] + self.response["status"] = "completed" + self.response["completed_at"] = int(time.time()) + return out + [self.event("response.completed", response=self.snapshot())] + + def failed(self, message: str, code: str = "server_error") -> dict: + """response.failed: an error after the stream started (the HTTP status is already sent).""" + if self.item is not None: + self.item["status"] = "incomplete" + self.item = None + self.response["status"] = "failed" + self.response["error"] = {"code": code, "message": message} + return self.event("response.failed", response=self.snapshot()) + + +def collect(events) -> dict: + """The non-streaming answer: the final response object.""" + last = None + for e in events: + if e is not None: + last = e + return last["response"] diff --git a/serve/runconfig.py b/serve/runconfig.py new file mode 100644 index 0000000000..472b00212b --- /dev/null +++ b/serve/runconfig.py @@ -0,0 +1,179 @@ +"""serve/runconfig.py - #564: the web page's Settings view of the run config (strata-.json). + +A short list of documented keys can be read and changed from Strata's own page (GET / POST /config). Everything +else in the file - the keys setup writes, the network and security keys (host, api_key, cors_origins, +trusted_origins, allowed_hosts), the MCP servers and before_load (which run programs), sampling keys not listed +here - is kept as it is: a change touches only the keys it names (as setup run again keeps the user's keys, #629). +The earlier file is kept as strata-.json.bak. The server reads the config when it starts, so a change is +used from the next start on. +""" +from __future__ import annotations + +import json +import os +import shutil +from pathlib import Path + +# (key, kind, help). kind: "bool", "int>=0", "num>=0", ("enum", values), "names", or ("sampling", check) for a key +# of the "sampling" block, ("arg", flag) for an engine option kept in "args". +EDITABLE = [ + ("sampling.temperature", ("sampling", "num>=0"), + "Default temperature for requests that send none (0 = greedy, the default without a sampling block)"), + ("sampling.top_p", ("sampling", "0=0", "Cap the thinking of every request at this many tokens (0 or empty: no cap)"), + ("fit_max_tokens", "bool", "Shorten a max_tokens that does not fit the context instead of answering 400"), + ("anthropic_thinking", ("enum", ["model", "on_request"]), + "Anthropic requests that do not ask for thinking: think as the model does (model) or not (on_request)"), + ("effort_position", ("enum", ["start", "end"]), + "Where a non-default reasoning effort goes: start (the default) or end (keeps the cache when it changes)"), + ("aliases", "names", "Other model names the server lists and answers to (comma-separated)"), + ("idle_unload_s", "num>=0", "Unload the model after this many seconds without requests (0 or empty: never)"), + ("lazy_load", "bool", "Start without loading the model; the first request loads it (text only)"), + ("engine_silence_s", "num>=0", "End a request when the engine says nothing for this long (default 300 s, 0 = wait)"), + ("api_monitor", "bool", "Keep the last 100 requests' prompts and answers in memory for /api-monitor"), + ("open_browser", "bool", "Open the chat page in the browser when the model is ready"), + ("vram_reserve_mib", ("arg", "--vram-reserve-mib"), + "VRAM in MiB the engine leaves free for other programs (engine default 700)"), +] +SPEC = {k: kind for k, kind, _ in EDITABLE} + + +def _arg(cfg: dict, flag: str): + a = cfg.get("args") if isinstance(cfg.get("args"), list) else [] + if flag in a[:-1]: + v = str(a[a.index(flag) + 1]) + return int(v) if v.isdigit() else v + return None + + +def value_of(cfg: dict, key: str): + kind = SPEC[key] + if isinstance(kind, tuple) and kind[0] == "sampling": + s = cfg.get("sampling") + return s.get(key.split(".", 1)[1]) if isinstance(s, dict) else None + if isinstance(kind, tuple) and kind[0] == "arg": + return _arg(cfg, kind[1]) + return cfg.get(key) + + +def view(cfg: dict, path: str | Path) -> dict: + """GET /config: the editable keys with their values (None: not set, the default applies).""" + out = [] + for key, kind, help_ in EDITABLE: + k = kind[0] if isinstance(kind, tuple) else kind + out.append({"key": key, "value": value_of(cfg, key), "help": help_, + "kind": "number" if k in ("sampling", "arg", "int>=0", "num>=0") else k, + **({"choices": kind[1]} if k == "enum" else {})}) + return {"file": Path(path).name, "keys": out, + "note": "Saved to the run config; used from the next start of the model."} + + +def _number(key, v, whole=False, lo=0.0, hi=None, lo_open=False): + if isinstance(v, bool) or not isinstance(v, (int, float)) or (whole and v != int(v)) or \ + (v <= lo if lo_open else v < lo) or (hi is not None and v > hi): + rng = f"{'more than' if lo_open else 'at least'} {lo:g}" + (f" and at most {hi:g}" if hi is not None else "") + raise ValueError(f"{key}: expected a {'whole ' if whole else ''}number, {rng}, not {v!r}") + return int(v) if whole else v + + +def check(key: str, v, cfg: dict): + """The value to store for `key`, or a ValueError naming what is expected. None removes the key (its default).""" + if key not in SPEC: + raise ValueError(f"{key!r} cannot be changed here (only the keys the Settings view lists)") + if v is None: + return None + kind = SPEC[key] + k = kind[0] if isinstance(kind, tuple) else kind + if k == "bool": + if not isinstance(v, bool): + raise ValueError(f"{key}: expected true or false, not {v!r}") + if key == "lazy_load" and v and cfg.get("vision"): + raise ValueError("lazy_load: lazy loading is text-only, and this model reads images (\"vision\")") + return v + if k == "enum": + if v not in kind[1]: + raise ValueError(f"{key}: expected one of {', '.join(kind[1])}, not {v!r}") + return v + if k == "names": + if isinstance(v, str): + v = [x.strip() for x in v.split(",") if x.strip()] + if not isinstance(v, list) or not all(isinstance(x, str) and x.strip() and len(x) <= 128 for x in v): + raise ValueError(f"{key}: expected a list of model names, not {v!r}") + return v or None + if k == "int>=0": + return _number(key, v, whole=True) + if k == "num>=0": + return _number(key, v) + if k == "arg": + return _number(key, v, whole=True) + rule = kind[1] # a sampling key: the server's own rules + if rule == "num>=0": + return float(_number(key, v)) + if rule == "0 tuple[dict, list[str]]: + """The config with `changes` ({key: value or None}) made, and the keys that changed. Every value is checked + first, so a request with one bad value changes nothing (ValueError).""" + if not isinstance(changes, dict) or not changes: + raise ValueError('send {"set": {"": , ...}}') + checked = {k: check(k, v, cfg) for k, v in changes.items()} + new = json.loads(json.dumps(cfg)) # a deep copy: the caller's dict is left alone + changed = [] + for key, v in checked.items(): + if value_of(new, key) == v: + continue + kind = SPEC[key] + if isinstance(kind, tuple) and kind[0] == "sampling": + s = dict(new.get("sampling") or {}) if isinstance(new.get("sampling"), dict) else {} + sub = key.split(".", 1)[1] + if v is None: + s.pop(sub, None) + else: + s[sub] = v + if s: + new["sampling"] = s + else: + new.pop("sampling", None) + elif isinstance(kind, tuple) and kind[0] == "arg": + a = list(new.get("args") or []) + flag = kind[1] + if flag in a[:-1]: + i = a.index(flag) + if v is None: + del a[i:i + 2] + else: + a[i + 1] = str(v) + elif v is not None: + a += [flag, str(v)] + new["args"] = a + elif v is None: + new.pop(key, None) + else: + new[key] = v + changed.append(key) + return new, changed + + +def save(path: str | Path, cfg: dict) -> Path: + """The config written whole (a temporary file moved over the old one), the earlier one kept as .bak.""" + path = Path(path) + bak = path.with_name(path.name + ".bak") + shutil.copyfile(path, bak) + tmp = path.with_name(path.name + ".tmp") + tmp.write_text(json.dumps(cfg, indent=1), encoding="utf-8") + os.replace(tmp, path) + return bak + + +def load(path: str | Path) -> dict: + cfg = json.loads(Path(path).read_text(encoding="utf-8-sig")) + if not isinstance(cfg, dict): + raise ValueError(f"{Path(path).name} is not a JSON object") + return cfg diff --git a/serve/server.py b/serve/server.py index 5a85db024c..9e669827c9 100644 --- a/serve/server.py +++ b/serve/server.py @@ -21,6 +21,7 @@ from __future__ import annotations import argparse +import contextlib import collections import base64 import hashlib @@ -51,14 +52,28 @@ sys.path.insert(0, str(ROOT / "tools")) sys.path.insert(0, str(ROOT)) # run as a script (run-.bat) as well as a module from serve.frontend import (ChatTemplate, Event, OutputParser, anthropic_to_messages, # noqa: E402 - images_of, openai_to_messages) + images_of, mark_think_literals, openai_to_messages, unmark_think_literals) from serve.mcp import McpCancelled, hub_from_config # noqa: E402 +from serve import runconfig # noqa: E402 from serve.winjob import contain # noqa: E402 from serve.structured import StructuredOutputError, prepare_format, validated_json # noqa: E402 +from serve import responses as responses_api # noqa: E402 +from serve.responses import ResponsesError, error_body as responses_error_body # noqa: E402 IM_END = "<|im_end|>" IMAGE_PAD = "<|image_pad|>" +# #458: the trailing effort turn ("effort_position": "end"); low is the template's own sentence, medium (which the +# template says nothing for: no xhigh sentence is medium there) says it, since the xhigh one stays at the top +EFFORT_TURN = "<|im_start|>system\n{}<|im_end|>\n" +EFFORT_TEXT = {"low": "Reasoning effort is set to low. Keep your thinking brief and focused, moving directly to the " + "conclusion without unnecessary elaboration.", + "medium": "Reasoning effort is set to medium. Think as much as the task needs, without unnecessary " + "elaboration."} VISION_START = "<|vision_start|>" +# #606: a reply that repeats one token this many times in a row is ended there ("length"): a model in a loop, or a +# broken state that answers one token forever (an issue saw 36,689 tokens of "!"). The config's "repeat_stop_tokens" +# sets it; 0 turns it off. +REPEAT_STOP_TOKENS = 256 # #123: what closes the thinking when it reaches reasoning_budget_tokens (the model's own end-of-thinking tag after it) REASONING_WRAP_UP = "\n\nI have thought about this long enough; time to give my answer.\n\n\n" LOOPBACK_NAMES = ("localhost", "127.0.0.1", "::1") @@ -292,6 +307,87 @@ def say(key, text): print(f"[strata] still starting ({time.time() - t0:.0f} s) - please wait ...", flush=True) +class ConvCacheLog: + """#596: the engine's conversation cache as its log tells it (the engine writes "strata serve: conversation + cache: parked N tokens ...; parked=P bytes=B evictions=E" and "restored N tokens ...; parked=P bytes=B" to stderr, + which is the log): read on from where it was last read, from the start of the engine's current run.""" + EVENT = re.compile(r"conversation cache: (parked|skipped|restored) (\d+) tokens.*?parked=(\d+) bytes=(\d+)" + r"(?: evictions=(\d+))?") + DROPPED = re.compile(r"conversation cache: dropped \d+ superseded .*?parked=(\d+)") + READ_MAX = 1 << 20 # at most the last MiB of new lines per read + + def __init__(self): + self.key, self.pos = None, 0 + self.reset() + + def reset(self): + self.state = {"parked": 0, "bytes": 0, "evictions": 0, "parks": 0, "restores": 0, "last_event": None, + "last_tokens": None, "last_at": None} + + def poll(self, path, start) -> dict: + """The state after the log's new lines; `start` is where the engine's current run began in it.""" + if not path or start is None: + return dict(self.state) + if self.key != (path, start): # another start of the engine: its cache starts empty + self.key, self.pos = (path, start), start + self.reset() + try: + size = os.path.getsize(path) + if size < self.pos: # the log was emptied or replaced + self.pos = 0 + if size - self.pos > self.READ_MAX: + self.pos = size - self.READ_MAX + with open(path, "rb") as f: + f.seek(self.pos) + data = f.read(size - self.pos) + except OSError: + return dict(self.state) + end = data.rfind(b"\n") + 1 # whole lines only: the rest is read next time + self.pos += end + now = time.time() + for line in data[:end].decode("utf-8", "replace").splitlines(): + if "conversation cache:" not in line: + continue + m = self.EVENT.search(line) + if m: + st = self.state + st["parked"], st["bytes"] = int(m.group(3)), int(m.group(4)) + if m.group(5) is not None: + st["evictions"] = int(m.group(5)) + if m.group(1) != "skipped": + st["parks" if m.group(1) == "parked" else "restores"] += 1 + st["last_event"], st["last_tokens"], st["last_at"] = m.group(1), int(m.group(2)), now + continue + m = self.DROPPED.search(line) + if m: + self.state["parked"] = int(m.group(1)) + return dict(self.state) + + +def conversation_cache_view(info: dict, hist: list, totals: dict, parked: dict) -> dict: + """#596: the Monitor's Conversation cache card: the parked conversations (the engine's opt-in + --conversation-cache-mib: budget, slots, what its log says) and how much of the prompts the cache gave back.""" + mib = info.get("conversation_cache_mib") + last = hist[-1] if hist else None + return {"enabled": isinstance(mib, int) and mib > 0, "budget_mib": mib if isinstance(mib, int) else None, + "slots": info.get("conversation_cache_slots"), **parked, + "requests": len(hist), "requests_reused": sum(1 for r in hist if (r.get("reused") or 0) > 0), + "reused_tokens": totals.get("reused", 0), "prompt_tokens": totals.get("prompt_tokens", 0), + "last_reused": last.get("reused") if last else None, + "last_prompt": last.get("prompt_tokens") if last else None} + + +_BTRACE = bool(os.environ.get("STRATA_BATCH_TRACE")) + + +def btrace(*a): + if _BTRACE: + print("[batch-trace]", threading.get_ident() % 10000, *a, file=sys.stderr, flush=True) + + +EOS_IDS = {248044, 248046} # <|endoftext|>, <|im_end|>: the engine's default --eos-ids + + class StrataEngine: """The resident engine: `strata --serve` reads `GEN ` lines and streams `T ` lines, then `DONE ...`. Requests are serialized by the service's FIFO, so one pipe is enough. @@ -302,6 +398,25 @@ class StrataEngine: """ silence_s = ENGINE_SILENCE_S # #481: main() sets the config's engine_silence_s (survives restart()) silent_note = None # #481: why the server ended a silent engine (death_note says it) + batch = 0 # --batch: the engine's batch slots (0: one request at a time) + + # `last`: the figures of the engine's last DONE line. With batch slots several requests run at once, each on its + # own thread, so each one reads its own request's figures (Service.run records them); one at a time, the one dict. + @property + def last(self): + if self.batch: + tl = self.__dict__.setdefault("_tl", threading.local()) + if not hasattr(tl, "last"): + tl.last = {} + return tl.last + return self.__dict__.get("_last", {}) + + @last.setter + def last(self, value): + self.__dict__["_last"] = value + if self.batch: + tl = self.__dict__.setdefault("_tl", threading.local()) + tl.last = value def __init__(self, exe: str, args: list[str], cwd: str | None = None, log: str | None = None, env: dict | None = None, lazy: bool = False): @@ -328,6 +443,7 @@ def __init__(self, exe: str, args: list[str], cwd: str | None = None, log: str | self.log = open(log, "a", encoding="utf-8") if log else subprocess.DEVNULL loading = threading.Event() # set once READY: the narrator below stops log_start = os.path.getsize(log) if log else 0 # where this start's lines begin (start_failure_hint) + self.log_start = log_start # #596: the Monitor's conversation cache reads from here if log: threading.Thread(target=narrate_start, args=(log, os.path.getsize(log), args, loading), daemon=True).start() @@ -361,6 +477,7 @@ def __init__(self, exe: str, args: list[str], cwd: str | None = None, log: str | pass raise RuntimeError("the engine exited before it was ready" + (f" (see {log})" if log else "") + start_failure_hint(log, log_start) + start_log_tail(log, log_start)) + self.known_ctx = self.max_context # survives a failed restart: requests keep their limit and restart it # (from PR #41, midhatn) a locally built engine can sit next to another release's BUILD.json: engines that # report their own version (INFO engine=, 0.1.8+) win, the manifest stays the fallback for older ones if self.info.get("engine"): @@ -368,16 +485,51 @@ def __init__(self, exe: str, args: list[str], cwd: str | None = None, log: str | self.ended = False # READY: alive from here (restart() set it True, #344) # the engine's stdout on a thread, so a request can wait with a timeout (heartbeats, cancel checks) self.lines: queue.Queue = queue.Queue() + # --batch: concurrent requests in the engine's batch slots (see generate_batched). The engine says how many + # it runs (INFO batch_slots=N: it may have fewer than asked, or none, when they do not fit) + asked = next((int(args[args.index(k) + 1]) for k in ("--batch", "--slots") if k in args), 0) + self.batch = int(self.info.get("batch_slots") or 0) + if asked and self.batch != asked: + print(f"[strata] parallel requests: {asked} asked, the engine runs {self.batch or 'one at a time'} " + "(its log says why)", flush=True) + groups = int(args[args.index("--batch-groups") + 1]) if "--batch-groups" in args else 1 + groups = groups if self.batch and groups > 0 and self.batch % groups == 0 else 1 + gs = self.batch // groups if self.batch else 0 + # slots in the order that spreads requests over the pipeline's groups first: 0, gs, 2gs, .., 1, gs+1, .. + self.slot_order = [g * gs + t for t in range(gs) for g in range(groups)] + self.slot_q = [queue.Queue() for _ in range(self.batch)] + self.slot_busy = [False] * self.batch + # what each slot's sessions hold (prompt + every token a window fed), so a conversation's next turn goes to + # the slot that has its start (the engine checks it again); when the slot was last used + self.slot_held: list[list[int]] = [[] for _ in range(self.batch)] + self.slot_used = [0.0] * self.batch + self.slot_live: list[dict | None] = [None] * self.batch # /metrics: the request in each slot + self.slot_cv = threading.Condition() + self.waiting = 0 # requests waiting for the control lines (ctl) + self.wait_lens: list[list[int]] = [] # ... their prompt lengths (a long read gives way to short ones) + self.ctl_epoch = 0 # how often the control lines were taken + self._yielded = None # (slot, tokens read): the last request on them gave way + self.ctl = threading.Lock() # one admission or solo request on the control lines at a time + self.wlock = threading.Lock() # stdin writes from several request threads self.pump = threading.Thread(target=self._pump, daemon=True) self.pump.start() def _pump(self): proc, lines = self.proc, self.lines # this process's: a restart replaces both (#344) + slot_q = self.slot_q for line in proc.stdout: + if line.startswith(("BT ", "BDONE ")) and slot_q: # --batch: a batch slot's own lines + try: + slot_q[int(line.split()[1])].put(line) + continue + except (IndexError, ValueError): + pass lines.put(line) if self.proc is proc: # a killed engine's pump must not mark its successor dead self.ended = True # its output closed: it is gone, even before the OS says so lines.put(None) + for q in slot_q: + q.put(None) def death_note(self) -> str: """Why the engine most likely ended, from the end of its log: its own watchdog (issue #29), else RAM.""" @@ -420,15 +572,37 @@ def unload(self): self.close() self.unloaded = True - def restart(self): - """Start the engine again (the same command) after it died; the new process has its own line queue.""" + RESTART_RETRY_S = 15.0 # between the tries of restart(): a dying engine's VRAM may take a while to come back + + def restart(self, tries: int = 3): + """Start the engine again (the same command) after it died; the new process has its own line queue. + close() ends and waits for the old process first (EngineStuck when it cannot be ended). A start that still + exits before READY - a dead engine's VRAM can take a while to come back, notably on ROCm - is retried + (PR #637).""" self.close() info = dict(self.info) - # #344: not alive until READY - __init__ sets max_context to 0 and blocks until the engine says READY, and a - # request that saw alive() in that window skipped load() and failed with "context (0)". __init__ clears - # `ended` itself once READY (before its pump thread can set it again). - self.ended = True - self.__init__(*self.spawn) + self.starting = True # prepare() answers 503 "starting" meanwhile (#344) + try: + for i in range(tries): + # #344: not alive until READY - __init__ sets max_context to 0 and blocks until the engine says + # READY, and a request that saw alive() in that window skipped load() and failed with "context + # (0)". __init__ clears `ended` itself once READY (before its pump thread can set it again). + self.ended = True + try: + self.__init__(*self.spawn) + break + except RuntimeError: + try: + self.proc.wait(timeout=60) + except (subprocess.TimeoutExpired, OSError): + pass + if i == tries - 1: + raise + print(f"[strata] the engine did not start (try {i + 1} of {tries}); again in " + f"{self.RESTART_RETRY_S:g} s", flush=True) + time.sleep(self.RESTART_RETRY_S) + finally: + self.starting = False self.info = {**info, **self.info} def _parse_done(self, line): @@ -443,8 +617,39 @@ def _parse_done(self, line): self.last.update(ram_blobs=int(f[11]), file_blobs=int(f[12]), file_mb=float(f[13])) if len(f) >= 15: # #471 (engine 0.1.36+): the prompt tokens actually read self.last.update(prompt_read=int(f[14])) - if len(f) >= 18: # lane-local conversation parking telemetry - self.last.update(parked_conversations=int(f[15]), parked_bytes=int(f[16]), park_evictions=int(f[17])) + if len(f) >= 16: # #588 (engine 0.1.39+): routed experts read over PCIe + self.last.update(offloaded=int(f[15])) + + def vram(self, reserve_mib: int | None, timeout: float = 120.0) -> dict: + """#533: `VRAM ` between requests (the caller holds the service's FIFO): the engine shrinks its + expert cache until that much VRAM is free, or grows it back when more is free; None: back to the reserve it + started with. -> the engine's figures (expert_slots, expert_cache_mib, vram_free_mib, ...); raises ValueError + with the engine's reason (e.g. it was not started with --vram-elastic), EngineDied when it ended.""" + if not self.alive(): + raise EngineDied("the engine is not running") + self.proc.stdin.write("VRAM" + ("" if reserve_mib is None else f" {int(reserve_mib)}") + "\n") + self.proc.stdin.flush() + deadline = time.time() + timeout + while True: + try: + line = self.lines.get(timeout=max(0.1, deadline - time.time())) + except queue.Empty: + raise EngineDied("the engine did not answer the VRAM command") from None + if line is None: + raise EngineDied("the engine ended") + line = line.strip() + if line.startswith("ERR"): + raise ValueError(line[4:].strip() or "the engine refused the VRAM command") + if line.startswith("VRAM "): + out = {} + for kv in line.split()[1:]: + k, _, v = kv.partition("=") + out[k] = int(v) if v.lstrip("-").isdigit() else v + self.info.update({k: out[k] for k in ("expert_slots", "vram_free_mib") if k in out}) + self.info["vram"] = out + return out + if time.time() > deadline: + raise EngineDied("the engine did not answer the VRAM command") @staticmethod def sampling_keys(sampling: dict) -> str: @@ -500,10 +705,414 @@ def projection_key(sampling: dict) -> str: on = sampling.get("experimental_speed_projection") return f" cvec={int(on)}" if isinstance(on, bool) else "" + def _send(self, text: str): + btrace("send>", text[:60]) + try: + with self.wlock: + self.proc.stdin.write(text + "\n") + self.proc.stdin.flush() + except OSError: # the pipe is gone: the engine died (not the client) + raise EngineDied(f"the engine stopped unexpectedly (exit code {self.exit_code()})") from None + + def _control(self, cancel, on_token, stop_when=None): + """Reads the control lines of the request on them (GEN / BGEN), yielding None heartbeats. Calls on_token(id) + for each `T`; returns ("done", None) at DONE, or ("badm", continues) at BADM (after DONE). `stop_when()` + true sends STOP once (the request is then read to its DONE).""" + stopped = False + while True: + try: + line = self.lines.get(timeout=10.0) + except queue.Empty: + if cancel.is_set() and not stopped: + self._send("STOP") + stopped = True + yield None + continue + if line is None: + raise EngineDied(f"the engine stopped unexpectedly (exit code {self.exit_code()})") + if not line.startswith(("PP ", "INFO")): + btrace("ctl<", self._ctl_mode, line.strip()[:60]) + if line.startswith("T "): + on_token(int(line[2:])) + if not stopped and (cancel.is_set() or (stop_when is not None and stop_when())): + self._send("STOP") + stopped = True + yield False # a token is pending (not a heartbeat) + elif line.startswith("PP "): + f = line.split() + if len(f) >= 3 and f[1].isdigit() and f[2].isdigit(): + self.progress = (int(f[1]), int(f[2])) + self.prefill_tok_s_mean = float(f[4]) if len(f) >= 5 else None + yield None + elif line.startswith("DONE"): + self._parse_done(line) + self._last_done = line + elif line.startswith("BADM "): + f = line.split() + self._ctl_result = ("badm", len(f) >= 3 and f[2] == "1") + return + elif line.startswith("YIELDED "): # the read gave way (BYIELD): + f = line.split() + if len(f) >= 3 and f[1].lstrip("-").isdigit() and f[2].isdigit(): + self._yielded = (int(f[1]), int(f[2])) + elif line.startswith("ERR"): + raise ValueError(line[4:].strip()) + if line.startswith("DONE") and self._ctl_mode == "solo": + self._ctl_result = ("done", None) + return + + def _drain_control(self, until: str, timeout: float = 300.0): + """After a consumer left early: read the control lines up to the next `until` line (DONE or BADM) so the next + request does not read this one's leftovers. Returns that line (None: the engine ended or never answered).""" + end = time.monotonic() + timeout + while time.monotonic() < end: + try: + line = self.lines.get(timeout=max(0.1, end - time.monotonic())) + except queue.Empty: + break + if line is None: + return None + if line.startswith("DONE"): + self._parse_done(line) + if line.startswith(until) or line.startswith("ERR"): + return line + return None + + def _release_slot_when_done(self, slot: int, stream: list[int] | None = None): + """A slot whose consumer left (a stop token, a stop string, a disconnect): BSTOP it and free it once the engine + says BDONE (in the background). `stream`: the prompt and every token of it so far - with the tokens still to + come before BDONE, all but the last are what the slot holds then (the next turn of its conversation).""" + try: + self._send(f"BSTOP {slot}") + except EngineDied: + pass + def wait(): + end = time.monotonic() + 600.0 + tail = list(stream or []) + while time.monotonic() < end: + try: + line = self.slot_q[slot].get(timeout=5.0) + except queue.Empty: + continue + if line is None: + tail = [] + break + if line.startswith("BT "): + try: + tail.append(int(line.split()[2])) + except (IndexError, ValueError): + tail = [] + if line.startswith("BDONE "): + break + self.slot_held[slot] = tail[:-1] if stream and tail else [] + with self.slot_cv: + self.slot_busy[slot] = False + self.slot_cv.notify_all() + threading.Thread(target=wait, daemon=True).start() + + YIELDS_MAX = 2 # #656: how often one request's prompt read gives way to a shorter waiting one + + def _take_control(self, cancel, plen: int, after_epoch: int | None = None): + """Waits for the control lines (one prompt read at a time), yielding None heartbeats; False when cancelled. + `after_epoch`: a request whose read gave way lets the requests waiting then go first.""" + entry = [plen] + with self.slot_cv: + self.waiting += 1 + self.wait_lens.append(entry) + beat = time.monotonic() + try: + while True: + with self.slot_cv: + turn = after_epoch is None or self.ctl_epoch > after_epoch or self.waiting <= 1 + if turn and self.ctl.acquire(timeout=0.5): + break + if not turn: + with self.slot_cv: + self.slot_cv.wait(timeout=0.5) + if cancel.is_set(): + return False + if time.monotonic() - beat >= 10.0: # a heartbeat every 10 s, as the engine's own wait + beat = time.monotonic() + yield None + finally: + with self.slot_cv: + self.waiting -= 1 + self.wait_lens.remove(entry) + with self.slot_cv: + self.ctl_epoch += 1 + self.slot_cv.notify_all() + return True + + SOLO_AGAIN_MAX = 2 # how often a request left alone in a slot goes back to the solo path + SOLO_AGAIN_MIN_LEFT = 32 # ... only with at least this many tokens still allowed (max_tokens) + + def _may_go_solo(self, left: int, times: int, embeddings) -> bool: + """A request decoding in a slot that is alone now (no other slot busy, nobody waiting) goes back to the solo + path with its MTP drafts: the engine continues it from the slot's sessions (its slot cache; INFO + slot_cache=1). STRATA_PARALLEL_SOLO=0 keeps it in the slot.""" + if (embeddings or times >= self.SOLO_AGAIN_MAX or left < self.SOLO_AGAIN_MIN_LEFT or + not (self.info or {}).get("slot_cache") or os.environ.get("STRATA_PARALLEL_SOLO") == "0"): + return False + with self.slot_cv: + return sum(1 for b in self.slot_busy if b) == 1 and self.waiting == 0 + + def _shorter_waiting(self, plen: int) -> bool: + """#656: someone waits for the control lines with a prompt under half this one's: worth giving way to.""" + with self.slot_cv: + return any(e[0] * 2 <= plen for e in self.wait_lens) + + def generate_batched(self, ids, max_new, sampling, cancel, embeddings=None): + """--batch. Alone (no slot busy, nobody waiting): the solo path (GEN, with drafts: the fastest stream) + - and when another request arrives meanwhile, this one is STOPped and continues in a batch slot (BGEN with + its prompt + what it generated: the engine reuses that prefix). Otherwise: BGEN into a free slot, then the + slot's own BT lines until BDONE. Several requests run at once; the control lines (prompt reading, admission) + are taken one request at a time - and a long prompt read gives way at a chunk boundary to a waiting request + with a much shorter prompt (#656: `BYIELD `; the part read waits in a slot and the read goes on after). + A consumer that stops early leaves the engine in step: the solo request is STOPped and read to its DONE, an + admission to its BADM, a slot is BSTOPped and freed at its BDONE.""" + self.progress = None + keys = self.sampling_keys(sampling or {}) + out: list[int] = [] + pending: list[int] = [] + prompt, left = list(ids), int(max_new) + ok = yield from self._take_control(cancel, len(prompt)) + if not ok: + return + holding = True + btrace("ctl acquired") + slot, gen0, reserved = None, None, None + phase = "none" # solo -> (admit -> slot) ; "done" once the engine has finished with this request + stop_sent = False + yields, solo_again = 0, 0 + try: + while True: # a request in a slot that is left alone goes back to the solo path + if not holding: + ok = yield from self._take_control(cancel, len(prompt)) + if not ok: + return + holding = True + with self.slot_cv: + alone = not any(self.slot_busy) and self.waiting == 0 + if alone and left > 1: + head = f"GENI {left}{keys} {embeddings}" if embeddings else f"GEN {left}{keys}" + self._send(f"{head} {','.join(str(int(t)) for t in prompt)}") + phase = "solo" + self._ctl_mode, self._ctl_result, self._yielded = "solo", None, None + def others(): + with self.slot_cv: + return self.waiting > 0 + for x in self._control(cancel, pending.append, stop_when=others): + while pending: + t = pending.pop(0) + out.append(t) + yield t + if x is None: # a heartbeat (False: a token, flushed above) + if (reserved is None and not out and not embeddings and yields < self.YIELDS_MAX and + self._shorter_waiting(len(prompt))): + with self.slot_cv: # the slot the part read will wait in + reserved = self.pick_slot(prompt) + if reserved is not None: + self.slot_busy[reserved] = True + if reserved is not None: + self._send(f"BYIELD {reserved}") + yield None + phase = "none" # its DONE is read + while pending: + t = pending.pop(0) + out.append(t) + yield t + if self._yielded is not None and reserved is not None and self._yielded[0] == reserved: + slot, reserved = reserved, None # gave way: the read goes on in that slot (below) + elif reserved is not None: # it did not give way: the slot is free again + with self.slot_cv: + self.slot_busy[reserved] = False + self.slot_cv.notify_all() + reserved = None + if slot is None: + finish = (self.last or {}).get("finish") if isinstance(self.last, dict) else None + left = int(max_new) - len(out) + if cancel.is_set() or left <= 0 or finish in ("stop", "length") or (out and out[-1] in EOS_IDS): + return + prompt = list(ids) + out # promoted: it continues in a batch slot from here + while True: + if slot is None: + # a free slot (they free themselves at BDONE, which needs no control lines): the one that holds + # the start of this prompt (its conversation's last turn), else the one used longest ago + with self.slot_cv: + while True: + slot = self.pick_slot(prompt) + if slot is not None: + self.slot_busy[slot] = True + break + self.slot_cv.wait(timeout=10.0) + if cancel.is_set(): + return + if self._yielded is not None: # it gave way: the others waiting then go first + self.slot_held[slot] = list(prompt[:self._yielded[1]]) + self._yielded = None + yields += 1 + self.ctl.release() + holding = False + with self.slot_cv: + epoch = self.ctl_epoch + ok = yield from self._take_control(cancel, len(prompt), after_epoch=epoch) + if not ok: + return + holding = True + while not self.slot_q[slot].empty(): + self.slot_q[slot].get_nowait() + head = f"BGENI {slot} {left}{keys} {embeddings}" if embeddings else f"BGEN {slot} {left}{keys}" + live = {"slot": slot, "state": "reading", "prompt_tokens": len(prompt), "generated": 0, + "started": time.time(), "first_token": None} + self.slot_live[slot] = live + self._send(f"{head} {','.join(str(int(t)) for t in prompt)}") + self.slot_held[slot] = [] # the admission overwrites what the slot held + phase = "admit" + self._ctl_mode, self._ctl_result, self._yielded = "batch", None, None + asked = False + for x in self._control(cancel, pending.append): + while pending: + t = pending.pop(0) + out.append(t) + yield t + if x is None: + if (not asked and not embeddings and yields < self.YIELDS_MAX and + self._shorter_waiting(len(prompt))): + self._send(f"BYIELD {slot}") + asked = True + yield None + cont = bool(self._ctl_result and self._ctl_result[1]) + phase = "slot" if cont else "none" + while pending: + t = pending.pop(0) + out.append(t) + yield t + if not cont and self._yielded is not None and self._yielded[0] == slot and not cancel.is_set(): + continue # gave way: again once the shorter request is in + break + self.ctl.release() + holding = False + if not cont: + return + live.update(state="decoding", first_token=time.time(), generated=len(out)) + gen0 = len(out) - 1 # the admission's own token: the slot feeds it first + going_solo = False + while True: + try: + line = self.slot_q[slot].get(timeout=10.0) + except queue.Empty: + if cancel.is_set() and not stop_sent: + self._send(f"BSTOP {slot}") + stop_sent = True + yield None + continue + if line is None: + phase = "none" + raise EngineDied(f"the engine stopped unexpectedly (exit code {self.exit_code()})") + if line.startswith("BT "): + t = int(line.split()[2]) + out.append(t) + live["generated"] = len(out) + if cancel.is_set(): + if not stop_sent: + self._send(f"BSTOP {slot}") + stop_sent = True + continue + yield t + if not going_solo and not stop_sent and self._may_go_solo(int(max_new) - len(out), solo_again, + embeddings): + self._send(f"BSTOP {slot}") # alone now: on with the drafts (below) + going_solo = True + elif line.startswith("BDONE "): + phase = "none" + f = line.split() + if len(f) >= 5 and isinstance(self.last, dict): + self.last = {**self.last, "finish": f[3], "decode_ms": float(f[4]), + "generated": int(f[2]) if f[2].isdigit() else self.last.get("generated")} + # what the slot's sessions hold now: the prompt and every token fed (all but the last one) + self.slot_held[slot] = list(prompt) + out[gen0:-1] if len(out) > gen0 else [] + if (going_solo and f[3:4] == ["cancel"] and not cancel.is_set() and len(out) < int(max_new) + and not (out and out[-1] in EOS_IDS)): + # the solo path continues it: the engine copies the slot's sessions back (all but the + # last token are in them) and decodes with MTP drafts again + self.slot_live[slot] = None + self.slot_used[slot] = time.time() + with self.slot_cv: + self.slot_busy[slot] = False + self.slot_cv.notify_all() + slot, gen0 = None, None + prompt, left = list(ids) + out, int(max_new) - len(out) + solo_again += 1 + btrace("back to the solo path") + break + return + finally: + # a consumer that left early (or an error): keep the engine and this server in step + btrace("finally phase", phase, "slot", slot, "holding", holding) + try: + if phase == "solo": + self._send("STOP") + self._drain_control("DONE") + elif phase == "admit": + line = self._drain_control("BADM") + if line and line.startswith("BADM ") and line.split()[2:3] == ["1"]: + phase = "slot" + except EngineDied: + pass + if holding: + self.ctl.release() + if reserved is not None: + with self.slot_cv: + self.slot_busy[reserved] = False + self.slot_cv.notify_all() + if slot is not None: + self.slot_live[slot] = None + self.slot_used[slot] = time.time() + if phase == "slot": + self.slot_held[slot] = [] + stream = list(prompt) + out[gen0:] if gen0 is not None and len(out) > gen0 else None + self._release_slot_when_done(slot, stream) # freed at its BDONE + else: + with self.slot_cv: + self.slot_busy[slot] = False + self.slot_cv.notify_all() + + def pick_slot(self, prompt: list[int]) -> int | None: + """A free slot for `prompt` (the caller holds slot_cv): the one whose held tokens are the longest start of the + prompt (the engine then reads only the rest), else an empty one, else the one used longest ago - so the + conversations other slots hold stay for their next turns. None: every slot is busy.""" + free = [b for b in self.slot_order if not self.slot_busy[b]] + if not free: + return None + def held_prefix(b): + h = self.slot_held[b] + return len(h) if h and len(h) < len(prompt) and prompt[:len(h)] == h else 0 + best = max(free, key=held_prefix) + if held_prefix(best) > 0: + return best + return min(free, key=lambda b: (bool(self.slot_held[b]), self.slot_used[b])) + + def slots_view(self) -> list[dict]: + """/metrics: each batch slot - idle (with the tokens it holds for a next turn), reading or decoding.""" + now, view = time.time(), [] + for b in range(self.batch): + r = self.slot_live[b] + if r is None: + view.append({"slot": b, "state": "idle", "held_tokens": len(self.slot_held[b])}) + continue + ft = r.get("first_token") + view.append({"slot": b, "state": r["state"], "prompt_tokens": r["prompt_tokens"], + "generated": r["generated"], "elapsed_s": round(now - r["started"], 1), + "tok_s": round(r["generated"] / max(1e-6, now - ft), 1) if ft else None}) + return view + def generate(self, ids, max_new, sampling, cancel, embeddings=None): """Yields token ids, and None as a heartbeat every 10 s while the engine is quiet (reading a long prompt): the HTTP layer turns it into an SSE comment, which keeps clients' watchdogs calm and notices a client that has gone. A consumer that stops early (or `cancel`) makes the engine STOP, so it does not run to max_new.""" + if getattr(self, "batch", 0): + yield from self.generate_batched(ids, max_new, sampling, cancel, embeddings) + return self.progress = None self.prefill_tok_s_mean = None # an image request takes the same sampling keys as text (#75: it used to decode greedily whatever was asked) @@ -780,6 +1389,31 @@ def gpu_list(cfg: dict) -> list[int]: return [int(str(x).strip()) for x in items if str(x).strip() != ""] +def effort_end_args(cfg: dict, exe: str, tok) -> list[str] | None: + """#458 (opt-in): the engine arguments for "effort_position": "end" - the id of "system" as --tail-role-token, so + the engine checkpoints in front of the trailing effort turn - or None when the config leaves it at the top (the + default) or the engine is too old for it (said once; the prompt stays the default one). ValueError for a value + other than "start" / "end".""" + pos = cfg.get("effort_position", "start") + if pos not in ("start", "end"): + raise ValueError(f'"effort_position" must be "start" (the default) or "end", not {pos!r}') + if pos == "start": + return None + try: + with open(exe, "rb") as f: + known = b"--tail-role-token" in f.read() + except OSError: + known = False + role = tok.encode("system", parse_special=True) + if not known or len(role) != 1: + print("[strata] effort_position \"end\" needs engine 0.1.39 or newer (--tail-role-token): the reasoning " + "effort stays at the top of the prompt", flush=True) + return None + print("[strata] effort_position end: a request's non-default reasoning effort goes right before the answer, so " + "changing it keeps the cached conversation", flush=True) + return ["--tail-role-token", str(role[0])] + + def engine_silence_s(cfg: dict) -> float: """#481: the config's "engine_silence_s" - seconds an engine may print nothing during a request before the server ends it (default ENGINE_SILENCE_S; 0 = wait forever). ValueError for anything but a number >= 0.""" @@ -791,18 +1425,84 @@ def engine_silence_s(cfg: dict) -> float: return float(v) +def layer_split_value(cfg: dict) -> str: + """#644: the config's "layer_split" as the engine's --layer-split value: "auto" (the default), or the FIRST LAYER of + each later GPU's share - one rising number per GPU after the first, e.g. "24,36,42" for 4 GPUs (layers 0-23 on the + first card, 24-35, 36-41, 42 to the end) - as a string or a JSON list ([24, 36, 42]). ValueError with the format + and an example for anything else, such as per-card layer counts ("24,16,12,12").""" + v = cfg.get("layer_split") + if v is None or (isinstance(v, str) and v.strip().lower() in ("", "auto")): + return "auto" + n = len(gpu_list(cfg)) + items = v if isinstance(v, (list, tuple)) else str(v).split(",") if isinstance(v, (str, int)) else None + vals = [] + for x in items or []: + try: + if isinstance(x, bool) or isinstance(x, float): + raise ValueError + vals.append(int(str(x).strip())) + except ValueError: + vals = None + break + want = max(n - 1, 1) + example = ",".join(str(round(48 * (i + 1) / (want + 1))) for i in range(want)) + hint = (f'"layer_split" is the first layer of each later GPU, one rising number per GPU after the first ' + f'({want} for {n} GPUs), not a count of layers per card - e.g. "{example}" (or [{example.replace(",", ", ")}])' + f' for an even share of 48 layers, or "auto" (the default) to place them by each card\'s free VRAM') + if not vals: + raise ValueError(f"{hint}; got {v!r}") + if n > 1 and len(vals) != n - 1: + raise ValueError(f"{hint}; got {len(vals)} number(s) ({v!r}) for {n} GPUs") + if vals[0] < 2 or any(b <= a for a, b in zip(vals, vals[1:])): + raise ValueError(f"{hint}; got {v!r}, which does not rise from 2 or more") + return ",".join(str(x) for x in vals) + + def engine_args(cfg: dict) -> list[str]: """The engine's arguments: the config's, and with several GPUs the layer split across them ("layer_split" in the - config: "auto" by default, or the first layer of each later GPU's share, e.g. "18" or "16,32").""" + config: "auto" by default, or the first layer of each later GPU's share, e.g. "18" or "16,32"; see + layer_split_value).""" args = list(cfg["args"]) if len(gpu_list(cfg)) > 1 and "--layer-split" not in args: - args += ["--layer-split", str(cfg.get("layer_split") or "auto")] + args += ["--layer-split", layer_split_value(cfg)] # opt-in: an auto split runs on the first card alone when it holds every profiled expert and the KV if len(gpu_list(cfg)) > 1 and cfg.get("split_skip_if_fits") and "--split-skip-if-fits" not in args: args.append("--split-skip-if-fits") + # #533 (opt-in): "vram_elastic": true - the expert cache in segments, so POST /v1/vram can give VRAM back to other + # programs and take it back; "vram_segment_mib" sets the segment size (the engine's default: 512) + if cfg.get("vram_elastic") is True and "--vram-elastic" not in args: + args.append("--vram-elastic") + seg = cfg.get("vram_segment_mib") + if isinstance(seg, int) and not isinstance(seg, bool) and seg > 0 and "--vram-segment-mib" not in args: + args += ["--vram-segment-mib", str(seg)] + args += parallel_args(cfg, args) return learned_profile_args(cfg, args) +PARALLEL_MAX = 8 # the engine's batch window holds at most 8 rows (kVerifyMaxT) + + +def parallel_args(cfg: dict, args: list[str]) -> list[str]: + """#465 (opt-in): "parallel": N in the config - up to N requests decode together in the engine's batch slots + (--batch N); more wait their turn. 1 or absent: one request at a time, as before. A value the engine cannot use + is said and passed on: the engine warns and adjusts (recommend, never force).""" + if "--batch" in args or "--slots" in args: + return [] + n = cfg.get("parallel") + if n is None or n is False: + return [] + if isinstance(n, bool) or not isinstance(n, int): + print(f'[strata] "parallel" must be a whole number of requests (2..{PARALLEL_MAX}), not {n!r}: ignored', + flush=True) + return [] + if n <= 1: + return [] + if n > PARALLEL_MAX: + print(f'[strata] "parallel": {n} - the engine runs at most {PARALLEL_MAX} requests together; it will use ' + f"{PARALLEL_MAX}", flush=True) + return ["--batch", str(n)] + + def profile_shape(path: str) -> tuple[int, int] | None: """An expert profile's (layers, experts per layer), from its header (tools/make_profile.py's format), or None when the file is missing, is not one or is shorter than the pairs its header promises.""" @@ -895,11 +1595,14 @@ class ByteTokenizer: """Tiny stand-in tokenizer for tests without the pack: one id per UTF-8 byte, specials as ids >= 256.""" SPECIALS = ["<|im_start|>", "<|im_end|>", "<|endoftext|>", "<|vision_start|>", "<|image_pad|>", "<|vision_end|>"] - def encode(self, text, parse_special=False): + ALWAYS = () # specials matched without parse_special (type 4, as ) + + def encode(self, text, parse_special=False, plain=()): out, i = [], 0 while i < len(text): for k, s in enumerate(self.SPECIALS): - if parse_special and text.startswith(s, i): + if (parse_special or s in self.ALWAYS) and text.startswith(s, i) and not any( + a <= i < b for a, b in plain): out.append(256 + k) i += len(s) break @@ -957,6 +1660,12 @@ def __init__(self, engine: Engine, tokenizer, template: ChatTemplate, model_name self.fifo = threading.Lock() self.embeddings = threading.local() # the current request's image embeddings file (GENI) self.api_key = "" # when set, /v1/* needs it (Bearer or x-api-key) + # #458 (opt-in, the config's "effort_position": "end"): a non-default reasoning effort goes in a short system + # turn before the answer instead of the top of the prompt, so switching it keeps the cached conversation + self.effort_end = False + self.conv_log = ConvCacheLog() # #596: the parked conversations, from the engine's log + self.config_path = None # #564: the run config the web page's Settings view edits + self.config_lock = threading.Lock() # #321: browser pages of these origins may call /v1/* (CORS; "*" = any page - only with an api_key that # matters); empty = no CORS headers at all, as before self.cors_origins: list[str] = [] @@ -969,6 +1678,9 @@ def __init__(self, engine: Engine, tokenizer, template: ChatTemplate, model_name self.host_names: set[str] = set(LOOPBACK_NAMES) self.status = {"busy": False, "queued": 0} # GET /status: what the model is doing right now self.rate = collections.deque(maxlen=32) # (time, generated) samples for the live tok/s window + # "parallel" (the engine's batch slots): every running request's own status and rate window; self.status + # then says busy while any runs and shows the newest one + self.live_reqs: dict[int, tuple[dict, collections.deque]] = {} # #332: the API request monitor (/api-monitor) keeps the last 100 requests' prompts and answers in memory, # so it is off unless the config's "api_monitor" (or --api-monitor) turns it on self.api_monitor = False @@ -990,7 +1702,9 @@ def __init__(self, engine: Engine, tokenizer, template: ChatTemplate, model_name self.idle_unload_s = 0 self.min_free_vram_mib = 0 self.before_load = None + self.vram_reserve = None # #533: the last POST /v1/vram reserve (None: the start's) self.reasoning_budget_tokens = 0 # #123: the config's default thinking budget (0: none) + self.repeat_stop_tokens = REPEAT_STOP_TOKENS # #606: one token this many times in a row ends a reply (0: off) self.anthropic_think_unasked = True # #278: "anthropic_thinking": "on_request" -> False self.stop_ids = set(tokenizer.encode(IM_END, parse_special=True) + tokenizer.encode("<|endoftext|>", parse_special=True)) @@ -1090,6 +1804,43 @@ def ensure_loaded(self): "(a minute or two) ...", flush=True) self.engine.restart() print("[strata] the engine is running again", flush=True) + if self.vram_reserve is not None and hasattr(self.engine, "vram"): # #533: the reserve asked for last + try: + self.engine.vram(self.vram_reserve) + except (ValueError, EngineDied) as e: + print(f"[strata] the VRAM reserve ({self.vram_reserve} MiB) was not applied: {e}", flush=True) + + vram_wait_s = 300.0 # #533: how long POST /v1/vram waits for a running request + + def vram(self, reserve_mib: int | None) -> dict: + """#533, POST /v1/vram: keep `reserve_mib` of VRAM free for other programs (None: the reserve the engine + started with), applied between requests - a request that is running finishes first (up to wait_s). Only an + engine started with --vram-elastic (the config's "vram_elastic": true) can do it; it never resizes on its own. + An unloaded engine applies it when it loads. -> {"status": ..., and the engine's figures}.""" + if not hasattr(self.engine, "vram"): + raise ValueError("this engine cannot resize its VRAM use") + if not self.fifo.acquire(timeout=self.vram_wait_s): + raise ModelBusy("a request is still running; try again when it has finished") + # parallel requests do not hold the fifo: the engine's control lines (a prompt being admitted) are taken too, + # and the engine refuses a resize while a slot decodes + ctl = getattr(self.engine, "ctl", None) if getattr(self.engine, "batch", 0) else None + if ctl is not None and not ctl.acquire(timeout=self.vram_wait_s): + self.fifo.release() + raise ModelBusy("a request is still running; try again when it has finished") + try: + if not self.loaded(): + self.vram_reserve = reserve_mib + return {"status": "not loaded", "reserve_mib": reserve_mib, + "note": "applied when the model loads"} + out = self.engine.vram(reserve_mib) + self.vram_reserve = reserve_mib + print(f"[strata] VRAM: {out.get('vram_free_mib')} MiB free, expert cache {out.get('expert_cache_mib')} of " + f"{out.get('expert_cache_full_mib')} MiB ({out.get('expert_slots')} experts)", flush=True) + return {"status": "ok", **out} + finally: + if ctl is not None: + ctl.release() + self.fifo.release() def _say_died(self, e: Exception) -> None: """The server window's line for an engine that died (or was ended, #481) in the middle of a request.""" @@ -1204,10 +1955,19 @@ def start_telemetry(self): amd=getattr(self, "backend", None) == "hip") def _tok_s(self): - """tok/s over the last RATE_WINDOW_S seconds. Returns 0.0 while nothing is generating.""" + """tok/s over the last RATE_WINDOW_S seconds. Returns 0.0 while nothing is generating. With parallel requests: + the sum of theirs.""" + with self.status_lock: + many = [(dict(st), list(rt)) for st, rt in self.live_reqs.values()] + if many: + return sum(self._rate_of(st, rt) for st, rt in many) with self.status_lock: s = dict(self.status) rate = list(self.rate) + return self._rate_of(s, rate) + + @staticmethod + def _rate_of(s, rate): if not s.get("busy") or not s.get("first_token"): return 0.0 now = time.time() @@ -1220,10 +1980,9 @@ def _tok_s(self): def _tok_s_mean(self): """The whole-request mean since the first token (the old formula), kept so the two can be compared.""" with self.status_lock: - s = dict(self.status) - if not s.get("busy") or not s.get("first_token"): - return 0.0 - return s["generated"] / max(1e-6, time.time() - s["first_token"]) + many = [dict(st) for st, _ in self.live_reqs.values()] or [dict(self.status)] + return sum(s["generated"] / max(1e-6, time.time() - s["first_token"]) for s in many + if s.get("busy") and s.get("first_token")) def _prefill_tok_s_mean(self): """Engine-reported mean over newly read tokens, excluding the cached prefix.""" @@ -1267,6 +2026,8 @@ def metrics(self, all_requests=False) -> dict: the hardware (with a minute of history per series).""" with self.status_lock: s = dict(self.status) + if self.live_reqs: # parallel requests: the newest one's status + s = {**s, **dict(list(self.live_reqs.values())[-1][0]), "queued": s.get("queued", 0)} hist = list(self.history) totals = dict(self.totals) now = time.time() @@ -1290,10 +2051,23 @@ def metrics(self, all_requests=False) -> dict: "tok_s_window_s": RATE_WINDOW_S if state == "generating" else None} if state == "reading" and progress: live["prompt_read"], live["prompt_total"] = progress + par = int(getattr(self.engine, "batch", 0) or 0) + if par: # #465: the requests running together, slot by slot + with self.status_lock: + running = len(self.live_reqs) + slots = self.engine.slots_view() if hasattr(self.engine, "slots_view") else [] + # the requests in flight that are not in a slot: the one alone on the solo path, or waiting their turn + in_slots = sum(1 for x in slots if x["state"] != "idle") + live.update(parallel=par, running=running, slots=slots, outside_slots=max(0, running - in_slots), + waiting=int(getattr(self.engine, "waiting", 0) or 0)) + if running and state == "idle": + live["state"] = "generating" engine = {"model": self.model, "max_context": self.engine.max_context, "images": self.vision is not None, **dict(getattr(self.engine, "info", {}) or {})} tel = self.telemetry.snapshot() if getattr(self, "telemetry", None) else {"now": {}, "history": {}, "static": {}} + parked = self.conv_log.poll(getattr(self.engine, "log_path", None), getattr(self.engine, "log_start", None)) return {"engine": engine, "live": live, "requests": hist[::-1][:None if all_requests else 12], + "conversation_cache": conversation_cache_view(engine, hist, totals, parked), "requests_kept": len(hist), "totals": totals, "hardware": tel["now"], "hardware_static": tel["static"], "history": tel["history"], "time": now} @@ -1322,12 +2096,16 @@ def scaled(v, unit, digits=0): "started": int(self.started_at), "uptime_s": int(time.time() - self.started_at), "cache_max_tokens": ctx, "context": {"native": ctx, "max_positions": ctx}, - "concurrency": {"serving": 1, "requested": 1}, # one request at a time; more wait their turn - "dialects": ["/v1/chat/completions", "/v1/messages"], + # one request at a time (more wait their turn), or "parallel": N batch slots (#465) + "concurrency": {"serving": max(1, int(getattr(self.engine, "batch", 0) or 0)), + "requested": max(1, int(getattr(self.engine, "batch", 0) or 0))}, + "dialects": ["/v1/chat/completions", "/v1/messages", "/v1/responses"], "vision": {"enabled": images, "available": images, "error": None}, "activity": {"requests": totals["requests"] + int(busy), "in_flight": int(busy) + int(s.get("queued") or 0), "last_request_at": int(last_at) if last_at else None}, "last_timings": last_t, + "vram": dict((getattr(self.engine, "info", {}) or {}).get("vram") or {}, + elastic=bool((getattr(self.engine, "info", {}) or {}).get("vram_elastic"))), "machine": { "at": int(time.time()), "gpu": {"name": static.get("gpu_name"), "used_mib": scaled(hw.get("gpu_mem_used"), 2 ** 20), @@ -1336,11 +2114,38 @@ def scaled(v, unit, digits=0): "ram": {"used_gib": scaled(hw.get("ram_used"), 2 ** 30, 1), "total_gib": scaled(hw.get("ram_total"), 2 ** 30, 1)} if hw.get("ram_total") else None}} + def render_prompt(self, messages, tools, kwargs) -> str: + """The template rendered. #458: with `effort_end`, a request with a non-default effort (low, medium or no + thinking) is rendered as a default one up to the answer - the same prompt start, so the conversation cache + keeps it - and its effort follows in a short system turn right before the answer (thinking off: the template's + empty thinking block). The engine (--tail-role-token) checkpoints in front of that turn.""" + effort = kwargs.get("reasoning_effort") + off = kwargs.get("enable_thinking") is False + if not self.effort_end or (not off and effort in (None, "", "xhigh", "high")): + return self.template.render(messages, tools=tools, **kwargs) + base = {k: v for k, v in kwargs.items() if k not in ("reasoning_effort", "enable_thinking")} + history = self.template.render(messages, tools=tools, add_generation_prompt=False, **base) + full = self.template.render(messages, tools=tools, add_generation_prompt=True, **kwargs) + mine = self.template.render(messages, tools=tools, add_generation_prompt=False, **kwargs) + if not full.startswith(mine): # a template this cannot take apart: rendered as asked + return full + tail = "" if off else EFFORT_TURN.format(EFFORT_TEXT.get(effort, EFFORT_TEXT["medium"])) + return history + tail + full[len(mine):] + + def encode_prompt(self, messages, tools, kwargs) -> list[int]: + """The request's prompt: the template rendered and tokenized. #537: a / written inside a + message's text is encoded as the text it is, not as the model's reasoning markers (the template's own are).""" + marked, marked_tools, changed = mark_think_literals(messages, tools) + prompt = self.render_prompt(marked, marked_tools, kwargs) + if not changed: + return self.tok.encode(prompt, parse_special=True) + prompt, plain = unmark_think_literals(prompt) + return self.tok.encode(prompt, parse_special=True, plain=plain) + def prepare(self, messages, tools, kwargs, max_new=None): """-> (ids, thinking, max_new). An unset or non-positive max_new (some clients send -1) means "unlimited": the rest of the context.""" - prompt = self.template.render(messages, tools=tools, **kwargs) - ids = self.tok.encode(prompt, parse_special=True) + ids = self.encode_prompt(messages, tools, kwargs) self.embeddings.path = None images = images_of(messages) if images: @@ -1376,30 +2181,37 @@ def prepare(self, messages, tools, kwargs, max_new=None): for path, _ in encoded: f.write(path.read_bytes()) self.embeddings.path = combined - if self.engine.max_context <= 0: # #344: (re)starting, not a prompt that is too long - raise EngineStarting("the engine is starting (a minute or two); try again shortly") - room = self.engine.max_context - CTX_SLACK - len(ids) + ctx = self.engine.max_context + if ctx <= 0: + if getattr(self.engine, "starting", False): # #344: (re)starting, not a prompt that is too long + raise EngineStarting("the engine is starting (a minute or two); try again shortly") + # a failed restart leaves max_context 0: check against the last known context, so the request reaches + # run() - which starts the engine again - instead of failing with "exceeds the context (0)" forever + ctx = getattr(self.engine, "known_ctx", 0) + if ctx <= 0: + raise EngineStarting("the engine is starting (a minute or two); try again shortly") + room = ctx - CTX_SLACK - len(ids) if max_new is None or max_new <= 0 or (self.fit_max_tokens and room < 1): if room < 1: raise ValueError(f"prompt ({len(ids)} tokens) leaves no room to answer in the context " - f"({self.engine.max_context}); requests are never truncated") + f"({ctx}); requests are never truncated") max_new = room elif max_new > room: if not self.fit_max_tokens: raise ValueError(f"prompt ({len(ids)} tokens) + max tokens ({max_new}) exceeds the context " - f"({self.engine.max_context}); requests are never truncated. Send a smaller " + f"({ctx}); requests are never truncated. Send a smaller " f"max_tokens (at most {max(0, room)} here), or add \"fit_max_tokens\": true to the " "model's strata-.json to shorten it to the room left (#545)") max_new = max(1, room) # --fit-max-tokens: a shorter completion beats a 400 return ids, kwargs.get("enable_thinking", True) is not False, max_new - def _note(self, n, evs): + def _note(self, n, evs, st=None, rate=None): with self.status_lock: - s = self.status + s = self.status if st is None else st s["generated"] = n if s.get("first_token") is None: s["first_token"] = time.time() - self.rate.append((time.time(), n)) # the live rate's window over the last RATE_WINDOW_S + (self.rate if rate is None else rate).append((time.time(), n)) # the live rate's window (RATE_WINDOW_S) for ev in evs: if ev.kind == "reasoning": s["phase"] = "thinking" @@ -1411,13 +2223,13 @@ def _note(self, n, evs): s["phase"] = "tool call complete" s["tail"] = ((s.get("tail") or "") + (ev.text or ""))[-600:] - def _progress(self, last_print, every=1.0): + def _progress(self, last_print, every=1.0, st=None): """A progress line in the server window every `every` seconds while a request runs.""" now = time.time() if now - last_print < every: return last_print with self.status_lock: - s = dict(self.status) + s = dict(self.status if st is None else st) el = now - s.get("started", now) if s.get("first_token") is None: pr = getattr(self.engine, "progress", None) # (position reached, prompt tokens): a reused prefix counts @@ -1438,6 +2250,8 @@ def run(self, ids, thinking, tools, max_new, sampling, cancel) -> Iterator[tuple sampling = {**defaults, **req_values} parser = OutputParser(thinking=thinking, tools=tools, stream_tools=True) detok, n, finish = Detokenizer(self.tok), 0, "length" + run_tok, run_len, repeated = None, 0, False # #606: the current run of one repeated token + thinking_n = 0 # tokens written while thinking (Responses' reasoning_tokens) timings, before = None, None # this request's timings; the engine's `last` before it raw_ids = [] # every generated id (STRATA_DEBUG: dump raw model text) emb = getattr(self.embeddings, "path", None) @@ -1446,10 +2260,16 @@ def run(self, ids, thinking, tools, max_new, sampling, cancel) -> Iterator[tuple engine_last0 = getattr(self.engine, "last", None) trace = getattr(self.request_trace, "record", None) waiting = time.perf_counter() + # #465: with "parallel" (the engine's batch slots) requests run at once: each keeps its own status and rate + # window (self.status says busy while any runs); one at a time they are self.status / self.rate, as before + par = bool(getattr(self.engine, "batch", 0)) + st = {} if par else self.status + rate = collections.deque(maxlen=32) if par else self.rate with self.status_lock: self.status["queued"] += 1 try: - with self.fifo: + # --batch: the engine runs several requests at once (StrataEngine.generate_batched orders them) + with (contextlib.nullcontext() if getattr(self.engine, "batch", 0) else self.fifo): try: with self.status_lock: if trace is not None: @@ -1457,13 +2277,18 @@ def run(self, ids, thinking, tools, max_new, sampling, cancel) -> Iterator[tuple trace["state"] = "generating" self.status["queued"] -= 1 # issue #27: it died in an earlier request (or was unloaded) - start it again instead of failing - self.ensure_loaded() + # (parallel requests do not hold the fifo: one of them starts it, the others wait for that) + with (self.fifo if par else contextlib.nullcontext()): + self.ensure_loaded() with self.status_lock: - self.status.update(busy=True, phase="reading the prompt", prompt_tokens=len(ids), - generated=0, started=time.time(), first_token=None, tool=None, tail="", - max_tokens=max_new) + st.update(busy=True, phase="reading the prompt", prompt_tokens=len(ids), + generated=0, started=time.time(), first_token=None, tool=None, tail="", + max_tokens=max_new) + if par: + self.live_reqs[id(st)] = (st, rate) + self.status.update(st) self.last_request_at = time.time() - self.rate.clear() # the previous request's samples must not leak into this one + rate.clear() # the previous request's samples must not leak into this one before = getattr(self.engine, "last", None) last_print = time.time() prompt, thought = ids, 0 # thought: the reasoning tokens so far (the budget's count) @@ -1474,7 +2299,7 @@ def run(self, ids, thinking, tools, max_new, sampling, cancel) -> Iterator[tuple try: for t in gen: if t is None: # heartbeat while the engine is quiet - last_print = self._progress(last_print) + last_print = self._progress(last_print, st=st) yield "ping", None continue n += 1 @@ -1487,9 +2312,15 @@ def run(self, ids, thinking, tools, max_new, sampling, cancel) -> Iterator[tuple break raw_ids.append(t) seg.append(t) + thinking_n += parser.state == "reasoning" + run_len = run_len + 1 if t == run_tok else 1 + run_tok = t + if self.repeat_stop_tokens and run_len >= self.repeat_stop_tokens: + repeated = True # #606: a degenerate output, not an answer: end it here + break evs = parser.feed(detok.push(t)) - self._note(n, evs) - last_print = self._progress(last_print) + self._note(n, evs, st, rate) + last_print = self._progress(last_print, st=st) for ev in evs: yield "event", ev if budget and parser.state == "reasoning": @@ -1533,13 +2364,19 @@ def run(self, ids, thinking, tools, max_new, sampling, cancel) -> Iterator[tuple for t in extra: n += 1 raw_ids.append(t) + thinking_n += parser.state == "reasoning" evs = parser.feed(detok.push(t)) - self._note(n, evs) + self._note(n, evs, st, rate) for ev in evs: yield "event", ev prompt = prompt + seg + extra if cancel.is_set(): finish = "cancel" + elif repeated: + print(f"[strata] the reply repeated one token ({self.tok.decode([run_tok])!r}) " + f"{run_len} times in a row: ended as \"length\" (repeat_stop_tokens in " + "strata-.json; 0 turns this off). If a new request with a short prompt does the " + "same, restart the server and report it (#606)", flush=True) except GeneratorExit: # the client disconnected mid-stream finish = "disconnect" raise @@ -1547,15 +2384,20 @@ def run(self, ids, thinking, tools, max_new, sampling, cancel) -> Iterator[tuple # #266: settle this request's status, history and totals while still holding the fifo: once # it is released the next request sets its own status, which this must not record or clear with self.status_lock: - if self.status.get("busy"): + if st.get("busy"): # only this request's DONE counts: same object means no DONE arrived (death, error, # disconnect) last = dict(getattr(self.engine, "last", {}) or {}) \ if getattr(self.engine, "last", None) is not engine_last0 else {} - started = self.status.get("started", time.time()) + started = st.get("started", time.time()) cvec = (getattr(self.engine, "info", {}) or {}).get("cvec", 0) loaded = str(cvec) not in ("0", "", "None") hit_rate = round(last["hits"] / last["lookups"], 3) if last.get("lookups") else None + # #588: the share the GPU read over PCIe (--pcie-frac) or another GPU computed - not in + # the hit rate's lookups, which is the VRAM share of the rest (None: an older engine) + routed = (last.get("lookups") or 0) + (last.get("offloaded") or 0) + pcie_share = round(last["offloaded"] / routed, 3) \ + if last.get("offloaded") is not None and routed else None seen = prompt_tokens_seen(len(ids), last) # #471: < len(ids) when cancelled mid-read self.history.append({ "projection": (sampling or {}).get("experimental_speed_projection") is not False @@ -1568,14 +2410,11 @@ def run(self, ids, thinking, tools, max_new, sampling, cancel) -> Iterator[tuple "prompt_ms": last.get("prompt_ms"), "decode_ms": last.get("decode_ms"), "decode_tok_s": round(last["generated"] / (last["decode_ms"] / 1000), 1) if n and last.get("generated") and last.get("decode_ms") else None, - "hit_rate": hit_rate, "ram_blobs": last.get("ram_blobs"), + "hit_rate": hit_rate, "pcie_share": pcie_share, "ram_blobs": last.get("ram_blobs"), "file_blobs": last.get("file_blobs"), "file_mb": last.get("file_mb"), # #457: the speculative drafts from the DONE line (None: the engine did not say) "drafts_offered": last.get("drafts_offered"), - "drafts_accepted": last.get("drafts_accepted"), - "parked_conversations": last.get("parked_conversations"), - "parked_bytes": last.get("parked_bytes"), - "park_evictions": last.get("park_evictions")}) + "drafts_accepted": last.get("drafts_accepted")}) t = self.totals t["requests"] += 1 t["prompt_tokens"] += seen @@ -1591,10 +2430,12 @@ def run(self, ids, thinking, tools, max_new, sampling, cancel) -> Iterator[tuple self.last_timings = dict(timings, at=int(time.time())) if timings else None self.last_request_at = time.time() now = time.time() - el = now - self.status.get("started", now) - ft = self.status.get("first_token") + el = now - st.get("started", now) + ft = st.get("first_token") rate = n / max(1e-6, now - ft) if ft else 0.0 hit_msg = f", expert cache {hit_rate*100:.1f}% hit" if hit_rate is not None else "" + if hit_msg and pcie_share: + hit_msg += f" (+{pcie_share*100:.1f}% of the routed experts over PCIe)" print(f"[strata] done: {n} tokens in {el:.0f} s ({rate:.1f} tok/s) " f"({finish}, cancel={cancel.is_set()}){hit_msg}", flush=True) if finish == "length" and parser.state == "reasoning": # #530 @@ -1603,16 +2444,24 @@ def run(self, ids, thinking, tools, max_new, sampling, cancel) -> Iterator[tuple "strata-.json for every request) leaves room to answer", flush=True) if os.environ.get("STRATA_DEBUG") and raw_ids: print(f"[strata] raw: {self.tok.decode(raw_ids)!r}", flush=True) - self.status["busy"] = False - self.status.pop("tail", None) # #212: the answer's end is not kept once it is done - self.status.pop("tool", None) + st["busy"] = False + st.pop("tail", None) # #212: the answer's end is not kept once it is done + st.pop("tool", None) + if par: + self.live_reqs.pop(id(st), None) + if self.live_reqs: # the newest request still running + self.status.update(list(self.live_reqs.values())[-1][0]) + else: + self.status.update(busy=False) + self.status.pop("tail", None) + self.status.pop("tool", None) finally: if emb: Path(emb).unlink(missing_ok=True) for ev in parser.finish(): yield "event", ev yield "done", {"finish": finish, "completion_tokens": n, "reused": (timings or {}).get("cache_n", 0), - "timings": timings} + "timings": timings, "reasoning_tokens": thinking_n} def prompt_tokens_seen(prompt_tokens: int, last: dict) -> int: @@ -2162,6 +3011,10 @@ def do_GET(self): if self._authorized(): self._json(200, {"shared": bool(svc.shared), "defaults": svc.shared}) return + if path == "/config": # #564: the Settings view's keys of the run config + if self._authorized(): + self._config_get() + return if path == "/mcp": # the MCP servers, their state and tools (the web app's switch and Monitor card) if self._authorized(): @@ -2231,6 +3084,11 @@ def do_POST(self): if path == "/settings": self._settings() return + if path == "/config": + self._config_post() + return + if path in ("/unload", "/load") and not self._control_body(): + return # JSON from Strata's own page only, as /settings: else a plain form POST from any site unloads the model if path in ("/unload", "/load") and not self._own_page("the model can be loaded or unloaded"): return @@ -2253,6 +3111,10 @@ def do_POST(self): req = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))) or b"{}") if not isinstance(req, dict): raise ValueError("send a JSON object") + if path.startswith("/v1/responses/"): # retrieve/delete/cancel/compact: nothing is stored + self._json(404, responses_error_body("this server keeps no responses (stateless); send the " + "whole conversation to POST /v1/responses", code="not_found")) + return if path in ("/v1/load", "/v1/unload"): if not self._own_page("the model can be loaded or unloaded"): return @@ -2277,9 +3139,22 @@ def do_POST(self): result = "loaded" self._json(200, {"status": result, **svc.v1_status()}) return - if path in ("/v1/chat/completions", "/v1/messages"): + if path == "/v1/vram": # #533: give VRAM back to other programs, or take it + if not self._own_page("the VRAM reserve can be changed"): + return + r = req.get("reserve_mib") + if r is not None and (isinstance(r, bool) or not isinstance(r, int) or r < 0): + raise ValueError("reserve_mib: a whole number of MiB (0 or more), or null for the start's") + try: + self._json(200, svc.vram(r)) + except EngineDied as e: + self._json(503, {"error": {"type": "server_error", "message": str(e)}}) + return + if path in ("/v1/chat/completions", "/v1/messages", "/v1/responses"): self.record = svc.begin_request(path, req) - if path == "/v1/chat/completions": + if path == "/v1/responses": + self._responses(req) + elif path == "/v1/chat/completions": self._openai(req) elif path == "/v1/messages": self._anthropic(req) @@ -2288,7 +3163,10 @@ def do_POST(self): else: self._json(404, {"error": {"message": "not found"}}) except ValueError as e: - self._json(400, {"error": {"type": "invalid_request_error", "message": str(e)}}) + if path == "/v1/responses": + self._json(400, responses_error_body(str(e))) + else: + self._json(400, {"error": {"type": "invalid_request_error", "message": str(e)}}) except ModelBusy as e: self._json(409, {"error": {"type": "model_busy", "message": str(e)}}) except StructuredOutputError as e: @@ -2340,6 +3218,28 @@ def _props(self): props["build_info"] = "Strata " + str(version) self._json(200, props) + def _control_body(self) -> bool: + """Consume the unused control body before replying/closing (Windows otherwise sends a TCP reset).""" + try: + length = int(self.headers.get("Content-Length", "0")) + except ValueError: + self._json(400, {"error": {"message": "invalid Content-Length"}}) + return False + if not 0 <= length <= 65536: + self._json(413, {"error": {"message": "control request body is limited to 64 KiB"}}) + return False + timeout = self.connection.gettimeout() + try: + self.connection.settimeout(2.0) + complete = len(self.rfile.read(length)) == length + except OSError: + complete = False + finally: + self.connection.settimeout(timeout) + if not complete: + self._json(400, {"error": {"message": "incomplete control request body"}}) + return complete + def _own_page(self, what) -> bool: """Only JSON (a form or a "simple" cross-site request can't send it without a CORS preflight, which this server never grants) and no foreign Origin: a web page elsewhere must not change settings or run tools.""" @@ -2357,6 +3257,44 @@ def _own_page(self, what) -> bool: return False return True + def _config_get(self): + if not svc.config_path: + self._json(404, {"error": {"message": "this server was started without a run config"}}) + return + try: + cfg = runconfig.load(svc.config_path) + except (OSError, ValueError) as e: + self._json(500, {"error": {"type": "server_error", "message": f"the run config cannot be read: {e}"}}) + return + self._json(200, runconfig.view(cfg, svc.config_path)) + + def _config_post(self): + """#564: change a few documented keys of the run config - JSON from Strata's own page only, as + /settings (the key is checked before); every other key of the file stays as it is.""" + body = self.rfile.read(int(self.headers.get("Content-Length", 0))) + if not self._own_page("the run config can be changed"): + return + if not svc.config_path: + self._json(404, {"error": {"message": "this server was started without a run config"}}) + return + try: + req = json.loads(body or b"{}") + with svc.config_lock: + cfg = runconfig.load(svc.config_path) + new, changed = runconfig.apply(cfg, req.get("set") if isinstance(req, dict) else None) + if changed: + bak = runconfig.save(svc.config_path, new) + except ValueError as e: + self._json(400, {"error": {"type": "invalid_request_error", "message": str(e)}}) + return + except OSError as e: + self._json(500, {"error": {"type": "server_error", "message": f"the run config cannot be written: {e}"}}) + return + if changed: + print(f"[strata] the Settings view changed {', '.join(changed)} in {Path(svc.config_path).name} " + f"(the earlier file: {bak.name}); used from the next start", flush=True) + self._json(200, {**runconfig.view(new, svc.config_path), "changed": changed}) + def _settings(self): # They change what every client gets, so only the app's own page may set them body = self.rfile.read(int(self.headers.get("Content-Length", 0))) @@ -2396,6 +3334,11 @@ def _captured(self, items, api): delta = item["choices"][0]["delta"] content, reasoning = delta.get("content", ""), delta.get("reasoning_content", "") usage, timings = item.get("usage"), item.get("timings") + elif api == "responses": + kind = item["type"] + content = item["delta"] if kind == "response.output_text.delta" else "" + reasoning = item["delta"] if kind == "response.reasoning_text.delta" else "" + usage, timings = (item.get("response") or {}).get("usage"), None else: _, event = item delta = event.get("delta", {}) @@ -2424,7 +3367,7 @@ def _openai(self, req): svc.load() max_req = max_new = int(req.get("max_completion_tokens") or req.get("max_tokens") or 0) # 0/-1: the rest use_mcp = req.get("strata_mcp") is True and svc.mcp is not None # the web app's opt-in (serve/mcp.py) - own = {t.get("name") for t in tools or []} + own = {t.get("name") for t in tools or [] if isinstance(t, dict)} # #592: a second line of defence if use_mcp: if not self._own_page("MCP tools can be used"): # tools run with the user's rights on this PC return @@ -2471,13 +3414,133 @@ def _openai(self, req): self._note(error=err["error"]) self.wfile.write(b"data: " + json.dumps(err).encode() + b"\n\ndata: [DONE]\n\n") + def _responses(self, req): + """#451: POST /v1/responses - OpenAI's Responses API, stateless (serve/responses.py), on the chat path. + Errors use the Responses format; once the stream has started they arrive as a response.failed event.""" + try: + ids, thinking, tools, max_new, asm, validator, req = self._responses_prepare(req) + except ResponsesError as e: + return self._json(e.status, e.body()) + except ModelBusy as e: + return self._json(409, responses_error_body(str(e), "server_error", code="model_busy")) + except (GpuBusy, EngineStarting, EngineStuck, EngineDied) as e: + return self._json(503, responses_error_body(str(e), "server_error", code="server_error")) + cancel = threading.Event() + self._watch_client(cancel) # #430 #431 + + def check(text, finish): + return validated_json(text, validator, finish) + + def events(): + yield from asm.start() + done = None + for kind, x in svc.run(ids, thinking, tools, max_new, req, cancel): + if kind == "ping": + yield None + elif kind == "event": + yield from asm.feed(x) + elif kind == "done": + done = x + if done is not None and done["finish"] != "cancel" and not cancel.is_set(): + yield from asm.finish(done, check) + items = self._capture(events(), "responses") + if not req.get("stream"): + try: + result = responses_api.collect(items) + except StructuredOutputError as e: + return self._json(502, responses_error_body(str(e), "server_error", + code="structured_output_failed")) + except EngineDied as e: + return self._json(503, responses_error_body(f"{e}; the next request restarts it", "server_error", + code="server_error")) + except ValueError as e: # the engine's ERR line + return self._json(500, responses_error_body(str(e), "server_error", code="server_error")) + return self._json(200, result) + self._sse() + last = time.monotonic() + + def send(e): + self.wfile.write(f"event: {e['type']}\n".encode() + b"data: " + + json.dumps(e, ensure_ascii=False).encode() + b"\n\n") + + try: + for e in items: + if e is None: + # Codex's idle timeout (5 minutes by default) counts events, not SSE comments: while a long + # prompt is read, a response.in_progress every 15 s tells it the server is still working + if time.monotonic() - last >= 15: + send(asm.in_progress()) + last = time.monotonic() + else: + self.wfile.write(b": keep-alive\n\n") + else: + send(e) + last = time.monotonic() + self.wfile.flush() + except OSError: + self._note(outcome="disconnected") + cancel.set() # client went away: stop the engine + items.close() + except EngineDied as e: # mid-stream: response.failed, then the stream ends + self._responses_failed(asm, f"{e}; the next request restarts it", "server_error", send) + except StructuredOutputError as e: + self._responses_failed(asm, str(e), "structured_output_failed", send) + except ValueError as e: # the engine's ERR after the stream started + self._responses_failed(asm, str(e), "server_error", send) + + def _responses_failed(self, asm, message, code, send): + e = asm.failed(message, code) + self._note(error=e["response"]["error"]) + try: + send(e) + self.wfile.flush() + except OSError: + pass + + def _responses_prepare(self, req): + responses_api.check_request(req) + messages = responses_api.input_messages(req) + tools, names, skipped = responses_api.request_tools(req) + kw = responses_api.template_kwargs(req, svc.shared) + try: + messages, validator = prepare_format(responses_api.text_format(req), messages) + except ValueError as e: + raise ResponsesError(str(e).replace("response_format", "text.format"), "text.format") from None + if validator is not None and tools: + raise ResponsesError("a JSON text.format with tools is not supported", "text.format", + "unsupported_parameter") + noted = svc.__dict__.setdefault("hosted_tools_noted", set()) # said once per tool, not per request + if set(skipped) - noted: + print(f"[strata] /v1/responses: left out the hosted tools {', '.join(sorted(set(skipped) - noted))} " + f"(this server cannot run them)", flush=True) + noted.update(skipped) + if req.get("max_output_tokens") is None and "max_tokens" in svc.shared: # the shared Chat settings + req = {**req, "max_output_tokens": svc.shared["max_tokens"]} + try: + svc.reasoning_budget(req) # a bad value is a 400 before anything is sent + except ValueError as e: + raise ResponsesError(str(e), "reasoning_budget_tokens") from None + svc.load() + try: + ids, thinking, max_new = svc.prepare(messages, tools, kw, req.get("max_output_tokens") or 0) + except ResponsesError: + raise + except ValueError as e: # too long for the context, an image without vision + raise ResponsesError(str(e), "input", "context_length_exceeded" if "context" in str(e) else None) \ + from None + _debug_req("responses", req, messages, tools, max_new, thinking, len(ids)) + include = req.get("include") if isinstance(req.get("include"), list) else [] + asm = responses_api.Assembler(req, svc.model_for(req), len(ids), names, + "reasoning.encrypted_content" in include, validator is not None) + # the request is also the sampling dict, as on the chat path (temperature, top_p, top_k, seed, ...) + return ids, thinking, tools, max_new, asm, validator, req + def _count_tokens(self, req): """Anthropic's token count, which Claude Code asks for its context figures: the prompt this server would read for the same request, rendered and tokenized - the model does not run.""" req = svc.with_shared(req, "anthropic") messages, tools, kw = anthropic_to_messages(req, svc.anthropic_think_unasked) - prompt = svc.template.render(messages, tools=tools, **kw) - self._json(200, {"input_tokens": len(svc.tok.encode(prompt, parse_special=True))}) + self._json(200, {"input_tokens": len(svc.encode_prompt(messages, tools, kw))}) def _anthropic(self, req): svc.load() @@ -2919,7 +3982,11 @@ def main() -> int: print("model unloaded; the first request loads it ..." if lazy else "loading the model (the first start takes a minute or two) ...", flush=True) if len(gpu_list(cfg)) > 1: - print(f"[strata] layer split across GPUs {gpu_list(cfg)} ({cfg.get('layer_split') or 'auto'})", flush=True) + try: + split = layer_split_value(cfg) # #644: before the (minutes-long) start + except ValueError as e: + raise SystemExit(f"[strata] config {e}") + print(f"[strata] layer split across GPUs {gpu_list(cfg)} ({split})", flush=True) # a relative "exe" is the config's cwd's: Windows' CreateProcess resolves "engine/strata.exe" against nothing # it is told about (WinError 2), so it is made absolute here exe = cfg["exe"] if os.path.isabs(cfg["exe"]) else os.path.abspath(os.path.join(cfg.get("cwd") or ".", cfg["exe"])) @@ -2927,7 +3994,12 @@ def main() -> int: silence = engine_silence_s(cfg) # #481: checked before the (minutes-long) start except ValueError as e: raise SystemExit(f"[strata] config {e}") - engine = StrataEngine(exe, engine_args(cfg), cwd=cfg.get("cwd"), log=cfg.get("log"), env=env, lazy=lazy) + try: + effort_end = effort_end_args(cfg, exe, tok) # #458 + except ValueError as e: + raise SystemExit(f"[strata] config {e}") + engine = StrataEngine(exe, engine_args(cfg) + (effort_end or []), cwd=cfg.get("cwd"), log=cfg.get("log"), + env=env, lazy=lazy) engine.silence_s = silence # an attribute of its own: restart() keeps it warn_tight_ram(engine.info.get("arena_mib")) note = desktop_vram_note(cfg.get("backend"), engine.info.get("vram_free_mib"), engine.spawn[1], @@ -2935,6 +4007,7 @@ def main() -> int: if note: # #560 #516: before --open starts a browser on that card print(note, flush=True) else: + effort_end = None engine, vision, sampling_defaults = MockEngine(tok, a.script or [ "Thinking about it.\n\nHello from the mock engine."]), None, {} # the model's own chat template (exported with its tokenizer), else the original model's @@ -2982,6 +4055,11 @@ def main() -> int: if mode not in ("model", "on_request"): raise SystemExit(f"[strata] config anthropic_thinking must be \"model\" or \"on_request\", not {mode!r}") svc.anthropic_think_unasked = mode == "model" + rs = cfg.get("repeat_stop_tokens", REPEAT_STOP_TOKENS) # #606: opt-out with 0 + if isinstance(rs, bool) or not isinstance(rs, int) or rs < 0: + raise SystemExit(f"[strata] config \"repeat_stop_tokens\" must be a whole number >= 0 (0 = off), not {rs!r}") + svc.repeat_stop_tokens = rs + svc.effort_end = bool(effort_end) # #458: "effort_position": "end" with an engine that has it if cfg.get("reasoning_budget_tokens") is not None: # #123: a default thinking budget for every request try: svc.reasoning_budget_tokens = cfg["reasoning_budget_tokens"] @@ -2994,6 +4072,8 @@ def main() -> int: svc.gpu_index = (gpu_list(cfg) or [0])[0] # the Monitor reads the card the engine runs on (issue #51) svc.gpu_indices = gpu_list(cfg) # ... or every card of a layer split (issue #112) svc.backend = cfg.get("backend") # "hip": the AMD cards' readings come from sysfs (#301) + if a.config: + svc.config_path = a.config # #564: the web page's Settings view if a.config: # the Chat settings shared with other apps, from last time svc.shared_path = str(Path(a.config).with_suffix("")) + ".shared-settings.json" try: @@ -3032,8 +4112,8 @@ def main() -> int: "in an admin PowerShell:\n New-NetFirewallRule -DisplayName \"Strata " + str(a.port) + "\" " "-Direction Inbound -Protocol TCP -LocalPort " + str(a.port) + " -Action Allow -Profile Private\n" " (and set this network to Private in Windows' network settings)", flush=True) - if a.open: - import webbrowser + if a.open and cfg.get("open_browser") is not False: # #609: the config's "open_browser": false wins (an older + import webbrowser # run-.bat still passes --open) webbrowser.open(f"http://{'127.0.0.1' if a.host in ('0.0.0.0', '') else a.host}:{a.port}/") # #96: docker stop sends SIGTERM, which Python ignores by default, so the container's PID 1 would be killed after # the grace period with the engine still running. SIGTERM takes Ctrl+C's path below (QUIT to the engine). diff --git a/serve/test_monitor.py b/serve/test_monitor.py index ad554e0006..0c82cb0b53 100644 --- a/serve/test_monitor.py +++ b/serve/test_monitor.py @@ -194,5 +194,68 @@ def test_page_and_relative_assets_are_served(self): self.assertEqual(self.request("/web/../server.py")[0], 404) +class ConversationCacheCard(unittest.TestCase): + """#596: /metrics' "conversation_cache": the parked conversations from the engine's log lines, the budget and + slots from its INFO line, and how much of the prompts the cache gave back.""" + + PARK = ("strata serve: conversation cache: parked 5000 tokens in 12.0 ms; parked=1 bytes=104857600 evictions=0 " + "snapshot_bytes=104857600 reused_kv_bytes=0\n") + RESTORE = "strata serve: conversation cache: restored 4000 tokens (exact) in 30.0 ms; parked=2 bytes=209715200\n" + + def test_the_log(self): + from serve.server import ConvCacheLog + import tempfile + with tempfile.TemporaryDirectory() as d: + log = Path(d) / "strata.log" + log.write_text("an earlier run\n" + self.PARK, encoding="utf-8") + start = log.stat().st_size # this run starts here: the earlier run's park is not counted + c = ConvCacheLog() + self.assertEqual(c.poll(str(log), start)["parked"], 0) + with open(log, "a", encoding="utf-8") as f: + f.write("strata serve: something else\n" + self.PARK + self.RESTORE + "strata serve: conversation ca") + st = c.poll(str(log), start) + self.assertEqual((st["parked"], st["bytes"], st["parks"], st["restores"]), (2, 209715200, 1, 1)) + self.assertEqual((st["last_event"], st["last_tokens"]), ("restored", 4000)) + with open(log, "a", encoding="utf-8") as f: # the cut line is read once it is whole + f.write("che: dropped 1 superseded copy of this conversation; parked=1\n") + self.assertEqual(c.poll(str(log), start)["parked"], 1) + self.assertEqual(c.poll(str(log), log.stat().st_size)["parked"], 0) # the engine started again + self.assertEqual(c.poll(None, None)["parked"], 0) + + def test_metrics(self): + tok = ByteTokenizer() + engine = MockEngine(tok, "Thought.\nHello.", max_context=4096) + engine.info = {"conversation_cache_mib": 8192, "conversation_cache_slots": 4} + svc = Service(engine, 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: + with urllib.request.urlopen(base + "/metrics", timeout=10) as r: + c = json.loads(r.read())["conversation_cache"] + self.assertEqual((c["enabled"], c["budget_mib"], c["slots"], c["parked"], c["requests"]), + (True, 8192, 4, 0, 0)) + req = urllib.request.Request(base + "/v1/chat/completions", headers={"Content-Type": "application/json"}, + data=json.dumps({"messages": [{"role": "user", "content": "hi"}]}).encode()) + with urllib.request.urlopen(req, timeout=10) as r: + r.read() + with urllib.request.urlopen(base + "/metrics", timeout=10) as r: + c = json.loads(r.read())["conversation_cache"] + self.assertEqual(c["requests"], 1) + self.assertGreater(c["last_prompt"], 0) + engine.info = {} + with urllib.request.urlopen(base + "/metrics", timeout=10) as r: + self.assertFalse(json.loads(r.read())["conversation_cache"]["enabled"]) # off: the reuse lines only + finally: + httpd.shutdown() + httpd.server_close() + + def test_the_card_is_on_the_page(self): + web = Path(__file__).parent / "web" + html, js = (web / "index.html").read_text(encoding="utf-8"), (web / "app.js").read_text(encoding="utf-8") + for el in ("cc-card", "cc-slots-text", "cc-mem-text", "cc-facts", "cc-note"): + self.assertIn(f'id="{el}"', html) + self.assertIn(f'"{el}"', js) + + if __name__ == "__main__": unittest.main() diff --git a/serve/test_multigpu_server.py b/serve/test_multigpu_server.py index 221d766363..6219507917 100644 --- a/serve/test_multigpu_server.py +++ b/serve/test_multigpu_server.py @@ -132,6 +132,27 @@ def test_bound_lane_config_uses_single_physical_gpu_for_telemetry(self): self.assertNotIn("layer_split", got) self.assertNotIn("--layer-split", got["args"]) + def test_lane_config_strips_upstream_internal_parallelism(self): + cfg = { + "parallel": 4, + "args": [ + "--pack", "/m", + "--batch", "4", + "--slots", "3", + "--batch-groups", "2", + "--trim-stage-weights", + ], + } + got = M.sanitize_lane_config(cfg) + self.assertNotIn("parallel", got) + self.assertNotIn("--batch", got["args"]) + self.assertNotIn("--slots", got["args"]) + self.assertNotIn("--batch-groups", got["args"]) + self.assertNotIn("--trim-stage-weights", got["args"]) + + def test_responses_api_is_generation_work(self): + self.assertIn("/v1/responses", M.GENERATE_PATHS) + def test_lane_config_disables_upstream_conversation_parking(self): cfg = {"args": ["--pack", "/m", "--conversation-cache-mib", "8192", "--conversation-cache-slots", "4"]} diff --git a/serve/test_parallel.py b/serve/test_parallel.py new file mode 100644 index 0000000000..a970fffae7 --- /dev/null +++ b/serve/test_parallel.py @@ -0,0 +1,382 @@ +"""#465: "parallel": N - several requests decode together in the engine's batch slots (--batch N). + +A fake engine (a Python script speaking the engine's GEN / BGEN / BT / BDONE / BADM / BSTOP lines) runs behind the +real StrataEngine and Service, so the server's side is tested without a GPU: requests up to N run at once, more +wait for a slot, /metrics shows the slots, each request's history row is its own, and a conversation's next turn +goes back to the slot that holds it.""" +import json +import sys +import tempfile +import threading +import time +import unittest +import urllib.request +from pathlib import Path +from unittest import mock + +from serve.frontend import ChatTemplate +from serve.server import ByteTokenizer, Service, StrataEngine, engine_args, parallel_args, serve + +# The fake engine: one token every STEP seconds per active slot (a "window" serves every active slot at once); +# GEN (solo) streams T lines; BGEN reads the prompt (one T line, DONE), answers BADM and continues in the slot. +# Every reply is the bytes of "ok, done." then <|im_end|> (ByteTokenizer: 257). +FAKE_BATCH = r'''import queue, sys, threading, time +args = sys.argv[1:] +slots = int(args[args.index("--batch") + 1]) if "--batch" in args else 0 +fit = int(args[args.index("--fit") + 1]) if "--fit" in args else slots +STEP = 0.02 +CH = 32 +lines, stop = queue.Queue(), threading.Event() +def reader(): + for l in sys.stdin: + l = l.strip() + if l == "STOP": + stop.set() + else: + lines.put(l) + lines.put(None) +threading.Thread(target=reader, daemon=True).start() +print("INFO engine=0.1.39" + (f" batch_slots={fit}" if fit >= 2 else "") + + (" slot_cache=1" if "--slotcache" in args else ""), flush=True) +print("READY 4096 stop", flush=True) +LONG = list(b"LONGREPLY") +def reply(ids): # the rest of the reply after what the prompt already ends with (a request continued) + ids = [int(x) for x in ids] + long_one = any(ids[i:i + len(LONG)] == LONG for i in range(len(ids))) + R = list(b"ok, " + b"la " * 15 + b"done." if long_one else b"ok, done.") + [257] + k = max(k for k in range(len(R)) if k == 0 or ids[-k:] == R[:k]) + return R[k:] +active = {} # slot -> [tokens left, max_new, produced] +stopped = set() # BSTOPped slots: they end "cancel" +def window(): # one batch window: every active slot one token + for b in sorted(active): + left, max_new, produced = active[b] + t = left.pop(0) if left else 257 + produced += 1 + print(f"BT {b} {t}", flush=True) + if t == 257 or produced >= max_new: + fin = 'stop' if t == 257 else 'cancel' if b in stopped else 'length' + stopped.discard(b) + print(f"BDONE {b} {produced} {fin} 1.0", flush=True) + del active[b] + else: + active[b] = [left, max_new, produced] +log = open(args[args.index("--log") + 1], "a") if "--log" in args else None +while True: + try: + line = lines.get(timeout=STEP if active else None) + except queue.Empty: + line = "" + if line is None or line == "QUIT": + break + if line.startswith("BYIELD "): + continue # too late: the read it was for has ended + if line.startswith("BSTOP "): + b = int(line.split()[1]) + if b in active: + active[b][1] = 0 + stopped.add(b) + continue + if line.startswith(("GEN ", "BGEN ")): + f = line.split() + slot = int(f[1]) if f[0] == "BGEN" else None + max_new = int(f[2] if f[0] == "BGEN" else f[1]) + ids = f[-1].split(",") + if log: + log.write(f"{f[0]} {slot} {len(ids)} {max(len(active), 0)}\n"); log.flush() + toks = reply(ids) + stop.clear() + # the prompt read: CH tokens a chunk, a PP line each; a BYIELD gives way at a chunk boundary + given = None + for q in range(CH, len(ids) - 1, CH): + time.sleep(STEP / 2) + print(f"PP {q} {len(ids)} 1 1", flush=True) + held = [] + while True: + try: + l2 = lines.get_nowait() + except queue.Empty: + break + if l2 and l2.startswith("BYIELD "): + ys = int(l2.split()[1]) + if len(ids) - 1 - q > CH and (slot is None or ys == slot) and given is None: + given = (ys, q) + else: + held.append(l2) + for l2 in held: + lines.put(l2) + if given: + break + if active and (q // CH) % 4 == 0: # the slots decode between the prompt's chunks (as the engine) + window() + if given: + if log: + log.write(f"YIELD {given[0]} {given[1]}\n"); log.flush() + print(f"YIELDED {given[0]} {given[1]}", flush=True) + print(f"DONE 0 {len(ids)} 5.0 0.0 cancel 0 0 0", flush=True) + if slot is not None: + print(f"BADM {slot} 0", flush=True) + continue + n = 1 if slot is not None else max_new + out = 0 + for t in toks[:n]: + if stop.is_set(): + break + print(f"T {t}", flush=True) + out += 1 + time.sleep(STEP) + fin = "stop" if out and toks[out - 1] == 257 else ("cancel" if stop.is_set() else "length") + print(f"DONE {out} {len(ids)} 5.0 {out * STEP * 1000:.1f} {fin} 0 0 0", flush=True) + if slot is not None: + cont = fin == "length" and max_new > 1 + if cont: + active[slot] = [toks[1:], max_new, 1] + print(f"BADM {slot} {1 if cont else 0}", flush=True) + continue + if line: + print("ERR unknown " + line[:20], flush=True) + continue + window() +''' + + +class ParallelArgs(unittest.TestCase): + def test_parallel_config_key(self): + self.assertEqual(parallel_args({"parallel": 4}, []), ["--batch", "4"]) + self.assertEqual(parallel_args({"parallel": 1}, []), []) + self.assertEqual(parallel_args({}, []), []) + self.assertEqual(parallel_args({"parallel": 2}, ["--batch", "3"]), []) # the args' own value wins + self.assertEqual(parallel_args({"parallel": "4"}, []), []) # said and ignored + self.assertEqual(parallel_args({"parallel": True}, []), []) + self.assertEqual(parallel_args({"parallel": 12}, []), ["--batch", "12"]) # the engine warns and caps + args = engine_args({"args": ["--pack", "p"], "parallel": 2}) + self.assertEqual(args[-2:], ["--batch", "2"]) + + +class PickSlot(unittest.TestCase): + def engine(self, n): + e = StrataEngine("missing-executable", [], lazy=True) + e.batch = n + e.slot_order = list(range(n)) + e.slot_busy = [False] * n + e.slot_held = [[] for _ in range(n)] + e.slot_used = [0.0] * n + e.slot_live = [None] * n + return e + + def test_the_slot_that_holds_the_conversation(self): + e = self.engine(3) + e.slot_held = [[1, 2, 3], [], [1, 2, 3, 4, 5]] + e.slot_used = [5.0, 0.0, 9.0] + self.assertEqual(e.pick_slot([1, 2, 3, 4, 5, 6]), 2) # the longest held start + self.assertEqual(e.pick_slot([1, 2, 3, 9]), 0) + self.assertEqual(e.pick_slot([7, 8]), 1) # no match: the empty slot first + e.slot_busy[1] = True + self.assertEqual(e.pick_slot([7, 8]), 0) # then the one used longest ago + e.slot_busy = [True] * 3 + self.assertIsNone(e.pick_slot([7, 8])) + e.slot_busy = [False] * 3 + self.assertEqual(e.pick_slot([1, 2, 3]), 1) # a held prompt is never the WHOLE prompt + + def test_slots_view(self): + e = self.engine(2) + e.slot_held[1] = [1, 2] + e.slot_live[0] = {"slot": 0, "state": "decoding", "prompt_tokens": 9, "generated": 4, + "started": time.time() - 2, "first_token": time.time() - 1} + v = e.slots_view() + self.assertEqual(v[0]["state"], "decoding") + self.assertEqual(v[0]["generated"], 4) + self.assertEqual(v[1], {"slot": 1, "state": "idle", "held_tokens": 2}) + + +class ParallelService(unittest.TestCase): + """The real StrataEngine and Service over HTTP, the fake engine behind them.""" + + def start(self, slots, fit=None, slot_cache=False): + import serve.server as server + self.tmp = tempfile.TemporaryDirectory() + script = Path(self.tmp.name) / "fake_strata.py" + script.write_text(FAKE_BATCH, encoding="utf-8") + self.log = Path(self.tmp.name) / "requests.log" + real = server.subprocess.Popen + extra = ["--batch", str(slots)] + (["--fit", str(fit)] if fit is not None else []) + ["--log", str(self.log)] + extra += ["--slotcache"] if slot_cache else [] + with mock.patch.object(server.subprocess, "Popen", + lambda cmd, **kw: real([sys.executable, str(script), *cmd[1:]], **kw)): + self.engine = StrataEngine("strata", extra) + tok = ByteTokenizer() + 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): + if getattr(self, "httpd", None): + self.httpd.shutdown() + self.httpd.server_close() + if getattr(self, "engine", None): + self.engine.unload() + if getattr(self, "tmp", None): + self.tmp.cleanup() + + def chat(self, text, max_tokens=64): + body = {"messages": [{"role": "user", "content": text}], "max_tokens": max_tokens, "reasoning_effort": "none"} + req = urllib.request.Request(self.base + "/v1/chat/completions", data=json.dumps(body).encode(), + headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(req, timeout=60) as r: + return json.loads(r.read().decode()) + + def get(self, path): + with urllib.request.urlopen(self.base + path, timeout=10) as r: + return json.loads(r.read().decode()) + + def test_engine_reports_fewer_slots(self): + self.start(4, fit=2) + self.assertEqual(self.engine.batch, 2) + self.assertEqual(self.get("/v1/status")["concurrency"]["serving"], 2) + + def test_engine_turns_batching_off(self): + self.start(4, fit=0) + self.assertEqual(self.engine.batch, 0) + self.assertEqual(self.chat("hi")["choices"][0]["message"]["content"], "ok, done.") + + def test_concurrent_requests_share_the_slots(self): + self.start(2) + # the first one decodes long enough for the others to arrive while it runs (also on a busy PC) + texts = ["first question LONGREPLY", "second question, longer", "a third one waits for a slot"] + reply = {t: "ok, " + "la " * 15 + "done." if "LONGREPLY" in t else "ok, done." for t in texts} + out, errors, seen = {}, [], [] + + def go(t): + try: + out[t] = self.chat(t) + except Exception as e: # noqa: BLE001 - reported below + errors.append(e) + + threads = [threading.Thread(target=go, args=(t,)) for t in texts] + for th in threads: + th.start() + time.sleep(0.05) + deadline = time.time() + 30 + while any(th.is_alive() for th in threads) and time.time() < deadline: + m = self.get("/metrics")["live"] + seen.append(m) + time.sleep(0.01) + for th in threads: + th.join(30) + self.assertEqual(errors, []) + for t in texts: + self.assertEqual(out[t]["choices"][0]["message"]["content"], reply[t]) + # the slots were reported, and a request was admitted while another decoded in a slot (the fake logs how many + # slots were active at each admission) + self.assertTrue(any(m.get("parallel") == 2 and len(m.get("slots", [])) == 2 for m in seen)) + admitted = [int(x.split()[3]) for x in self.log.read_text().splitlines() if x.startswith("BGEN")] + self.assertIn(1, admitted) + # every request recorded its own row; nothing is left running + with self.svc.status_lock: + self.assertEqual(len(self.svc.history), 3) + self.assertEqual(self.svc.live_reqs, {}) + self.assertFalse(self.svc.status["busy"]) + self.assertEqual(sorted(r["output_tokens"] for r in self.svc.history), + sorted(len(reply[t]) + 1 for t in texts)) + # never more than two in the slots at once (the fake logs how many were active at each admission) + active = [int(x.split()[3]) for x in self.log.read_text().splitlines() if x.startswith("BGEN")] + self.assertTrue(all(a <= 1 for a in active), active) + + def race(self, first, second, delay=0.15, max_tokens=64): + """`first` sent, `second` after `delay` seconds; -> {text: (reply, seconds it took)}""" + res = {} + + def go(t): + t0 = time.time() + r = self.chat(t, max_tokens=max_tokens) + res[t] = (r["choices"][0]["message"]["content"], time.time() - t0) + a = threading.Thread(target=go, args=(first,)) + a.start() + time.sleep(delay) + go(second) + a.join(30) + return res + + def test_a_long_prompt_gives_way_to_a_short_one(self): + """#656: a long solo prompt read gives way at a chunk boundary; the short request is answered first, then the + long one goes on (in the slot its part waited in) and is answered too.""" + self.start(2) + long_q, short_q = "long " * 600, "short" + res = self.race(long_q, short_q) + self.assertEqual(res[long_q][0], "ok, done.") + self.assertEqual(res[short_q][0], "ok, done.") + # the long read takes ~1 s (3,000 tokens, 32 a chunk, 10 ms a chunk): the short one did not wait for it + self.assertLess(res[short_q][1], res[long_q][1] - 0.5, res) + log = self.log.read_text().splitlines() + self.assertTrue(any(x.startswith("YIELD ") for x in log), log) + with self.svc.status_lock: + self.assertEqual(len(self.svc.history), 2) + self.assertFalse(any(self.engine.slot_busy)) + + def test_an_admission_gives_way_too(self): + """The same while another request decodes in a slot: the long one is being admitted, a short one waits.""" + self.start(3) + bg = threading.Thread(target=lambda: self.chat("background", max_tokens=60)) + bg.start() + time.sleep(0.05) + long_q, short_q = "long " * 600, "short" + res = self.race(long_q, short_q) + bg.join(30) + self.assertEqual(res[long_q][0], "ok, done.") + self.assertLess(res[short_q][1], res[long_q][1] - 0.5, res) + log = self.log.read_text().splitlines() + self.assertTrue(any(x.startswith("YIELD ") for x in log), log) + self.assertFalse(any(self.engine.slot_busy)) + + def test_no_way_given_to_a_long_one(self): + """Two prompts of about the same length: no BYIELD (it only goes to a much shorter one).""" + self.start(2) + res = self.race("long " * 600, "lung " * 600) + self.assertEqual(sorted(r[0] for r in res.values()), ["ok, done."] * 2) + self.assertFalse(any(x.startswith("YIELD ") for x in self.log.read_text().splitlines())) + + def test_left_alone_it_goes_back_to_the_solo_path(self): + """A request decoding in a slot whose neighbour finished goes back to the solo path (MTP drafts), continued + from its slot - only with an engine that keeps the slots' conversations (INFO slot_cache=1).""" + self.start(2, slot_cache=True) + res = {} + long = threading.Thread(target=lambda: res.setdefault("long", self.chat("LONGREPLY please", max_tokens=200))) + long.start() + time.sleep(0.05) + short = self.chat("short", max_tokens=200) + long.join(30) + self.assertEqual(short["choices"][0]["message"]["content"], "ok, done.") + self.assertEqual(res["long"]["choices"][0]["message"]["content"], "ok, " + "la " * 15 + "done.") + kinds = [x.split()[0] for x in self.log.read_text().splitlines()] + # solo, then both in slots, then the long one alone again: a GEN after the BGENs + self.assertIn("BGEN", kinds) + self.assertEqual(kinds[-1], "GEN", kinds) + self.assertFalse(any(self.engine.slot_busy)) + with self.svc.status_lock: + self.assertEqual(len(self.svc.history), 2) + + def test_next_turn_goes_back_to_its_slot(self): + self.start(2) + a = "conversation A, first turn" + r1 = {} + th = threading.Thread(target=lambda: r1.setdefault("x", self.chat("blocker LONGREPLY", max_tokens=200))) + th.start() + time.sleep(0.05) + first = self.chat(a) + th.join(30) + deadline = time.time() + 10 # a slot left at its stop token is freed at its BDONE + while not [h for h in self.engine.slot_held if h] and time.time() < deadline: + time.sleep(0.02) + held = [h for h in self.engine.slot_held if h] + self.assertTrue(held, "a finished slot request keeps what its slot holds") + # the held tokens are the prompt and every token fed: all of the reply but its last token + reply = list(b"ok, done.") + [257] + mine = [h for h in held if h[-len(reply) + 1:] == reply[:-1] and bytes(x for x in h if x < 256).find(b"conversation A") >= 0] + self.assertEqual(len(mine), 1, held) + slot = self.engine.slot_held.index(mine[0]) + self.assertEqual(self.engine.pick_slot(mine[0] + [10, 11]), slot) + self.assertEqual(first["choices"][0]["message"]["content"], "ok, done.") + + +if __name__ == "__main__": + unittest.main() diff --git a/serve/test_responses.py b/serve/test_responses.py new file mode 100644 index 0000000000..2bae986966 --- /dev/null +++ b/serve/test_responses.py @@ -0,0 +1,361 @@ +"""serve/test_responses.py - #451: the Responses API (POST /v1/responses) against the mock engine (no GPU, no pack). + + python -m unittest serve.test_responses -v +""" +from __future__ import annotations + +import base64 +import contextlib +import http.client +import io +import json +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from serve.frontend import ChatTemplate # noqa: E402 +from serve.responses import (ENCRYPTED_PREFIX, ResponsesError, input_messages, request_tools, # noqa: E402 + template_kwargs, text_format) +from serve.server import ByteTokenizer, MockEngine, Service, serve # noqa: E402 + +ROOT = Path(__file__).resolve().parents[1] +TEMPLATE = ChatTemplate(ROOT / "serve/chat_template.jinja") + +CALL = ("Let me look.\n\n\nChecking.\n\n\n\n\ncat a.txt\n" + "\n\n") +ANSWER = "Read it.\n\n\nThe file says before." +TOOLS = [{"type": "function", "name": "exec_command", "description": "Runs a command.", "strict": False, + "parameters": {"type": "object", "properties": {"cmd": {"type": "string"}}, "required": ["cmd"]}}] + + +# ------------------------------------------------------------------------------------------------ parsing +class Parsing(unittest.TestCase): + def test_a_string_input_is_one_user_message(self): + self.assertEqual(input_messages({"input": "hi", "instructions": "Be brief."}), + [{"role": "system", "content": "Be brief."}, {"role": "user", "content": "hi"}]) + + def test_leading_developer_messages_join_the_system_message_later_ones_become_user(self): + msgs = input_messages({"instructions": "I", "input": [ + {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": "D1"}, + {"type": "input_text", "text": "D2"}]}, + {"role": "user", "content": "u"}, + {"type": "message", "role": "developer", "content": "late"}]}) + self.assertEqual(msgs, [{"role": "system", "content": "I\n\nD1D2"}, {"role": "user", "content": "u"}, + {"role": "user", "content": "late"}]) + + def test_codex_history_becomes_one_assistant_turn_per_answer(self): + items = [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "edit it"}]}, + {"type": "reasoning", "id": "rs_1", "summary": [], + "content": [{"type": "reasoning_text", "text": "look first"}]}, + {"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "Looking."}]}, + {"type": "function_call", "id": "fc_1", "call_id": "c1", "name": "exec_command", + "arguments": "{\"cmd\":\"cat a\"}"}, + {"type": "function_call", "id": "fc_2", "call_id": "c2", "name": "spawn_agent", + "namespace": "multi_agent_v1", "arguments": "{}"}, + # results in the order they finished, not the order of the calls + {"type": "function_call_output", "call_id": "c2", "output": "agent"}, + {"type": "function_call_output", "call_id": "c1", + "output": [{"type": "input_text", "text": "before"}]}, + {"type": "reasoning", "id": "rs_2", "summary": [{"type": "summary_text", "text": "s"}], + "content": [{"type": "reasoning_text", "text": "now write"}]}, + {"type": "function_call", "call_id": "c3", "name": "exec_command", "arguments": "{\"cmd\":\"w\"}"}, + {"type": "function_call_output", "call_id": "c3", "output": "ok"}] + self.assertEqual(input_messages({"input": items}), [ + {"role": "user", "content": "edit it"}, + {"role": "assistant", "content": "Looking.", "reasoning_content": "look first", "tool_calls": [ + {"function": {"name": "exec_command", "arguments": {"cmd": "cat a"}}}, + {"function": {"name": "multi_agent_v1.spawn_agent", "arguments": {}}}]}, + {"role": "tool", "content": "before"}, {"role": "tool", "content": "agent"}, + {"role": "assistant", "content": "", "reasoning_content": "now write", "tool_calls": [ + {"function": {"name": "exec_command", "arguments": {"cmd": "w"}}}]}, + {"role": "tool", "content": "ok"}]) + + def test_reasoning_comes_back_from_our_encrypted_content_and_foreign_tokens_are_ignored(self): + enc = ENCRYPTED_PREFIX + base64.b64encode(json.dumps({"text": "hidden"}).encode()).decode() + for item, want in (({"encrypted_content": enc, "summary": []}, "hidden"), + ({"encrypted_content": "gAAAAAB-someone-elses", "summary": []}, None)): + msgs = input_messages({"input": [{"role": "user", "content": "q"}, {"type": "reasoning", **item}, + {"role": "assistant", "content": "a"}]}) + self.assertEqual(msgs[1].get("reasoning_content"), want) + with self.assertRaises(ResponsesError) as e: + input_messages({"input": [{"type": "reasoning", "encrypted_content": ENCRYPTED_PREFIX + "!!"}]}) + self.assertEqual(e.exception.param, "input[0].encrypted_content") + + def test_bad_arguments_from_a_cut_off_call_do_not_refuse_the_conversation(self): + msgs = input_messages({"input": [{"role": "user", "content": "q"}, + {"type": "function_call", "call_id": "c", "name": "f", "arguments": "{\"a"}, + {"type": "function_call_output", "call_id": "c", "output": "x"}]}) + self.assertEqual(msgs[1]["tool_calls"][0]["function"]["arguments"], {"arguments": "{\"a"}) + + def test_unsupported_items_are_named(self): + for items, param in (([{"type": "item_reference", "id": "msg_1"}], "input[0]"), + ([{"type": "web_search_call"}], "input[0].type"), + ([{"role": "user", "content": [{"type": "input_file", "file_id": "f"}]}], + "input[0].content[0].type"), + ([{"role": "robot", "content": "x"}], "input[0].role")): + with self.assertRaises(ResponsesError) as e: + input_messages({"input": items}) + self.assertEqual(e.exception.param, param) + with self.assertRaises(ResponsesError): + input_messages({}) + + def test_tools_namespaces_custom_and_hosted(self): + tools, names, skipped = request_tools({"tools": [ + *TOOLS, {"type": "namespace", "name": "mcp_fs", "description": "Files.", + "tools": [{"type": "function", "name": "read", "parameters": {"type": "object"}}]}, + {"type": "custom", "name": "apply_patch", "description": "Patch.", + "format": {"type": "grammar", "syntax": "lark", "definition": "start: \"x\""}}, + {"type": "web_search"}]}) + self.assertEqual([t["name"] for t in tools], ["exec_command", "mcp_fs.read", "apply_patch"]) + self.assertEqual(tools[1]["description"], "Files.") + self.assertIn("start: \"x\"", tools[2]["description"]) + self.assertEqual(names["mcp_fs.read"], ("mcp_fs", "read", "function")) + self.assertEqual(names["apply_patch"], (None, "apply_patch", "custom")) + self.assertEqual(skipped, ["web_search"]) + self.assertIsNone(request_tools({"tools": TOOLS, "tool_choice": "none"})[0]) + + def test_effort_and_text_format(self): + self.assertEqual(template_kwargs({"reasoning": {"effort": "minimal"}}, {}), {"enable_thinking": False}) + self.assertEqual(template_kwargs({"reasoning": {"effort": "high"}}, {}), {"reasoning_effort": "xhigh"}) + self.assertEqual(template_kwargs({"reasoning": {"summary": "auto"}}, {}), {}) # Codex: the default + self.assertEqual(template_kwargs({}, {"reasoning_effort": "low"}), {"reasoning_effort": "low"}) + with self.assertRaises(ResponsesError): + template_kwargs({"reasoning": {"effort": "huge"}}, {}) + self.assertEqual(text_format({"text": {"format": {"type": "json_schema", "name": "n", "schema": {}}}}), + {"type": "json_schema", "json_schema": {"name": "n", "schema": {}}}) + self.assertIsNone(text_format({"text": {"format": {"type": "text"}}})) + + +# ------------------------------------------------------------------------------------------------ over HTTP +class Server(unittest.TestCase): + script = ANSWER + + def setUp(self): + self.tok = ByteTokenizer() + self.engine = MockEngine(self.tok, self.script, max_context=16384) + self.svc = Service(self.engine, self.tok, TEMPLATE) + self.httpd = serve(self.svc, port=0) + self.port = self.httpd.server_address[1] + + def tearDown(self): + self.httpd.shutdown() + self.httpd.server_close() + + def post(self, body, path="/v1/responses", headers=None): + """(status, parsed body: a dict, or the list of SSE events of a stream).""" + c = http.client.HTTPConnection("127.0.0.1", self.port, timeout=30) + try: + with contextlib.redirect_stdout(io.StringIO()): + c.request("POST", path, body=json.dumps(body).encode(), + headers={"Content-Type": "application/json", **(headers or {})}) + r = c.getresponse() + raw = r.read().decode() + if r.getheader("Content-Type") == "text/event-stream": + events = [] + for block in raw.split("\n\n"): + lines = block.strip().splitlines() + if not lines or lines[0].startswith(":"): + continue + name = lines[0][len("event: "):] + data = json.loads(lines[1][len("data: "):]) + self.assertEqual(name, data["type"]) + events.append(data) + return r.status, events + return r.status, json.loads(raw) + finally: + c.close() + + + +class OverHttp(Server): + def test_non_streaming_text_with_reasoning(self): + code, r = self.post({"model": "m", "input": "hi", "store": False}) + self.assertEqual(code, 200) + self.assertEqual((r["object"], r["status"], r["error"]), ("response", "completed", None)) + rs, msg = r["output"] + self.assertEqual(rs["type"], "reasoning") + self.assertEqual(rs["content"], [{"type": "reasoning_text", "text": "Read it.\n"}]) + self.assertEqual(rs["summary"], []) + self.assertNotIn("encrypted_content", rs) + self.assertEqual(msg["type"], "message") + self.assertEqual(msg["content"][0]["text"], "The file says before.") + u = r["usage"] + self.assertEqual(u["output_tokens"], len(self.tok.encode(ANSWER)) + 1) # + the end-of-turn token + self.assertEqual(u["output_tokens_details"]["reasoning_tokens"], len("Read it.\n")) + self.assertEqual(u["total_tokens"], u["input_tokens"] + u["output_tokens"]) + self.assertEqual(r["store"], False) + + def test_stream_event_order(self): + code, events = self.post({"model": "m", "input": "hi", "stream": True, + "include": ["reasoning.encrypted_content"]}) + self.assertEqual(code, 200) + types = [e["type"] for e in events] + deltas_r = types.count("response.reasoning_text.delta") + deltas_t = types.count("response.output_text.delta") + self.assertGreater(deltas_r, 0) + self.assertGreater(deltas_t, 0) + self.assertEqual(types, ["response.created", "response.in_progress", + "response.output_item.added", *["response.reasoning_text.delta"] * deltas_r, + "response.reasoning_text.done", "response.output_item.done", + "response.output_item.added", "response.content_part.added", + *["response.output_text.delta"] * deltas_t, + "response.output_text.done", "response.content_part.done", + "response.output_item.done", "response.completed"]) + self.assertEqual([e["sequence_number"] for e in events], list(range(len(events)))) + final = events[-1]["response"] + self.assertEqual(final["status"], "completed") + rs, msg = final["output"] + self.assertEqual([e["item"]["id"] for e in events if e["type"] == "response.output_item.done"], + [rs["id"], msg["id"]]) + self.assertEqual("".join(e["delta"] for e in events if e["type"] == "response.output_text.delta"), + msg["content"][0]["text"]) + self.assertTrue(rs["encrypted_content"].startswith(ENCRYPTED_PREFIX)) + # the encrypted_content alone brings the thinking back into the prompt + replay = input_messages({"input": [{"role": "user", "content": "hi"}, + {"type": "reasoning", "summary": [], "encrypted_content": + rs["encrypted_content"]}, msg]}) + self.assertEqual(replay[1]["reasoning_content"], "Read it.\n") + + def test_max_output_tokens_ends_incomplete(self): + code, r = self.post({"model": "m", "input": "hi", "max_output_tokens": 5}) + self.assertEqual(code, 200) + self.assertEqual(r["status"], "incomplete") + self.assertEqual(r["incomplete_details"], {"reason": "max_output_tokens"}) + self.assertEqual(r["output"][0]["status"], "incomplete") + code, events = self.post({"model": "m", "input": "hi", "max_output_tokens": 5, "stream": True}) + self.assertEqual(events[-1]["type"], "response.incomplete") + + def test_effort_none_does_not_think(self): + self.engine.scripts = [self.tok.encode("Hello.<|im_end|>", parse_special=True)] + self.engine.script = self.engine.scripts[0] + code, r = self.post({"model": "m", "input": "hi", "reasoning": {"effort": "none"}}) + self.assertEqual([o["type"] for o in r["output"]], ["message"]) + self.assertIn("\n\n", self.tok.decode(self.engine.last_prompt)) + + def test_errors_use_the_responses_format(self): + for body, param in (({"model": "m"}, "input"), ({"model": "m", "input": "x", "previous_response_id": "r"}, + "previous_response_id"), + ({"model": "m", "input": "x", "reasoning": {"effort": "huge"}}, "reasoning.effort")): + code, r = self.post(body) + self.assertEqual(code, 400) + self.assertEqual(set(r["error"]), {"message", "type", "param", "code"}) + self.assertEqual(r["error"]["param"], param) + code, r = self.post({}, path="/v1/responses/resp_123/cancel") + self.assertEqual(code, 404) + self.assertEqual(r["error"]["code"], "not_found") + c = http.client.HTTPConnection("127.0.0.1", self.port, timeout=30) + c.request("POST", "/v1/responses", body=b"{not json", headers={"Content-Type": "application/json"}) + r = c.getresponse() + err = json.loads(r.read())["error"] + c.close() + self.assertEqual((r.status, err["type"], err["param"]), (400, "invalid_request_error", None)) + + def test_too_long_for_the_context(self): + code, r = self.post({"model": "m", "input": "x" * 20000}) + self.assertEqual(code, 400) + self.assertEqual(r["error"]["code"], "context_length_exceeded") + + def test_api_key_and_foreign_pages(self): + self.svc.api_key = "s3cret" + self.assertEqual(self.post({"model": "m", "input": "hi"})[0], 401) + self.assertEqual(self.post({"model": "m", "input": "hi"}, headers={"Authorization": "Bearer s3cret"})[0], 200) + self.svc.api_key = None + code, r = self.post({"model": "m", "input": "hi"}, headers={"Origin": "https://evil.example.com"}) + self.assertEqual(code, 403) + + def test_json_schema_text_format(self): + self.engine.scripts = [self.tok.encode("\n\n{\"n\": 3}<|im_end|>", parse_special=True)] + self.engine.script = self.engine.scripts[0] + fmt = {"type": "json_schema", "name": "num", "strict": True, + "schema": {"type": "object", "properties": {"n": {"type": "integer"}}, "required": ["n"]}} + code, r = self.post({"model": "m", "input": "a number", "text": {"format": fmt}}) + self.assertEqual(code, 200, r) + self.assertEqual(r["output"][-1]["content"][0]["text"], "{\"n\":3}") + code, events = self.post({"model": "m", "input": "a number", "text": {"format": fmt}, "stream": True}) + self.assertEqual([e["delta"] for e in events if e["type"] == "response.output_text.delta"], ["{\"n\":3}"]) + self.assertEqual(events[-1]["type"], "response.completed") + # an answer that fails the schema + bad = {**fmt, "schema": {**fmt["schema"], "properties": {"n": {"type": "string"}}}} + code, r = self.post({"model": "m", "input": "a number", "text": {"format": bad}}) + self.assertEqual((code, r["error"]["code"]), (502, "structured_output_failed")) + code, events = self.post({"model": "m", "input": "a number", "text": {"format": bad}, "stream": True}) + self.assertEqual(events[-1]["type"], "response.failed") + self.assertEqual(events[-1]["response"]["error"]["code"], "structured_output_failed") + + def test_monitor_and_status(self): + self.svc.api_monitor = True + self.post({"model": "m", "input": "hi", "stream": True}) + rec = self.svc.request_records()[0] + self.assertEqual(rec["path"], "/v1/responses") + full = self.svc.request_records(rec["id"]) + self.assertEqual(full["output"], "The file says before.") + self.assertEqual(full["reasoning"], "Read it.\n") + self.assertIn("/v1/responses", self.svc.v1_status()["dialects"]) + + +class ToolRoundTrip(Server): + script = [CALL, ANSWER] + + def test_function_call_round_trip(self): + user = {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "what is in a.txt?"}]} + code, events = self.post({"model": "m", "instructions": "You are a coding agent.", "input": [user], + "tools": TOOLS, "store": False, "stream": True, + "include": ["reasoning.encrypted_content"], "reasoning": {"summary": "auto"}, + "parallel_tool_calls": True, "tool_choice": "auto"}) + self.assertEqual(code, 200) + first_prompt = self.tok.decode(self.engine.last_prompt) + types = [e["type"] for e in events] + n_args = types.count("response.function_call_arguments.delta") + self.assertGreater(n_args, 0) + call_part = types[types.index("response.output_text.done") + 2:] + self.assertEqual(call_part, ["response.output_item.done", "response.output_item.added", + *["response.function_call_arguments.delta"] * n_args, + "response.function_call_arguments.done", "response.output_item.done", + "response.completed"]) + output = events[-1]["response"]["output"] + self.assertEqual([o["type"] for o in output], ["reasoning", "message", "function_call"]) + fc = output[2] + self.assertEqual((fc["name"], json.loads(fc["arguments"]), fc["status"]), + ("exec_command", {"cmd": "cat a.txt"}, "completed")) + self.assertTrue(fc["call_id"]) + added = next(e for e in events if e["type"] == "response.output_item.added" + and e["item"]["type"] == "function_call") + self.assertEqual((added["item"]["call_id"], added["item"]["arguments"]), (fc["call_id"], "")) + # Codex sends everything back, plus the result: the prompt continues exactly where the model stopped + code, r = self.post({"model": "m", "instructions": "You are a coding agent.", "tools": TOOLS, "store": False, + "input": [user, *output, {"type": "function_call_output", "call_id": fc["call_id"], + "output": "before"}]}) + self.assertEqual(code, 200, r) + second_prompt = self.tok.decode(self.engine.last_prompt) + self.assertTrue(second_prompt.startswith(first_prompt + CALL + "<|im_end|>"), second_prompt[-600:]) + self.assertIn("\nbefore\n", second_prompt) + self.assertEqual(r["output"][-1]["content"][0]["text"], "The file says before.") + + def test_namespace_and_custom_calls_come_back_under_their_own_names(self): + self.engine.scripts = [self.tok.encode( + "\n\n\n\n\nhi\n\n" + "\n\n\n\n\n*** Begin Patch\n" + "\n\n<|im_end|>", parse_special=True)] + self.engine.script = self.engine.scripts[0] + tools = [{"type": "namespace", "name": "multi_agent_v1", "description": "Agents.", "tools": [ + {"type": "function", "name": "spawn_agent", "strict": False, + "parameters": {"type": "object", "properties": {"message": {"type": "string"}}}}]}, + {"type": "custom", "name": "apply_patch", "description": "Edit files."}] + code, r = self.post({"model": "m", "input": "go", "tools": tools}) + self.assertEqual(code, 200, r) + ns, custom = r["output"] + self.assertEqual((ns["type"], ns["namespace"], ns["name"], json.loads(ns["arguments"])), + ("function_call", "multi_agent_v1", "spawn_agent", {"message": "hi"})) + self.assertEqual((custom["type"], custom["name"], custom["input"]), + ("custom_tool_call", "apply_patch", "*** Begin Patch")) + msgs = input_messages({"input": [{"role": "user", "content": "go"}, ns, custom, + {"type": "function_call_output", "call_id": ns["call_id"], "output": "1"}, + {"type": "custom_tool_call_output", "call_id": custom["call_id"], + "output": "2"}]}) + self.assertEqual([c["function"]["name"] for c in msgs[1]["tool_calls"]], + ["multi_agent_v1.spawn_agent", "apply_patch"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/serve/test_runconfig.py b/serve/test_runconfig.py new file mode 100644 index 0000000000..bb17119698 --- /dev/null +++ b/serve/test_runconfig.py @@ -0,0 +1,165 @@ +"""serve/test_runconfig.py - #564: the web page's Settings view of the run config (GET / POST /config), against the +mock engine: only the listed keys change, every other key of the file stays, a bad value changes nothing, and only +Strata's own page may write it (JSON, no foreign Origin, the API key when one is set). + + python -m unittest serve.test_runconfig +""" +from __future__ import annotations + +import contextlib +import io +import json +import tempfile +import unittest +import urllib.error +import urllib.request +from pathlib import Path + +from serve import runconfig +from serve.frontend import ChatTemplate +from serve.server import ByteTokenizer, MockEngine, Service, serve + +ROOT = Path(__file__).resolve().parents[1] +CFG = {"exe": "engine/strata.exe", "args": ["--pack", "data/packs/q2_0", "--kv", "int8", "--spec-min-p", "0.5"], + "model_name": "qwen3.8-flash-next-q2_0", "port": 8080, "host": "0.0.0.0", "api_key": "", + "sampling": {"temperature": 0.7, "presence_penalty": 1.5}, + "mcp_servers": {"files": {"command": "npx", "args": ["-y", "server-filesystem", "."]}}, + "cors_origins": ["http://localhost:3000"]} + + +class Apply(unittest.TestCase): + def test_only_the_named_keys_change(self): + new, changed = runconfig.apply(CFG, {"sampling.top_p": 0.95, "reasoning_budget_tokens": 4096, + "vram_reserve_mib": 2048, "aliases": "qwen, local", "lazy_load": True}) + self.assertEqual(changed, ["sampling.top_p", "reasoning_budget_tokens", "vram_reserve_mib", "aliases", + "lazy_load"]) + self.assertEqual(new["sampling"], {"temperature": 0.7, "presence_penalty": 1.5, "top_p": 0.95}) + self.assertEqual(new["args"][-2:], ["--vram-reserve-mib", "2048"]) + self.assertEqual(new["aliases"], ["qwen", "local"]) + for k in ("exe", "model_name", "port", "host", "api_key", "mcp_servers", "cors_origins"): + self.assertEqual(new[k], CFG[k], k) + self.assertEqual(CFG["sampling"], {"temperature": 0.7, "presence_penalty": 1.5}) # the input is left alone + + def test_null_is_the_default_again(self): + cfg, _ = runconfig.apply(CFG, {"vram_reserve_mib": 2048, "fit_max_tokens": True}) + new, changed = runconfig.apply(cfg, {"vram_reserve_mib": None, "fit_max_tokens": None, + "sampling.temperature": None, "idle_unload_s": None}) + self.assertEqual(changed, ["vram_reserve_mib", "fit_max_tokens", "sampling.temperature"]) + self.assertEqual(new["args"], CFG["args"]) + self.assertNotIn("fit_max_tokens", new) + self.assertEqual(new["sampling"], {"presence_penalty": 1.5}) + new, _ = runconfig.apply({"sampling": {"top_k": 20}}, {"sampling.top_k": None}) + self.assertNotIn("sampling", new) # an empty block goes + + def test_refused(self): + for changes in ({"api_key": "x"}, {"host": "0.0.0.0"}, {"mcp_servers": {}}, {"before_load": "calc.exe"}, + {"args": []}, {"exe": "evil.exe"}, {"trusted_origins": ["*"]}, {"allowed_hosts": ["*"]}, + {"sampling.temperature": -1}, {"sampling.top_p": 0}, {"sampling.top_k": 65}, + {"sampling.min_p": 2}, {"reasoning_budget_tokens": 1.5}, {"fit_max_tokens": "yes"}, + {"effort_position": "middle"}, {"vram_reserve_mib": -5}, {"idle_unload_s": True}, + {"aliases": [1]}, {}, None, ["x"]): + with self.subTest(changes=changes), self.assertRaises(ValueError): + runconfig.apply(CFG, changes) + with self.assertRaises(ValueError): # one bad value: nothing is applied + runconfig.apply(CFG, {"fit_max_tokens": True, "sampling.top_k": 0}) + with self.assertRaises(ValueError): + runconfig.apply({**CFG, "vision": {"gpu": True}}, {"lazy_load": True}) + + def test_view(self): + v = runconfig.view({**CFG, "args": CFG["args"] + ["--vram-reserve-mib", "1500"]}, "x/strata-q2_0.json") + self.assertEqual(v["file"], "strata-q2_0.json") + got = {k["key"]: k for k in v["keys"]} + self.assertEqual(got["sampling.temperature"]["value"], 0.7) + self.assertEqual(got["vram_reserve_mib"]["value"], 1500) + self.assertIsNone(got["fit_max_tokens"]["value"]) + self.assertEqual(got["anthropic_thinking"]["choices"], ["model", "on_request"]) + self.assertNotIn("api_key", got) + self.assertNotIn("mcp_servers", got) + + +class Http(unittest.TestCase): + def setUp(self): + self.dir = tempfile.TemporaryDirectory() + self.path = Path(self.dir.name) / "strata-q2_0.json" + self.path.write_text(json.dumps(CFG, indent=1), encoding="utf-8") + tok = ByteTokenizer() + self.svc = Service(MockEngine(tok, "ok", max_context=4096), tok, ChatTemplate(ROOT / "serve/chat_template.jinja")) + self.svc.config_path = str(self.path) + self.httpd = serve(self.svc, port=0) + self.host = f"127.0.0.1:{self.httpd.server_address[1]}" + + def tearDown(self): + self.httpd.shutdown() + self.httpd.server_close() + self.dir.cleanup() + + def call(self, method, body=None, headers=None): + h = {"Content-Type": "application/json", **(headers or {})} + req = urllib.request.Request(f"http://{self.host}/config", method=method, headers=h, + data=None if body is None else (body if isinstance(body, bytes) + else json.dumps(body).encode())) + try: + with urllib.request.urlopen(req, timeout=10) as r: + return r.status, json.loads(r.read()) + except urllib.error.HTTPError as e: + with e: + return e.code, json.loads(e.read()) + + def saved(self): + return json.loads(self.path.read_text(encoding="utf-8")) + + def test_read_and_write_from_the_page(self): + status, v = self.call("GET") + self.assertEqual(status, 200, v) + self.assertEqual({k["key"]: k["value"] for k in v["keys"]}["sampling.temperature"], 0.7) + with contextlib.redirect_stdout(io.StringIO()): + status, v = self.call("POST", {"set": {"sampling.temperature": 1.0, "open_browser": False}}, + {"Origin": f"http://{self.host}"}) + self.assertEqual(status, 200, v) + self.assertEqual(v["changed"], ["sampling.temperature", "open_browser"]) + cfg = self.saved() + self.assertEqual(cfg["sampling"]["temperature"], 1.0) + self.assertIs(cfg["open_browser"], False) + for k in ("mcp_servers", "cors_origins", "host", "args", "exe"): + self.assertEqual(cfg[k], CFG[k], k) + self.assertEqual(json.loads((self.path.parent / "strata-q2_0.json.bak").read_text(encoding="utf-8")), CFG) + status, v = self.call("POST", {"set": {"open_browser": False}}) # no Origin (a script on this PC): allowed + self.assertEqual((status, v["changed"]), (200, [])) + + def test_only_from_strata_s_own_page(self): + before = self.path.read_bytes() + status, _ = self.call("POST", {"set": {"fit_max_tokens": True}}, {"Origin": "https://evil.example"}) + self.assertEqual(status, 403) + status, _ = self.call("POST", b'{"set": {"fit_max_tokens": true}}', {"Content-Type": "text/plain"}) + self.assertEqual(status, 415) + for body in ({"set": {"api_key": ""}}, {"set": {"mcp_servers": {"x": {"command": "calc"}}}}, + {"set": {"sampling.top_k": 0}}, {"nothing": 1}, b"not json"): + status, b = self.call("POST", body, {"Origin": f"http://{self.host}"}) + self.assertEqual(status, 400, (body, b)) + self.assertEqual(self.path.read_bytes(), before) # nothing was written + self.assertFalse((self.path.parent / "strata-q2_0.json.bak").exists()) + + def test_the_api_key(self): + self.svc.api_key = "secret" + self.assertEqual(self.call("GET")[0], 401) + self.assertEqual(self.call("POST", {"set": {"fit_max_tokens": True}})[0], 401) + with contextlib.redirect_stdout(io.StringIO()): + status, _ = self.call("POST", {"set": {"fit_max_tokens": True}}, {"Authorization": "Bearer secret"}) + self.assertEqual(status, 200) + self.assertIs(self.saved()["fit_max_tokens"], True) + + def test_without_a_run_config(self): + self.svc.config_path = None + self.assertEqual(self.call("GET")[0], 404) + self.assertEqual(self.call("POST", {"set": {"fit_max_tokens": True}})[0], 404) + + def test_the_page_has_the_view(self): + web = ROOT / "serve" / "web" + html, js = (web / "index.html").read_text(encoding="utf-8"), (web / "app.js").read_text(encoding="utf-8") + for el in ("cfg-card", "cfg-form", "cfg-save", "cfg-msg"): + self.assertIn(f'id="{el}"', html) + self.assertIn('fetch("config", {method: "POST", headers: headers(true)', js) + + +if __name__ == "__main__": + unittest.main() diff --git a/serve/test_security.py b/serve/test_security.py index cebd756d12..429c716217 100644 --- a/serve/test_security.py +++ b/serve/test_security.py @@ -238,6 +238,31 @@ def test_with_a_key_the_key_decides(self): {**headers, "Authorization": "Bearer s3cret"})[0], 200) # --- /unload and /load + def test_control_body_arrives_before_the_operation_and_reply(self): + self.start() + for body, expected in ((b"{}", 200), (b"{", 400)): + c = http.client.HTTPConnection("127.0.0.1", self.port, timeout=5) + try: + with mock.patch.object(self.svc, "unload", return_value="unloaded") as unload: + c.putrequest("POST", "/unload") + c.putheader("Content-Type", "application/json") + c.putheader("Content-Length", "2") + c.endheaders() + c.sock.settimeout(0.1) + with self.assertRaises(socket.timeout): + c.sock.recv(1) + unload.assert_not_called() + c.sock.settimeout(5) + c.send(body) + if len(body) < 2: + c.sock.shutdown(socket.SHUT_WR) + r = c.getresponse() + self.assertEqual(r.status, expected) + r.read() + self.assertEqual(unload.call_count, int(expected == 200)) + finally: + c.close() + def test_unload_and_load_need_json_from_the_own_page(self): self.start() form = {"Content-Type": "application/x-www-form-urlencoded"} diff --git a/serve/test_server.py b/serve/test_server.py index 76c1797a3f..0ffe2f6c97 100644 --- a/serve/test_server.py +++ b/serve/test_server.py @@ -21,12 +21,14 @@ 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, # noqa: E402 - engine_args, prompt_tokens_seen, request_timings, serve, start_failure_hint) + engine_args, layer_split_value, prompt_tokens_seen, request_timings, serve, + start_failure_hint) from types import SimpleNamespace # noqa: E402 ROOT = Path(__file__).resolve().parents[1] CTX = 4096 -ANSWER = "x" * 2000 # longer than the old 1024 fallback: one token per byte +ANSWER = "xy" * 1000 # longer than the old 1024 fallback: one token per byte (#606: not +# one token repeated, which the server ends at 256) class RecordingEngine(MockEngine): @@ -241,6 +243,190 @@ def test_literal_marker_with_an_image(self): svc.embeddings.path.unlink(missing_ok=True) +class ThinkTokenizer(ByteTokenizer): + """The byte tokenizer with the model's reasoning markers as specials that are matched even without parse_special, + as the real tokenizer does (GGUF token type 4).""" + SPECIALS = ByteTokenizer.SPECIALS + ["", ""] + ALWAYS = ("", "") + + +class LiteralThinkTags(unittest.TestCase): + """#537: a / written inside a message is text, not the model's reasoning markers: a quoted + "" no longer ends the model's reasoning before it starts. The template's own markers stay special.""" + + @classmethod + def setUpClass(cls): + cls.tok = ThinkTokenizer() + cls.svc = Service(MockEngine(cls.tok, "ok", max_context=CTX), cls.tok, + ChatTemplate(ROOT / "serve/chat_template.jinja")) + cls.open, cls.close = (cls.tok.encode(t)[0] for t in ("", "")) + + def ids(self, messages, tools=None, **kw): + return self.svc.prepare(messages, tools, kw)[0] + + def old_ids(self, messages, tools=None, **kw): + return self.tok.encode(self.svc.template.render(messages, tools=tools, **kw), parse_special=True) + + def test_a_quoted_tag_in_a_user_message(self): + text = "Quote this exact literal string, then explain it: and " + ids = self.ids([{"role": "user", "content": text}]) + self.assertEqual(ids.count(self.close), 0) + self.assertEqual(ids.count(self.open), 1) # the generation prompt's own + self.assertEqual(ids[-2:], [self.open, ord("\n")]) + self.assertIn(text, self.tok.decode(ids)) # the text is all there, as text + self.assertEqual(ids.count(self.close), 0) + # thinking off: the template's empty block stays two specials, the user's tag is text + ids = self.ids([{"role": "user", "content": text}], enable_thinking=False) + self.assertEqual((ids.count(self.open), ids.count(self.close)), (1, 1)) + + def test_without_a_tag_the_prompt_is_unchanged(self): + msgs = [{"role": "system", "content": "Be brief."}, {"role": "user", "content": "1+1?"}, + {"role": "assistant", "content": "2", "reasoning_content": "easy"}, {"role": "user", "content": "x"}] + self.assertEqual(self.ids(msgs), self.old_ids(msgs)) + self.assertEqual(self.ids(msgs, enable_thinking=False), self.old_ids(msgs, enable_thinking=False)) + + def test_history_tool_results_and_tools(self): + msgs = [{"role": "user", "content": "go"}, + {"role": "assistant", "content": "It wrote here.", "reasoning_content": "the tag", + "tool_calls": [{"function": {"name": "write", "arguments": {"text": "a b"}}}]}, + {"role": "tool", "content": "file has in it"}, + {"role": "user", "content": "why did you write "}] + tools = [{"name": "write", "description": "writes text (may contain )", "parameters": {}}] + ids = self.ids(msgs, tools) + # the template's markers: the history turn's ... and the generation prompt's + self.assertEqual((ids.count(self.open), ids.count(self.close)), (2, 1)) + text = self.tok.decode(ids) + for part in ("It wrote here.", "the tag", "a b", "file has in it", + "why did you write ", "may contain "): + self.assertIn(part, text) + + def test_a_client_that_sends_the_reasoning_inline(self): + # an assistant turn whose content opens with its own ... block keeps that block's markers + msgs = [{"role": "user", "content": "hi"}, + {"role": "assistant", "content": "\nplan: say hello\n\n\nHello, is a tag."}, + {"role": "user", "content": "again"}] + ids = self.ids(msgs) + self.assertEqual((ids.count(self.open), ids.count(self.close)), (3, 2)) # template's + the inline block's + self.assertIn("Hello, is a tag.", self.tok.decode(ids)) + + def test_the_real_tokenizer_reads_the_tag_as_text(self): + import strata_tokenizer as ST + b2u = ST.bytes_to_unicode() + tokens = [b2u[b] for b in range(256)] + ["", "", "<|im_end|>"] + tok = ST.Tokenizer(tokens, [], [1] * 256 + [4, 4, 3]) + text = "say now<|im_end|>" + self.assertEqual(tok.encode(text, parse_special=True), [*b"say ", 257, *b" now", 258]) + start = text.index("") + plain = tok.encode(text, parse_special=True, plain=[(start, start + len(""))]) + self.assertEqual(plain, [*b"say now", 258]) # the tag as text, im_end still special + + +class EffortAtTheEnd(unittest.TestCase): + """#458 (opt-in "effort_position": "end"): a non-default effort goes in a system turn right before the answer, so + a request that changes only the effort keeps the cached conversation. The engine's checkpoint rule is simulated + on the token ids (src/program/generate.cpp: the last <|im_start|>, and with --tail-role-token the one in front + of a trailing system turn); the default prompt does not change.""" + + TURN = 256 # the byte tokenizer's <|im_start|> + ROLE = ord("s") # "system"'s first byte stands for its token + + def setUp(self): + self.tok = ByteTokenizer() + self.svc = Service(MockEngine(self.tok, "ok", max_context=1 << 20), self.tok, + ChatTemplate(ROOT / "serve/chat_template.jinja")) + + def ids(self, messages, end, **kw): + self.svc.effort_end = end + return self.svc.encode_prompt(messages, None, kw) + + def checkpoint(self, ids, tail, resume=0): + """Where the engine checkpoints a prompt (the tokens before it), as generate.cpp's serve loop does.""" + turn_at = next((i for i in range(len(ids) - 1, resume, -1) if ids[i] == self.TURN), -1) + if turn_at > 0 and tail: + i = next((i for i in range(turn_at - 1, resume, -1) if ids[i] == self.TURN), None) + if i is not None and ids[i + 1] == self.ROLE: + turn_at = i + return ids[:turn_at] + + def session(self, requests, end, tail): + """The reused tokens of each request after the first: the longest earlier checkpoint it starts with.""" + checks, reused = [], [] + for messages, kw in requests: + ids = self.ids(messages, end, **kw) + reused.append(max([len(c) for c in checks if ids[:len(c)] == c], default=0)) + checks.append(self.checkpoint(ids, tail)) + return reused[1:] + + CHAT = [{"role": "system", "content": "You are a careful assistant. " * 40}, + {"role": "user", "content": "Explain the conversation cache. " * 20}] + REPLY = {"role": "assistant", "content": "It keeps the prompt's state. " * 20, "reasoning_content": "ok"} + NEXT = {"role": "user", "content": "And the checkpoints?"} + + def test_the_default_prompt_is_unchanged(self): + for kw in ({}, {"reasoning_effort": "xhigh"}): + self.assertEqual(self.ids(self.CHAT, True, **kw), self.ids(self.CHAT, False, **kw)) + self.assertEqual(self.ids(self.CHAT, True, **kw), + self.tok.encode(self.svc.template.render(self.CHAT, **kw), parse_special=True)) + for kw in ({"reasoning_effort": "low"}, {"reasoning_effort": "medium"}, {"enable_thinking": False}): + self.assertEqual(self.ids(self.CHAT, False, **kw), # off: every effort renders as before + self.tok.encode(self.svc.template.render(self.CHAT, **kw), parse_special=True)) + + def test_the_trailing_turn(self): + text = self.tok.decode(self.ids(self.CHAT, True, reasoning_effort="low")) + self.assertTrue(text.endswith("<|im_end|>\n<|im_start|>system\nReasoning effort is set to low. Keep your " + "thinking brief and focused, moving directly to the conclusion without " + "unnecessary elaboration.<|im_end|>\n<|im_start|>assistant\n\n"), text[-300:]) + self.assertIn("Reasoning effort is set to xhigh", text) # the top stays the default's + text = self.tok.decode(self.ids(self.CHAT, True, enable_thinking=False)) + self.assertTrue(text.endswith("<|im_start|>assistant\n\n\n\n\n")) + self.assertNotIn("<|im_start|>system\nReasoning effort", text[-200:]) + default = self.ids(self.CHAT, True) + for kw in ({"reasoning_effort": "low"}, {"reasoning_effort": "medium"}, {"enable_thinking": False}): + ids = self.ids(self.CHAT, True, **kw) + head = len(self.checkpoint(default, False)) # all before the answer's turn + self.assertEqual(ids[:head], default[:head], kw) # the same prompt up to the answer + + def test_changing_the_effort_keeps_the_conversation(self): + efforts = [{}, {"reasoning_effort": "low"}, {"enable_thinking": False}, {"reasoning_effort": "medium"}, {}] + history = len(self.checkpoint(self.ids(self.CHAT, False), False)) + # at the top (the default): another effort differs a few tokens in, and the whole prompt is read again + self.assertEqual(self.session([(self.CHAT, kw) for kw in efforts], False, False)[:2], [0, 0]) + # at the end, with the engine's rule: every request reuses the whole conversation + self.assertEqual(self.session([(self.CHAT, kw) for kw in efforts], True, True), [history] * 4) + + def test_the_next_turn_reuses_the_checkpoint(self): + turn1, turn2 = self.CHAT, self.CHAT + [self.REPLY, self.NEXT] + low = {"reasoning_effort": "low"} + first = len(self.checkpoint(self.ids(turn1, True, **low), True)) + # the engine's rule: the next turn (the same effort) reuses the first turn's whole conversation + self.assertEqual(self.session([(turn1, low), (turn2, low)], True, True), [first]) + # without it the checkpoint held the effort turn and the next turn reused nothing (what #458 measured) + self.assertEqual(self.session([(turn1, low), (turn2, low)], True, False), [0]) + + def test_the_config(self): + from serve.server import effort_end_args + + class Tok: + def encode(self, text, parse_special=False): + return [8678] if text == "system" else [1, 2] + + with tempfile.TemporaryDirectory() as d: + new, old = Path(d) / "new.exe", Path(d) / "old.exe" + new.write_bytes(b"... --tail-role-token ID --serve: ...") + old.write_bytes(b"... --turn-token ID ...") + out = io.StringIO() + with contextlib.redirect_stdout(out): + self.assertIsNone(effort_end_args({}, str(new), Tok())) + self.assertIsNone(effort_end_args({"effort_position": "start"}, str(new), Tok())) + self.assertEqual(effort_end_args({"effort_position": "end"}, str(new), Tok()), + ["--tail-role-token", "8678"]) + self.assertIsNone(effort_end_args({"effort_position": "end"}, str(old), Tok())) + self.assertIsNone(effort_end_args({"effort_position": "end"}, str(new), ByteTokenizer())) + self.assertIn("needs engine 0.1.39 or newer", out.getvalue()) + with self.assertRaises(ValueError): + effort_end_args({"effort_position": "middle"}, str(new), Tok()) + + class StatusNeedsTheKey(unittest.TestCase): """#212: /status shows the end of the answer being written, so it needs the key like /v1/*.""" @@ -499,6 +685,36 @@ def test_messages_as_a_json_string_over_http(self): self.assertEqual(status, 400, b) self.assertIn("messages must be a list of objects", b["error"]["message"]) + def test_malformed_tools_are_a_400(self): + # #592: a "tools" value that is not a list of named tool objects is a 400 naming the field, on both APIs, + # not a dropped connection (an AttributeError/TypeError in the request thread) + msgs = [{"role": "user", "content": "hi"}] + bad = ("auto", ["get_weather"], [{"description": "no name"}], {"name": "x"}, + [{"type": "function", "function": "get_weather"}], [{"type": "function", "function": {"name": ""}}]) + for path in ("/v1/chat/completions", "/v1/messages"): + for tools in bad: + with self.subTest(path=path, tools=tools): + status, b = self.post(path, {"model": "x", "max_tokens": 8, "messages": msgs, "tools": tools}) + self.assertEqual(status, 400, b) + self.assertIn("tools", b["error"]["message"]) + status, b = self.post("/v1/chat/completions", {"model": "x", "max_tokens": 8, "messages": msgs}) + self.assertEqual(status, 200, b) # the server goes on + + def test_well_formed_tools_still_work(self): + msgs = [{"role": "user", "content": "hi"}] + fn = {"name": "get_weather", "description": "the weather", "parameters": {"type": "object", "properties": {}}} + for path, tools in (("/v1/chat/completions", [{"type": "function", "function": fn}]), + ("/v1/chat/completions", [fn]), # the bare shape some clients send + ("/v1/chat/completions", json.dumps([{"type": "function", "function": fn}])), + ("/v1/chat/completions", []), ("/v1/chat/completions", None), + ("/v1/messages", [{"name": "get_weather", "input_schema": {"type": "object"}}]), + ("/v1/messages", [])): + with self.subTest(path=path, tools=tools): + status, b = self.post(path, {"model": "x", "max_tokens": 8, "messages": msgs, "tools": tools}) + self.assertEqual(status, 200, b) + if tools: + self.assertIn("get_weather", self.prompt_text()) + 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 @@ -805,6 +1021,26 @@ def test_drafts_in_history_and_totals(self): self.assertEqual((m["totals"]["drafts_offered"], m["totals"]["drafts_accepted"]), (17, 10)) +class PcieShare(unittest.TestCase): + """#588: the hit rate stays the VRAM share of the lookups; the routed experts the GPU read over PCIe (the DONE + line's 16th field, engine 0.1.39+) are given as their own share of all routed experts.""" + + def test_history(self): + tok = ByteTokenizer() + engine = DoneLineEngine(tok, "\n\nok", max_context=CTX, done_lines=[ + "DONE 4 20 40.0 30.0 stop 3 5 0 60 100 0 0 0.0 20 25", # 25 more over PCIe: 20% of 125 routed + "DONE 4 20 40.0 30.0 stop 3 5 0 60 100 0 0 0.0 20 0", + "DONE 4 20 40.0 30.0 stop 3 5 0 60 100 0 0 0.0 20"]) # an older engine + svc = Service(engine, tok, ChatTemplate(ROOT / "serve/chat_template.jinja")) + with contextlib.redirect_stdout(io.StringIO()) as out: + for _ in range(3): + list(svc.run(tok.encode("hi"), False, None, 10, {}, threading.Event())) + rows = list(svc.history) + self.assertEqual([r["hit_rate"] for r in rows], [0.6, 0.6, 0.6]) + self.assertEqual([r["pcie_share"] for r in rows], [0.2, 0.0, None]) + self.assertIn("expert cache 60.0% hit (+20.0% of the routed experts over PCIe)", out.getvalue()) + + 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.""" @@ -847,6 +1083,67 @@ def test_no_profile_in_the_args(self): self.assertEqual(engine_args(cfg), ["--native", "x", "--expert-profile-save", "learned.bin"]) +class RepeatStop(unittest.TestCase): + """#606: one token repeated repeat_stop_tokens times in a row ends the reply as "length"; 0 turns it off.""" + + def run_reply(self, script, limit=None): + tok = ByteTokenizer() + svc = Service(MockEngine(tok, script, max_context=CTX), tok, ChatTemplate(ROOT / "serve/chat_template.jinja")) + if limit is not None: + svc.repeat_stop_tokens = limit + ids = tok.encode("hi") + with contextlib.redirect_stdout(io.StringIO()) as out: + done = [x for kind, x in svc.run(ids, False, None, 3000, {}, threading.Event()) if kind == "done"][0] + return done, out.getvalue() + + def test_a_long_run_is_ended(self): + done, log = self.run_reply("ok " + "!" * 1000 + " never") + self.assertEqual(done["finish"], "length") + self.assertEqual(done["completion_tokens"], 3 + 256) + self.assertIn("repeated one token ('!') 256 times", log) + + def test_short_runs_and_off(self): + done, _ = self.run_reply("=" * 255 + " fine") + self.assertEqual(done["finish"], "stop") + done, log = self.run_reply("!" * 1000, limit=0) + self.assertEqual((done["finish"], done["completion_tokens"]), ("stop", 1001)) + self.assertNotIn("repeated one token", log) + done, _ = self.run_reply("ab" * 400, limit=8) # alternating tokens are not one run + self.assertEqual(done["finish"], "stop") + + +class LayerSplit(unittest.TestCase): + """#644: "layer_split" is the first layer of each later GPU; a list is accepted, counts per card are not.""" + + def cfg(self, split, gpus=(2, 0, 1, 3)): + c = {"args": ["--native", "x"], "gpu": list(gpus)} + if split is not ...: + c["layer_split"] = split + return c + + def test_auto_and_absent(self): + for v in (..., None, "", "auto", "AUTO"): + self.assertEqual(engine_args(self.cfg(v))[-2:], ["--layer-split", "auto"]) + + def test_string_and_list(self): + self.assertEqual(engine_args(self.cfg("24,36,42"))[-2:], ["--layer-split", "24,36,42"]) + self.assertEqual(engine_args(self.cfg(" 24, 36 ,42 "))[-2:], ["--layer-split", "24,36,42"]) + self.assertEqual(engine_args(self.cfg([24, 36, 42]))[-2:], ["--layer-split", "24,36,42"]) + self.assertEqual(engine_args(self.cfg(18, gpus=(0, 1)))[-2:], ["--layer-split", "18"]) + self.assertEqual(engine_args(self.cfg([18], gpus=(0, 1)))[-2:], ["--layer-split", "18"]) + + def test_counts_per_card_are_refused_with_the_format(self): + for bad in ("24,16,12,12", [24, 16, 12, 12], "24,16,12", "24,36", "x", [24.5, 30, 40], "1,20,30", [True]): + with self.assertRaises(ValueError) as e: + layer_split_value(self.cfg(bad)) + self.assertIn("first layer of each later GPU", str(e.exception)) + self.assertIn('"12,24,36"', str(e.exception)) # the example for 4 GPUs + + def test_one_gpu_ignores_it(self): + self.assertEqual(engine_args({"args": ["--native", "x"], "gpu": [0], "layer_split": "24,16"}), + ["--native", "x"]) + + 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.""" @@ -994,12 +1291,6 @@ 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_parking_fields(self): - e = object.__new__(StrataEngine) - StrataEngine._parse_done(e, "DONE 1 20 40.0 30.0 stop 3 5 7 9 10 11 12 13.0 14 4 123456789 2") - self.assertEqual((e.last["parked_conversations"], e.last["parked_bytes"], e.last["park_evictions"]), - (4, 123456789, 2)) - 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") @@ -1961,6 +2252,13 @@ def second(): break ''' +FAKE_STRATA_FAIL_ONCE = '''import pathlib, sys, time +fail = pathlib.Path(sys.argv[sys.argv.index("--fail") + 1]) +if fail.exists(): # this start fails before READY (as one next to a dying engine did) + fail.unlink() + sys.exit(1) +''' + FAKE_STRATA.split("\n", 1)[1] + class RestartWindow(unittest.TestCase): """#344: while the engine restarts it is not alive (a request waits for the restart instead of reading @@ -2012,6 +2310,54 @@ def test_not_alive_until_ready(self): gate.touch() eng.unload() + def test_restart_retries_a_start_that_fails(self): + """A dead engine's VRAM is freed only when its process is gone, so a new engine started at once can exit + before READY; restart() tries again (3 times) instead of leaving the server with max_context 0.""" + from unittest import mock + import serve.server as server + with tempfile.TemporaryDirectory() as d: + script, gate, fail = Path(d) / "fake_strata.py", Path(d) / "ready", Path(d) / "fail_once" + script.write_text(FAKE_STRATA_FAIL_ONCE, 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)), \ + mock.patch.object(StrataEngine, "RESTART_RETRY_S", 0.0): + gate.touch() + eng = StrataEngine("strata", ["--gate", str(gate), "--fail", str(fail)]) + try: + self.assertEqual(eng.max_context, 4096) + self.assertEqual(eng.known_ctx, 4096) + fail.touch() # the next start exits before READY, the one after works + eng.proc.kill() + eng.proc.wait(10) + eng.restart() + self.assertFalse(fail.exists(), "the failing start never ran") + self.assertTrue(eng.alive()) + self.assertEqual(eng.max_context, 4096) + self.assertFalse(eng.starting) + finally: + gate.touch() + eng.unload() + + def test_failed_restart_keeps_the_known_context(self): + """After a restart that failed, max_context is 0 but the engine is not starting: a request is checked against + the last known context and reaches run() (which starts the engine again), not a 400 about context 0.""" + tok = ByteTokenizer() + eng = MockEngine(tok, "\n\nok", max_context=0) + eng.known_ctx, eng.starting = 4096, False + svc = Service(eng, tok, ChatTemplate(ROOT / "serve/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", + data=json.dumps({"model": "m", "max_tokens": 16, + "messages": [{"role": "user", "content": "hi"}]}).encode(), + headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(req, timeout=30) as r: + self.assertEqual(r.status, 200) + finally: + httpd.shutdown() + def test_context_zero_is_503(self): tok = ByteTokenizer() svc = Service(MockEngine(tok, "\n\nok", max_context=0), tok, diff --git a/serve/test_vram.py b/serve/test_vram.py new file mode 100644 index 0000000000..e1abe0b6b6 --- /dev/null +++ b/serve/test_vram.py @@ -0,0 +1,136 @@ +"""#533: POST /v1/vram (opt-in hot VRAM resize) - the engine command, the route, the config key, a reserve kept for +an unloaded engine and applied when it loads again.""" +import io +import json +import queue +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, Service, StrataEngine, engine_args, serve +from serve.test_lifecycle import ResidentEngine + + +class VramEngine(ResidentEngine): + """The resident test engine with an elastic cache: VRAM n frees what the reserve asks for, None takes the + start's (700 MiB).""" + def __init__(self, tok, elastic=True): + super().__init__(tok) + self.elastic, self.calls = elastic, [] + + def vram(self, reserve_mib, timeout=120.0): + self.calls.append(reserve_mib) + if not self.elastic: + raise ValueError("VRAM needs an engine started with --vram-elastic (one NVIDIA GPU, --serve)") + r = 700 if reserve_mib is None else reserve_mib + out = {"reserve_mib": r, "expert_slots": max(100, 4000 - r), "expert_cache_mib": max(500, 6000 - r), + "expert_cache_full_mib": 6000, "vram_free_mib": r, "prompt_chunk": 4096} + self.info["vram"] = out + return out + + +class EngineCommand(unittest.TestCase): + def engine(self, replies): + e = StrataEngine("missing-executable", [], lazy=True) + e.proc = mock.Mock() + e.proc.poll.return_value = None + e.proc.stdin = io.StringIO() + e.ended = False + e.lines = queue.Queue() + for r in replies: + e.lines.put(r) + return e + + def test_the_line_and_the_answer(self): + e = self.engine(["strata serve: something else\n", + "VRAM reserve_mib=6000 expert_slots=1200 expert_slots_full=3700 expert_cache_mib=1536 " + "expert_cache_full_mib=4900 vram_free_mib=6100 prompt_chunk=2048\n"]) + out = e.vram(6000) + self.assertEqual(e.proc.stdin.getvalue(), "VRAM 6000\n") + self.assertEqual(out["expert_slots"], 1200) + self.assertEqual(out["prompt_chunk"], 2048) + self.assertEqual(e.info["expert_slots"], 1200) + e = self.engine(["VRAM reserve_mib=700 expert_slots=3700\n"]) + e.vram(None) + self.assertEqual(e.proc.stdin.getvalue(), "VRAM\n") + + def test_refused_and_died(self): + e = self.engine(["ERR VRAM needs an engine started with --vram-elastic\n"]) + with self.assertRaisesRegex(ValueError, "--vram-elastic"): + e.vram(100) + e = self.engine([None]) + with self.assertRaises(EngineDied): + e.vram(100) + + def test_config_key(self): + cfg = {"args": ["--max-context", "8192"]} + self.assertEqual(engine_args(cfg), ["--max-context", "8192"]) # off unless asked + self.assertEqual(engine_args({**cfg, "vram_elastic": True, "vram_segment_mib": 256}), + ["--max-context", "8192", "--vram-elastic", "--vram-segment-mib", "256"]) + self.assertEqual(engine_args({**cfg, "vram_elastic": "yes"}), ["--max-context", "8192"]) + + +class Route(unittest.TestCase): + def setUp(self): + tok = ByteTokenizer() + self.engine = VramEngine(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: + return response.status, json.loads(response.read().decode()) + + def test_resize_and_status(self): + self.engine.loaded = True + code, body = self.request("/v1/vram", {"reserve_mib": 4000}) + self.assertEqual(code, 200, body) + self.assertEqual(body["status"], "ok") + self.assertEqual(body["expert_cache_mib"], 2000) + self.assertEqual(self.request("/v1/status")[1]["vram"]["expert_cache_mib"], 2000) + code, body = self.request("/v1/vram", {"reserve_mib": None}) # back to the start's reserve + self.assertEqual(code, 200) + self.assertEqual(self.engine.calls, [4000, None]) + + def test_bad_input_busy_and_foreign_pages(self): + self.engine.loaded = True + for bad in ({"reserve_mib": -1}, {"reserve_mib": "8 GiB"}, {"reserve_mib": True}, {"reserve_mib": 1.5}): + self.assertEqual(self.request("/v1/vram", bad)[0], 400, bad) + self.assertEqual(self.request("/v1/vram", {"reserve_mib": 1}, {"Origin": "https://other.example"})[0], 403) + self.assertEqual(self.request("/v1/vram", {"reserve_mib": 1}, {"Content-Type": "text/plain"})[0], 415) + self.svc.vram_wait_s = 0.2 + with self.svc.fifo: # a request is running + self.assertEqual(self.request("/v1/vram", {"reserve_mib": 1})[0], 409) + self.assertEqual(self.engine.calls, []) + + def test_an_engine_without_the_flag_says_so(self): + self.engine.loaded, self.engine.elastic = True, False + code, body = self.request("/v1/vram", {"reserve_mib": 3000}) + self.assertEqual(code, 400) + self.assertIn("--vram-elastic", body["error"]["message"]) + + def test_unloaded_keeps_the_reserve_for_the_next_load(self): + code, body = self.request("/v1/vram", {"reserve_mib": 5000}) + self.assertEqual(code, 200) + self.assertEqual(body["status"], "not loaded") + self.assertEqual(self.engine.calls, []) + self.assertEqual(self.request("/v1/load", {})[0], 200) + self.assertEqual(self.engine.calls, [5000]) + + +if __name__ == "__main__": + unittest.main() diff --git a/serve/web/app.css b/serve/web/app.css index 4e8363d4a6..dd9a3e1fe3 100644 --- a/serve/web/app.css +++ b/serve/web/app.css @@ -115,7 +115,18 @@ main { flex: 1; min-height: 0; display: flex; } .table-wrap.all { max-height: 420px; overflow-y: auto; } .req-totals { margin: 0; font-size: var(--st-fs-sm); } .req-totals:empty { display: none; } +.cc-card { display: flex; flex-direction: column; gap: var(--st-s-3); } +.cc-bars { display: grid; grid-template-columns: 1fr 1fr; gap: var(--st-s-2) var(--st-s-6); } +.cc-bars[hidden] { display: none; } +.cc-note { margin: 0; } +.cc-note:empty { display: none; } .mcp-card { display: flex; flex-direction: column; gap: var(--st-s-3); } +.cfg-form { display: grid; grid-template-columns: minmax(180px, max-content) minmax(0, 1fr); gap: var(--st-s-3) var(--st-s-4); + align-items: center; } +.cfg-form label { font-size: var(--st-fs-sm); font-weight: var(--st-fw-medium); } +.cfg-form label code { font: 400 12px var(--st-font-mono); color: var(--st-ink-muted); display: block; } +.cfg-form .st-input { height: 38px; width: 100%; max-width: 360px; } +.cfg-actions { display: flex; align-items: center; gap: var(--st-s-3); } .mcp-server { display: flex; flex-direction: column; gap: var(--st-s-2); padding-top: var(--st-s-3); border-top: 1px solid var(--st-line-soft); } .mcp-server:first-child { padding-top: 0; border-top: 0; } .mcp-server__head { display: flex; align-items: center; gap: var(--st-s-2); flex-wrap: wrap; font-size: var(--st-fs-sm); } @@ -155,6 +166,9 @@ main { flex: 1; min-height: 0; display: flex; } @media (max-width: 1000px) { .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } .monitor-row { grid-template-columns: 1fr; } + .cc-bars { grid-template-columns: 1fr; } + .cfg-form { grid-template-columns: 1fr; gap: 4px; } + .cfg-form .st-input { margin-bottom: var(--st-s-2); } } @media (max-width: 640px) { .st-header { padding: 0 var(--st-s-3); gap: var(--st-s-2); } diff --git a/serve/web/app.js b/serve/web/app.js index 6e8fa6b293..ade19016ba 100644 --- a/serve/web/app.js +++ b/serve/web/app.js @@ -79,6 +79,7 @@ function showTab(name) { if (location.hash.slice(1) !== tab) history.replaceState(null, "", tab === "chat" ? location.pathname : `#${tab}`); if (tab === "chat") $("input").focus(); if (tab === "monitor") loadMcp(); + if (tab === "about") loadConfig(); if (lastMetrics) render(lastMetrics); } for (const b of document.querySelectorAll(".st-tab")) b.onclick = () => showTab(b.dataset.tab); @@ -190,9 +191,43 @@ function render(m) { } if (live.queued > 0) setPill("queued", `${live.queued} queued`); if (tab === "monitor") renderMonitor(live, hw, st, eng, h, last, m.requests || [], m.totals, m.requests_kept); + if (tab === "monitor") renderConvCache(m.conversation_cache); if (tab === "about") renderAbout(eng, hw, st); } +// #596: the conversation cache - the prompt's state the engine keeps between requests (always), and the whole +// conversations it parks in RAM when "--conversation-cache-mib N" is in the run config's args (opt-in) +function since(t) { + if (!t) return ""; + const s = Math.max(0, Date.now() / 1000 - t); + return s < 60 ? "just now" : s < 3600 ? `${fmt(s / 60)} min ago` : `${fmt(s / 3600, 1)} h ago`; +} +function renderConvCache(c) { + $("cc-card").hidden = !c; + if (!c) return; // an older server + const pct = (a, b) => (b ? `${Math.min(100, (100 * a) / b)}%` : "0%"); + $("cc-bars").hidden = !c.enabled; + if (c.enabled) { + $("cc-slots-text").textContent = `${fmt(c.parked)} / ${fmt(c.slots)}`; + $("cc-slots-bar").style.width = pct(c.parked, c.slots); + const budget = c.budget_mib * 1048576; + $("cc-mem-text").textContent = `${gb(c.bytes)} / ${gb(budget)} GB`; + $("cc-mem-bar").style.width = pct(c.bytes, budget); + } + $("cc-sum").textContent = c.requests ? `${fmt(c.requests_reused)} of ${fmt(c.requests)} requests reused part of their prompt` : ""; + const share = c.prompt_tokens ? ` (${fmt((100 * c.reused_tokens) / c.prompt_tokens)}% of all prompt tokens)` : ""; + const event = c.last_event ? `${c.last_event === "parked" ? "Parked" : "Restored"} ${fmt(c.last_tokens)} tokens, ${since(c.last_at)}` : null; + facts($("cc-facts"), [ + ["Last request", c.last_prompt != null ? `${fmt(c.last_reused || 0)} of ${fmt(c.last_prompt)} prompt tokens reused` : null], + ["Reused since start", c.requests ? `${fmt(c.reused_tokens)} tokens${share}` : null], + ["Parked / restored", c.enabled ? `${fmt(c.parks)} / ${fmt(c.restores)}${c.evictions ? ` · ${fmt(c.evictions)} evicted` : ""}` : null], + ["Last switch", c.enabled ? event : null], + ]); + $("cc-note").textContent = c.enabled + ? "A request that continues a parked conversation gets its state back instead of reading it again; the oldest goes when the slots or the memory are full." + : "The engine keeps the last conversation's state, so a follow-up reads only what is new. To keep several conversations (agents taking turns), add \"--conversation-cache-mib\", \"8192\" to the run config's args (docs/DETAILS.md)."; +} + function renderTotals(t) { if (!t || !t.requests) return ""; const since = new Date(t.since * 1000).toLocaleString([], {weekday: "short", hour: "2-digit", minute: "2-digit"}); @@ -302,7 +337,9 @@ function renderMonitor(live, hw, st, eng, h, last, requests, totals, kept) { const [cls, text] = badge[r.finish] || ["", r.finish || "–"]; const t = new Date(r.time * 1000).toLocaleTimeString([], {hour: "2-digit", minute: "2-digit", second: "2-digit"}); const proj = r.projection == null ? "" : ` ${r.projection ? "ESP" : "stock"}`; - const hit = r.hit_rate == null ? "–" : `${(r.hit_rate * 100).toFixed(1)}%`; + // #588: the VRAM share; the PCIe share (--pcie-frac) beside it when there is one + const hit = r.hit_rate == null ? "–" : `${(r.hit_rate * 100).toFixed(1)}%` + + (r.pcie_share ? ` +${(r.pcie_share * 100).toFixed(1)}% PCIe` : ""); return `${esc(t)}${esc(text)}${proj}${fmt(r.prompt_tokens)} ${fmt(r.reused)}${fmt(r.output_tokens)}${fmt(r.decode_tok_s, 1)} ${hit}${fmt(r.duration_s, 1)} s`; @@ -394,6 +431,62 @@ function renderMcp() { }).join(""); } +// ------------------------------------------------------------------ Model settings (GET / POST /config, #564) +// A few documented keys of the run config (strata-.json), for every client, from the next start on. The +// server lists them, checks every value and keeps every other key of the file as it is. +let cfgKeys = []; +async function loadConfig() { + let r; + try { r = await fetch("config", {headers: headers()}); } catch (e) { return; } + if (!r.ok) { $("cfg-card").hidden = true; return; } // no run config, an older server, or no key yet + const c = await r.json(); + cfgKeys = c.keys || []; + $("cfg-file").textContent = c.file || ""; + $("cfg-form").innerHTML = cfgKeys.map((k, i) => { + const id = `cfg-${i}`, v = k.value; + let input; + if (k.kind === "bool" || k.kind === "enum") { + const opts = k.kind === "bool" ? [["true", "on"], ["false", "off"]] : k.choices.map((x) => [x, x]); + const cur = v == null ? "" : String(v); + input = ``; + } else { + const text = v == null ? "" : Array.isArray(v) ? v.join(", ") : String(v); + input = ``; + } + return `${input}`; + }).join(""); + $("cfg-card").hidden = false; +} +function configValue(k, el) { + const s = el.value.trim(); + if (s === "") return null; + if (k.kind === "bool") return s === "true"; + if (k.kind === "number") return Number(s); + return s; // enum, or names (the server splits them at commas) +} +$("cfg-save").addEventListener("click", async () => { + const set = {}; + cfgKeys.forEach((k, i) => { + const v = configValue(k, $(`cfg-${i}`)); + const old = Array.isArray(k.value) ? k.value.join(", ") : k.value; + if (JSON.stringify(v) !== JSON.stringify(old ?? null)) set[k.key] = v; + }); + if (!Object.keys(set).length) { $("cfg-msg").textContent = "Nothing changed."; return; } + try { + const r = await fetch("config", {method: "POST", headers: headers(true), body: JSON.stringify({set})}); + const b = await r.json(); + if (!r.ok) throw new Error((b.error || {}).message || `HTTP ${r.status}`); + $("cfg-msg").textContent = b.changed.length + ? `Saved (${b.changed.join(", ")}); the earlier file is ${b.file}.bak. Start the model again to use it.` : "Nothing changed."; + loadConfig(); + } catch (e) { + $("cfg-msg").textContent = ""; + toast("error", "Not saved", String(e.message || e), 6000); + } +}); + // ------------------------------------------------------------------ Markdown (escaped first, then formatted) function inline(s) { const codes = []; diff --git a/serve/web/index.html b/serve/web/index.html index d858d2f08a..5c26ad8279 100644 --- a/serve/web/index.html +++ b/serve/web/index.html @@ -117,7 +117,7 @@
- +
TimeStatusPromptReusedOutputTok/sHit rateDuration
Tok/sVRAM hit rateDuration
No requests yet
@@ -125,6 +125,18 @@ +
+
Conversation cache
+ +
+

+
+ + diff --git a/setup.py b/setup.py index bc5cdd3002..80d1fba57c 100644 --- a/setup.py +++ b/setup.py @@ -28,8 +28,8 @@ --host 0.0.0.0 --api-key KEY (reach it from other devices on your network), --experimental-speed-projection on|off (EXPERIMENTAL, off by default), --models-dir DIR, --gguf-dir DIR (use GGUF files you already have), --build (compile instead of the ready-made -engine), --check (only check this PC), --resident-budget-gib N (UD-Q4_K_XL's experts in RAM), --kv-streaming -on|off|auto. +engine), --check (only check this PC), --resident-budget-gib N (UD-Q4_K_XL's or UD-IQ4_XS's experts in RAM), +--kv-streaming on|off|auto. Setup recommends, it never forces: the recommended answers are the defaults (--yes, or Enter), and a bigger choice than it recommends - a longer context, more GPUs, a bigger RAM budget, a size it thinks will not fit - is kept, with @@ -104,7 +104,18 @@ def hf_unpinned(url: str) -> str: # the CUDA libraries the ready-made engine loads (the same CUDA 13.0 it is built with), from NVIDIA's pip packages CUDA_WHEELS = ["nvidia-cublas==13.0.2.14", "nvidia-cuda-runtime==13.0.96"] MIN_DRIVER = 580 # CUDA 13.0 -MIN_ENGINE = (0, 1, 38) # v0.1.38: prompts faster (one gather per expert group #372, the first chunk's PLE rows beside layer 0 #374, DeltaNet three heads per thread #413), --kv q4_0 prompts on tensor cores (#452), Q5_0 experts on the GPU (#473), IQ4_XS on AVX-2 (#415), unbuffered expert loading on Windows (#357 #362), --peer-device (#531), a 6 GB card starts (#496), PR batch; v0.1.37: a silent engine is restarted (#481), Windows AMD counts the desktop's VRAM (#380 #377 #497), a steadier PCIe probe (#485), fixes #496 #495 #498 #505 #493; v0.1.36: a cancelled prompt logged as read so far (#471), the draft-head hint (#474), UPDATE.bat (#475), --expert-profile-save (#477); v0.1.35: Windows AMD uses its bundled HIP runtime (#468 #461), the low-RAM resident mode on Windows 32 GB (#467), fixes #460 #459 #446 #447 #457 #448 #444; v0.1.34: AMD on Windows (a ready-made HIP engine), an MCP server for AI assistants (tools/strata_mcp.py), a shorter README; v0.1.33: a portable image encoder again (#411 #412), setup recommends instead of forcing (#406 #403 #364 #384), fixes #352 #365 #369 #371 #375 #393 #408 #414; v0.1.32: split prompts faster (#340), AMD router +12%, Unsloth Q4 in setup, faster Q4 prompts, #326/#327/#342/#344 fixes, PR batch; v0.1.31: Unsloth UD-Q4_K_XL (experimental), GGUF-in-place low-RAM mode, Windows GGUF load 2x, server race + tokenizer fixes, AMD intrinsics; v0.1.30: short prompts faster (streaming from 1024 tokens), resident low-RAM variant, multi-GPU session carve, RDNA4; v0.1.29: sampled answers faster (split top-k), #154 correctness fixes; v0.1.28: the expert cache reserves the draft head, a cancelled request no longer fails the next; v0.1.27: RTX 20 (sm_75) in the ready-made engine, the HIP build without CUDA headers; v0.1.26: the draft layer's prompt pass in batches; v0.1.25: faster prompts (grouping off the copy engine, fused hyper-connection kernels), AMD HIP backend, --kv k8v4; v0.1.24: long prompts faster (QSA select on tensor cores); v0.1.23: image requests honor sampling, 8 GB cards start, batched verify window; v0.1.22: faster prompts (tensor-core attention), multi-GPU across images/steering/KV streaming; v0.1.21: multi-GPU layer split (--gpus); v0.1.20: system-prompt checkpoint, PCIe probe, hit rate; v0.1.19: penalties +# Older NVIDIA GPUs (experimental): CUDA 13 dropped Pascal (sm_60/61) and Volta (sm_70), so a model whose GPUs include +# one runs a second engine, built with CUDA 12.9 (-DSTRATA_EXPERIMENTAL_SM60=ON) and kept in its own folder: the +# ready-made one is CUDA12_ASSET (Windows; on Linux it is compiled here with a CUDA 12.x toolkit). One engine runs per +# model, so the choice is per model config, by its oldest GPU; --cuda 12|13 overrides it (docs/OLDER_GPUS.md). +CUDA13_MIN_ARCH = 75 # the oldest compute capability CUDA 13 compiles for (sm_75, RTX 20) +CUDA12_ASSET = "strata-windows-x64-cuda12.zip" if WIN else "strata-linux-x64-cuda12.zip" +CUDA12_WHEELS = ["nvidia-cublas-cu12==12.9.1.4", "nvidia-cuda-runtime-cu12==12.9.79"] +# CUDA 12.x minor-version compatibility (NVIDIA's table: Linux 525.60.13, Windows 527.41); the wheels match the 12.9.1 +# toolkit the CUDA 12 zip is built with (cuBLAS 12.9.1.4, runtime 12.9.79). Not tested on such an old driver here. +CUDA12_MIN_DRIVER = 528 if WIN else 525 +ENGINE12_DIR = "engine-cuda12" +MIN_ENGINE = (0, 1, 39) # v0.1.39: the #577 file-tier regression fixed, the OpenAI Responses API (#451, Codex), a reply stuck on one token ended (#606), the head before the arena (#620), effort_position (#458), --vram-reserve hot resize opt-in (#533), PR batch; v0.1.38: prompts faster (one gather per expert group #372, the first chunk's PLE rows beside layer 0 #374, DeltaNet three heads per thread #413), --kv q4_0 prompts on tensor cores (#452), Q5_0 experts on the GPU (#473), IQ4_XS on AVX-2 (#415), unbuffered expert loading on Windows (#357 #362), --peer-device (#531), a 6 GB card starts (#496), PR batch; v0.1.37: a silent engine is restarted (#481), Windows AMD counts the desktop's VRAM (#380 #377 #497), a steadier PCIe probe (#485), fixes #496 #495 #498 #505 #493; v0.1.36: a cancelled prompt logged as read so far (#471), the draft-head hint (#474), UPDATE.bat (#475), --expert-profile-save (#477); v0.1.35: Windows AMD uses its bundled HIP runtime (#468 #461), the low-RAM resident mode on Windows 32 GB (#467), fixes #460 #459 #446 #447 #457 #448 #444; v0.1.34: AMD on Windows (a ready-made HIP engine), an MCP server for AI assistants (tools/strata_mcp.py), a shorter README; v0.1.33: a portable image encoder again (#411 #412), setup recommends instead of forcing (#406 #403 #364 #384), fixes #352 #365 #369 #371 #375 #393 #408 #414; v0.1.32: split prompts faster (#340), AMD router +12%, Unsloth Q4 in setup, faster Q4 prompts, #326/#327/#342/#344 fixes, PR batch; v0.1.31: Unsloth UD-Q4_K_XL (experimental), GGUF-in-place low-RAM mode, Windows GGUF load 2x, server race + tokenizer fixes, AMD intrinsics; v0.1.30: short prompts faster (streaming from 1024 tokens), resident low-RAM variant, multi-GPU session carve, RDNA4; v0.1.29: sampled answers faster (split top-k), #154 correctness fixes; v0.1.28: the expert cache reserves the draft head, a cancelled request no longer fails the next; v0.1.27: RTX 20 (sm_75) in the ready-made engine, the HIP build without CUDA headers; v0.1.26: the draft layer's prompt pass in batches; v0.1.25: faster prompts (grouping off the copy engine, fused hyper-connection kernels), AMD HIP backend, --kv k8v4; v0.1.24: long prompts faster (QSA select on tensor cores); v0.1.23: image requests honor sampling, 8 GB cards start, batched verify window; v0.1.22: faster prompts (tensor-core attention), multi-GPU across images/steering/KV streaming; v0.1.21: multi-GPU layer split (--gpus); v0.1.20: system-prompt checkpoint, PCIe probe, hit rate; v0.1.19: penalties PY_PACKAGES = ["numpy", "jinja2", "regex", "pyyaml", "tqdm", "requests", "cmake", "ninja", "pillow", "psutil"] REQUIREMENTS = ROOT / "requirements.txt" # the same packages and their dependencies, pinned (#214) @@ -127,7 +138,17 @@ def hf_unpinned(url: str) -> str: # keeps a RAM budget of them (--resident-budget-gib, chosen below) and reads the rest from the GGUF on the SSD "UD-Q4_K_XL": {"about": "4-bit (Unsloth Dynamic), EXPERIMENTAL: the best quality, but most experts come from the " "SSD on a 64 GB PC (7-8.5 tokens/s measured)", "download_gb": 111.3, "ram_gb": 48, - "arena_gb": 77.0, "families": ("unsloth",), "budget": True}, + "arena_gb": 77.0, "families": ("unsloth",), "budget": True, "nvidia_only": True, + "experimental": True}, + # #621: Unsloth's UD-IQ4_XS - IQ3_S gate/up experts with IQ4_NL (43 layers) or Q8_0 (5) downs, the dense side as + # UD-Q4_K_XL's; three shards. A regular choice from 0.1.39 (no longer experimental). Its 59.5 GB of experts: a + # RAM budget of them, like UD-Q4_K_XL, but far fewer read from the SSD on a 64 GB PC and none from ~80 GB of RAM. + # Images: the vision path has no restriction for this pack (the same base model and image encoder), so setup asks + "UD-IQ4_XS": {"about": "~4-bit i-quant (Unsloth Dynamic), between IQ3_S and UD-Q4_K_XL in quality; on a PC with " + "less than ~80 GB of RAM part of its experts are read from the SSD", + "download_gb": 93.7, "ram_gb": 48, "arena_gb": 59.5, "families": ("unsloth",), "budget": True, + "shards": 3, "file": "Qwen3.8-Flash-Next-{q}-0000{i}-of-00003.gguf", "engine": (0, 1, 38), + "vision": True}, } # The experimental Unsloth file's four shards at the pinned revision: name -> (bytes, sha256), checked after the # download (setup trusts no other model file by name and size alone either: check_shards reads their directories). @@ -141,6 +162,15 @@ def hf_unpinned(url: str) -> str: "Qwen3.8-Flash-Next-UD-Q4_K_XL-00004-of-00004.gguf": (12087983520, "753bda48b98ba4f1636134a90a967de1b2d3908a236c026e464777342e53510a"), } +# #621: UD-IQ4_XS's three shards at the same revision (sizes and SHA-256: the Hub's LFS pointers) +UNSLOTH_IQ4_XS_SHARDS = { + "Qwen3.8-Flash-Next-UD-IQ4_XS-00001-of-00003.gguf": + (10946624, "5ce89370720f8bf90890f439361282104c1aa1482d4013bb9a50923e758e71a4"), + "Qwen3.8-Flash-Next-UD-IQ4_XS-00002-of-00003.gguf": + (49835229856, "577a38a2392b40ca2193cea502e1d92f60b8cd370675d308e0ec21885d9daaa7"), + "Qwen3.8-Flash-Next-UD-IQ4_XS-00003-of-00003.gguf": + (43836407744, "d4634e6d84f0ebb0940be15c90d3790bf6464e3dea3a1cddc567dc0e83ad8833"), +} UNSLOTH_ENGINE = (0, 1, 32) # the first engine setup configures for UD-Q4_K_XL (0.1.31 ran it by hand) UNSLOTH_RAM_LEFT_GB = 24 # RAM beside the budget: the OS, the engine, and the file cache the rest is read through # Contexts past 262144 (the model's trained length) extend it by rope scaling: for the context it will @@ -173,20 +203,24 @@ def hf_unpinned(url: str) -> str: "mmproj_hf": hf("ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF"), "mmproj": "mmproj-Qwen3.8-Flash-Next-BF16.gguf", "name": "qwen3.8-flash-next-coder", "profile": "expert-profile-coder.bin"}, - # EXPERIMENTAL: Unsloth's UD-Q4_K_XL of the original model (docs/UNSLOTH_Q4.md): four shards, no images yet - "unsloth": {"title": "Qwen3.8-Flash-Next (Unsloth)", "by": "Unsloth's 4-bit quantization (EXPERIMENTAL)", - "about": "4-bit, 111 GB download, most experts read from the SSD: slow (7-8.5 tokens/s on a 64 GB PC)", + # Unsloth's UD-IQ4_XS (three shards, #621; a regular choice from 0.1.39) and the EXPERIMENTAL UD-Q4_K_XL (four) + # of the original model (docs/UNSLOTH_Q4.md); "experimental" and "vision" are per model (MODELS) + "unsloth": {"title": "Qwen3.8-Flash-Next (Unsloth)", "by": "Unsloth's ~4-bit quantizations", + "about": "UD-IQ4_XS: a 94 GB download; with less than ~80 GB of RAM part of its experts are read from " + "the SSD (UD-Q4_K_XL, 111 GB: experimental)", "hf": hf("unsloth/Qwen3.8-Flash-Next-GGUF") + "{q}/", "file": "Qwen3.8-Flash-Next-{q}-0000{i}-of-00004.gguf", "shards": 4, "tag": "unsloth-", "mmproj_hf": hf("ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF"), "mmproj": "mmproj-Qwen3.8-Flash-Next-BF16.gguf", "name": "qwen3.8-flash-next-unsloth", - "experimental": True, "vision": False, "pack_args": ["--compat-bf16"], "sha256": UNSLOTH_SHARDS}, + "vision": False, "pack_args": ["--compat-bf16"], + "sha256": {**UNSLOTH_SHARDS, **UNSLOTH_IQ4_XS_SHARDS}}, } MMPROJ = "mmproj-Qwen3.8-Flash-Next-BF16.gguf" # EXPERIMENTAL, off by default (setup asks): a control vector shipped with the repository, see its README ESP_VECTOR = ROOT / "data" / "experimental-speed-projection" / "Qwen3.8-Flash-Next-experimental-speed-projection.gguf" # the image encoder on the GPU (~1.2 GB at 1024 image tokens) warms up before the engine starts, so the engine # sizes its expert slots around it and the default reserve (700 MiB) is enough; engines before 0.1.2 need more +VISION_GPU_SMALL_RESERVE_MIB = 1000 # the tip for images on a <= 12 GB card (the engine's LOW line asked ~1003) VISION = {"gpu": {"max_tokens": 1024, "reserve_mib": 700}, "cpu": {"max_tokens": 300, "reserve_mib": 700}} EXE = "strata.exe" if WIN else "strata" @@ -297,6 +331,74 @@ def page_file_gb(): return max(0.0, (m.ullTotalPageFile - m.ullTotalPhys) / 2**30) +def cpu_cores(): + """#642: (performance cores, efficiency cores) of a hybrid CPU (Intel 12th gen+, AMD Zen 5 + Zen 5c), counted + as the engine's pool counts them (detect_cpu_topology: physical cores, by Windows' EfficiencyClass or Linux's + cpu_capacity); None on a CPU whose cores are all alike, or when the OS does not say.""" + classes = [] # one entry per physical core: its efficiency/capacity class + try: + if WIN: + k32 = ctypes.windll.kernel32 + n = ctypes.c_ulong(0) + k32.GetLogicalProcessorInformationEx(0, None, ctypes.byref(n)) # RelationProcessorCore: the size + if not n.value: + return None + buf = ctypes.create_string_buffer(n.value) + if not k32.GetLogicalProcessorInformationEx(0, buf, ctypes.byref(n)): + return None + raw, at = buf.raw[:n.value], 0 + while at + 10 <= len(raw): # SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX: Relationship, Size, then + rel, size = struct.unpack_from(" int | None: + """#642 (Hardin22's measurement): on a hybrid CPU the expert pool runs best on the P-cores but the host loop's one + plus HALF of the E-cores - an E-core runs the expert kernels ~2.2x slower and each layer waits for its slowest + part (i9-14900KF, 8P + 16E: 15 workers decoded 165 / 116 tok/s against 106 / 84 with all 23). Only on a CPU with + more E-cores than P-cores: on an i7-13700KF (8P + 8E, docs/AMD_HIP.md's gfx1030 report) all 15 workers decoded + 38-42 tok/s against 36 with 8, so there the engine's own count stays. None: the engine's own default (one worker + per physical core but the host's) stays.""" + if not cores: + return None + p, e = cores + if e <= p: + return None + return max(1, p - 1 + e // 2) + + +def recommend_pool_workers(args: list) -> list: + """`args` with setup's recommended `--pool-workers` for a hybrid CPU, unless they set one already (a calibration's + measured count, or the user's own). A recommendation: the config line can be edited or removed.""" + n = hybrid_pool_workers(cpu_cores()) + if n is None or "--pool-workers" in args: + return args + p, e = cpu_cores() + ok(f"hybrid CPU ({p} performance + {e} efficiency cores): {n} CPU expert workers - the performance cores and half " + "of the efficiency cores (--pool-workers in the config; START-HERE --calibrate measures it on this PC)") + return [*args, "--pool-workers", str(n)] + + def cpu_info(): """(name, avx2, avx512): avx512 means everything Strata's fast AVX-512 kernels use (F, BW, VL, VNNI, VBMI), the same test the engine makes (cpu_avx512_ok), not just AVX-512F.""" @@ -320,6 +422,48 @@ def cpu_info(): return name, avx2, avx512 +def _cpuid_floor() -> str: + """Below AVX2 (Windows): "avx" when the CPU has AVX and the OS saves the YMM registers, "sse4.2" with SSE4.2 and + POPCNT, else "".""" + try: + regs = (ctypes.c_uint32 * 4)() + _run_stub(bytes([0x53, 0x49, 0x89, 0xC8, 0x89, 0xD0, 0x31, 0xC9, 0x0F, 0xA2, # push rbx; r8=rcx; eax=edx; ecx=0; cpuid + 0x41, 0x89, 0x00, 0x41, 0x89, 0x58, 0x04, 0x41, 0x89, 0x48, 0x08, # [r8]=eax, [r8+4]=ebx, [r8+8]=ecx + 0x41, 0x89, 0x50, 0x0C, 0x5B, 0xC3]), # [r8+12]=edx; pop rbx + ctypes.addressof(regs), 1) + ecx1 = regs[2] + if (ecx1 >> 27) & 1 and (ecx1 >> 28) & 1: # OSXSAVE, AVX + xcr0 = (ctypes.c_uint32 * 2)() + _run_stub(bytes([0x49, 0x89, 0xC8, 0x31, 0xC9, 0x0F, 0x01, 0xD0, # r8=rcx; ecx=0; xgetbv + 0x41, 0x89, 0x00, 0x41, 0x89, 0x50, 0x04, 0xC3]), ctypes.addressof(xcr0)) + if xcr0[0] & 6 == 6: + return "avx" + return "sse4.2" if (ecx1 >> 20) & 1 and (ecx1 >> 23) & 1 else "" + except Exception: + return "" + + +def cpu_floor(avx2: bool) -> str: + """The experimental older-CPU build this PC needs (#394 #595 #623): "" with AVX2 (the normal engine), "avx" (Sandy / + Ivy Bridge, AMD Bulldozer), "none" (SSE4.2 + POPCNT: Nehalem, Westmere), or "unsupported". STRATA_ISA_FLOOR=avx| + none asks for that build on any PC (testing it on a newer one).""" + forced = os.environ.get("STRATA_ISA_FLOOR", "").strip().lower() + if forced in ("avx", "none"): + return forced + if avx2: + return "" + if WIN: + f = _cpuid_floor() + else: + try: + txt = open("/proc/cpuinfo").read() + flags = set(re.search(r"^flags\s*:\s*(.*)$", txt, re.M).group(1).split()) + except (OSError, AttributeError): + flags = set() + f = "avx" if "avx" in flags else "sse4.2" if {"sse4_2", "popcnt"} <= flags else "" + return {"avx": "avx", "sse4.2": "none"}.get(f, "unsupported") + + def _cpuid_avx512_full() -> bool: """Windows has no feature bit for VNNI / VBMI: ask the CPU (CPUID leaf 7) through a tiny machine-code stub.""" try: @@ -410,22 +554,84 @@ def cc(g) -> str: return f"{g['arch'][:-1]}.{g['arch'][-1]}" +OLD_GPUS = None # why Pascal / Volta cards are admitted in this run (old_gpus_opt_in), None: they are not + + def experimental_sm60() -> bool: - """#295: STRATA_EXPERIMENTAL_SM60=1 admits Pascal (6.x) and Volta (7.0) cards: the community build - (-DSTRATA_EXPERIMENTAL_SM60=ON, compiled here with a CUDA 12.x toolkit), not the ready-made engine.""" - return os.environ.get("STRATA_EXPERIMENTAL_SM60", "").strip() == "1" + """#295: STRATA_EXPERIMENTAL_SM60=1 admits Pascal (6.x) and Volta (7.0) cards, run by the experimental CUDA 12 + engine (-DSTRATA_EXPERIMENTAL_SM60=ON). So does naming such a card (--gpu N / --gpus), --cuda 12, or a PC that + has no newer card (old_gpus_opt_in).""" + return os.environ.get("STRATA_EXPERIMENTAL_SM60", "").strip() == "1" or OLD_GPUS is not None def sm60_card(arch) -> bool: return 60 <= int(arch) <= 70 +def old_gpus_opt_in(found, named=(), cuda=None, other=False): + """Why this run may use Pascal / Volta cards (the experimental CUDA 12 engine), or None. The cards are an opt-in: + the user named one (`named`: --gpu / --gpus), asked for --cuda 12, set STRATA_EXPERIMENTAL_SM60=1, or the PC has + no card the ready-made engine runs on and no supported AMD card (`other`; it used to stop there). A PC with a + newer card keeps recommending it.""" + old = [g for g in found if sm60_card(g["arch"])] + if not old: + return None + if os.environ.get("STRATA_EXPERIMENTAL_SM60", "").strip() == "1": + return "STRATA_EXPERIMENTAL_SM60=1" + if str(cuda) == "12": + return "--cuda 12" + picked = [g for g in old if g["index"] in set(named)] + if picked: + return "you chose " + ", ".join(f"GPU {g['index']} ({g['name']})" for g in picked) + if not other and not any(int(g["arch"]) >= CUDA13_MIN_ARCH for g in found): + return "it is the only kind of NVIDIA GPU in this PC" + return None + + +def named_gpus(gpu, gpus) -> list: + """The card numbers --gpu / --gpus name (an unreadable value: none; parse_gpus says what is wrong later).""" + try: + if gpus and str(gpus).strip().lower() != "all": + return [int(x) for x in str(gpus).split(",") if x.strip()] + return [int(gpu)] if gpu is not None else [] + except ValueError: + return [] + + +def cuda_choice(archs, cuda=None): + """The CUDA toolkit of one model's engine: (12 or 13, why). 13 (the ready-made engine) unless a card is older + than CUDA 13 supports (Pascal / Volta: CUDA 13 cannot compile for them) - one engine runs per model, so its oldest + card decides. `cuda` (--cuda 12|13) overrides it; setup recommends, it does not refuse (the caller warns).""" + archs = sorted({int(x) for x in archs}) + old = [a for a in archs if a < CUDA13_MIN_ARCH] + if str(cuda) == "13": + return 13, ("--cuda 13 (as you chose)" + (f"; CUDA 13 has no code for sm_{old[0]}: the engine will not run " + "on that card" if old else "")) + if str(cuda) == "12": + return 12, "--cuda 12 (as you chose" + ("; RTX 50 (sm_120) engines built with CUDA 12.8 crashed on long " + "prompts, #220" if archs and archs[-1] >= 120 else "") + ")" + if old: + return 12, (f"sm_{old[0]} is older than CUDA 13 supports (it dropped Pascal and Volta): this model runs the " + "experimental CUDA 12 engine") + return 13, None + + +def engine_dir(toolkit=13) -> Path: + """The folder of the engine a model runs: engine/ (CUDA 13, or HIP), engine-cuda12/ (the experimental one).""" + return ROOT / (ENGINE12_DIR if int(toolkit) == 12 else "engine") + + +def config_toolkit(cfg: dict) -> int: + """12 when a model config runs the experimental CUDA 12 engine (its exe is in engine-cuda12/), else 13.""" + return 12 if cfg.get("cuda") == 12 or Path(str(cfg.get("exe", ""))).parent.name == ENGINE12_DIR else 13 + + def gpu_problem(g, together=False): """Why Strata cannot use this card, in plain words (None: it can).""" if int(g["arch"]) < 75 and not (sm60_card(g["arch"]) and experimental_sm60()): return (f"not supported - older than the RTX 20 series (compute capability {cc(g)}; Strata needs 7.5 or " - "newer" + ("; STRATA_EXPERIMENTAL_SM60=1 tries the community build for it" if sm60_card(g["arch"]) - else "") + ")") + "newer" + ("; experimental: choose it with --gpu " + str(g["index"]) + " (the CUDA 12 engine, " + "docs/OLDER_GPUS.md)" if sm60_card(g["arch"]) else "") + ")") if together and g["vram_gb"] < SPLIT_MIN_VRAM_GB - 0.5: return (f"not supported together with other GPUs - {g['vram_gb']:.0f} GB of VRAM (a card sharing the model " f"needs {SPLIT_MIN_VRAM_GB} GB or more)") @@ -515,9 +721,9 @@ def check_gpus(sel, found, what="", yes=False, named=False) -> None: fail(f"GPU {i}{'' if g is None else ' (' + g['name'] + ')'} {what}cannot be used: {p}", hint) -def engine_archs(): +def engine_archs(toolkit=13): """The GPU generations the installed engine has code for: (archs, ptx), or None when there is none.""" - info = ROOT / "engine" / "BUILD.json" + info = engine_dir(toolkit) / "BUILD.json" try: meta = json.loads(info.read_text()) except (OSError, ValueError): @@ -534,8 +740,8 @@ def engine_archs_hip(): return [str(x) for x in meta.get("archs", [])] if meta.get("backend") == "hip" else None -def engine_runs_on(g) -> bool: - ea = engine_archs() +def engine_runs_on(g, toolkit=13) -> bool: + ea = engine_archs(toolkit) if ea is None or not ea[0]: return True archs, ptx = ea @@ -608,6 +814,23 @@ def split_mmap(cfg: dict) -> bool: return True +def model_file(fam: dict, model: str, i: int) -> str: + """Shard i's file name: the family's pattern, or the model's own (#621: UD-IQ4_XS has three shards, not four).""" + return MODELS.get(model, {}).get("file", fam["file"]).format(q=model, i=i) + + +def model_shards(fam: dict, model: str) -> int: + return MODELS.get(model, {}).get("shards", fam.get("shards", 2)) + + +def budget_model(cfg: dict) -> str: + """The Unsloth model a config with a RAM budget runs, from its --native shard's name (UD-Q4_K_XL by default).""" + a = cfg.get("args", []) + native = Path(a[a.index("--native") + 1]).name.upper() if "--native" in a and a.index("--native") + 1 < len(a) \ + else "" + return next((m for m, d in MODELS.items() if d.get("budget") and f"-{m}-" in native), "UD-Q4_K_XL") + + def unsloth_split_need_gb(model="UD-Q4_K_XL") -> float: """#498: the RAM UD-Q4_K_XL needs on several GPUs, where it has no RAM budget (the engine refuses --resident-budget-gib with a layer split): its GGUF files and UNSLOTH_RAM_LEFT_GB more (~135 GB). Measured safe @@ -623,18 +846,42 @@ def split_budget(cfg: dict) -> bool: a = cfg.get("args", []) if "--resident-budget-gib" not in a: return False - need, ram = unsloth_split_need_gb(), ram_gb() + model = budget_model(cfg) + need, ram = unsloth_split_need_gb(model), ram_gb() if ram < need: - fail(f"UD-Q4_K_XL cannot share its RAM budget across GPUs (the engine has no layer split with it), and without " + fail(f"{model} cannot share its RAM budget across GPUs (the engine has no layer split with it), and without " f"the budget it needs ~{need:.0f} GB of RAM (its GGUF files and {UNSLOTH_RAM_LEFT_GB} GB more); this PC " f"has {ram:.0f} GB", "start it on one GPU: START-HERE.bat --gpu N (Linux: ./setup.sh --gpu N)") i = a.index("--resident-budget-gib") del a[i:i + 2] - ok("UD-Q4_K_XL on several GPUs: no RAM budget (the engine has none with a layer split) - all its experts are " + ok(f"{model} on several GPUs: no RAM budget (the engine has none with a layer split) - all its experts are " "loaded into RAM from the model files at start, and the files pass through the OS file cache (#498)") return True +REMOTE_EXPERT_OPT = "--remote-expert-opt" + + +def recommend_remote_expert_opt(cfg: dict, off: bool = False) -> None: + """0.1.39b (#578): a config on two or more GPUs gets --remote-expert-opt - the helper expert caches + (--expert-cache-device1..3) then stay complementary to the main GPU's, return their rows already weighted and skip + the CPU's activation quantization where no expert is left to it (dual RTX 4090: +63% mixed, +132% code over the + plain helper path). The engine uses it only with a helper cache; a layer split runs as before. A recommendation: + `off` (setup's --no-remote-expert-opt) or "remote_expert_opt": false in the config keeps it out, and a single-GPU + config is not touched.""" + if not isinstance(cfg.get("gpu"), list) or len(cfg["gpu"]) < 2: + return + args = cfg.setdefault("args", []) + if off or cfg.get("remote_expert_opt") is False: + if REMOTE_EXPERT_OPT in args: + args.remove(REMOTE_EXPERT_OPT) + return + if REMOTE_EXPERT_OPT not in args: + args.append(REMOTE_EXPERT_OPT) + ok("multi-GPU: --remote-expert-opt (helper expert caches complementary to the main GPU's, #578; " + "--no-remote-expert-opt leaves it out)") + + def offer_together(cfg_path: Path, cfg: dict, yes: bool) -> dict: """Starting a model set up for one card on a PC with two or more that can share it: asked once (the answer is saved in its config).""" @@ -646,7 +893,7 @@ def offer_together(cfg_path: Path, cfg: dict, yes: bool) -> dict: return cfg # #498: UD-Q4_K_XL's RAM budget has no layer split; without it the RAM must hold the GGUFs and 24 GB more budget = "--resident-budget-gib" in cfg.get("args", []) - if budget and ram_gb() < unsloth_split_need_gb(): + if budget and ram_gb() < unsloth_split_need_gb(budget_model(cfg)): return cfg pair = can[:2] cfg["gpus_asked"] = True @@ -660,13 +907,14 @@ def offer_together(cfg_path: Path, cfg: dict, yes: bool) -> dict: say(" use). On both, the experts the GPUs do not hold are read through the OS file cache instead: faster in") say(" two reports (#364, #384), but RAM can fill up to 0 free during long prompts.") if budget: - say(" This model (UD-Q4_K_XL) runs on one GPU with a RAM budget of its experts (recommended: the tested") + say(f" This model ({budget_model(cfg)}) runs on one GPU with a RAM budget of its experts (recommended: the tested") say(" setup). On both it has no budget: all its experts are loaded into RAM at start, which this PC's RAM") say(" holds - about twice as fast in #498 (2x RTX 3090: 31 -> 64-78 tokens/s).") short = split_short(pair) # #448: one card recommended (asked "n" by default), as for --resident for g in short: say(f" {split_short_note(g)}.") - missing = [g for g in pair if not engine_runs_on(g)] + tk = config_toolkit(cfg) + missing = [g for g in pair if not (engine_runs_on(g) if tk == 13 else engine_runs_on(g, tk))] if missing: say(" The installed engine has no code for " + ", ".join(g["name"] for g in missing) + ": to use them " "together, run START-HERE.bat --setup --gpus " + ",".join(str(g["index"]) for g in pair)) @@ -676,6 +924,7 @@ def offer_together(cfg_path: Path, cfg: dict, yes: bool) -> dict: cfg["layer_split"] = cfg.get("layer_split") or "auto" split_mmap(cfg) split_budget(cfg) + recommend_remote_expert_opt(cfg) ok("from now on this model runs on " + " + ".join(gpu_name(g) for g in pair)) else: ok("staying on one GPU (START-HERE.bat --gpus " + ",".join(str(g["index"]) for g in pair) + " switches)") @@ -700,10 +949,25 @@ def gpu_info(pick=None): def find_nvcc(below=None): - """The newest CUDA toolkit's nvcc and its (major, minor); with `below`, the newest older than that version.""" + """The newest CUDA toolkit's nvcc and its (major, minor); with `below`, the newest older than that version. + #601: STRATA_NVCC= is the only one considered (a newer toolkit beside it that cannot build on this + PC - CUDA 12.9 with glibc 2.43 - is not taken instead).""" + pick = os.environ.get("STRATA_NVCC") + if pick: + if not Path(pick).exists(): + warn(f"STRATA_NVCC={pick}: no such file; looking for a CUDA toolkit as usual") + else: + v = re.search(r"release (\d+)\.(\d+)", out([pick, "--version"])) + ver = (int(v.group(1)), int(v.group(2))) if v else None + if ver and below is not None and ver >= below: + warn(f"STRATA_NVCC={pick} is CUDA {ver[0]}.{ver[1]}; this build needs one older than " + f"{below[0]}.{below[1]}") + return (None, None) + return (pick, ver) if ver else (None, None) cands = [shutil.which("nvcc")] - if os.environ.get("CUDA_PATH"): - cands.append(str(Path(os.environ["CUDA_PATH"]) / "bin" / ("nvcc.exe" if WIN else "nvcc"))) + for var in ("CUDA_PATH", "CUDA_HOME"): # CUDA_HOME: Linux's usual name (#601) + if os.environ.get(var): + cands.append(str(Path(os.environ[var]) / "bin" / ("nvcc.exe" if WIN else "nvcc"))) if WIN: base = Path(r"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA") if base.exists(): @@ -854,7 +1118,7 @@ def gguf_dir_shards(folder: Path, fam: dict, model: str) -> list[Path]: engine and tools/iq_pack.py do. The published name first; else the one first shard in the folder whose name has the size in it (an upload split or named differently: -00001-of-00003, Unsloth's ...-00001-of-00004.gguf). A missing shard is check_shards' error later, as before.""" - first = folder / fam["file"].format(q=model, i=1) + first = folder / model_file(fam, model, 1) if not first.exists(): found = sorted(p for p in folder.glob("*-00001-of-*.gguf") if SHARD_NAME.search(p.name)) mine = [p for p in found if model.lower() in p.name.lower()] @@ -873,7 +1137,8 @@ def gguf_dir_shards(folder: Path, fam: dict, model: str) -> list[Path]: GGUF_QUANT = re.compile(r"(? tuple | None: the original's sizes: the size tells them apart).""" for f, d in FAMILIES.items(): for m in MODELS: - if f in MODELS[m].get("families", ("qwen", "swift")) and name == d["file"].format(q=m, i=1): + if f in MODELS[m].get("families", ("qwen", "swift")) and name == model_file(d, m, 1): return f, m return None @@ -1061,38 +1326,46 @@ def pip_install(packages, what): ok(f"{what} installed") -def cuda_lib_dirs(): - """Where pip put NVIDIA's CUDA libraries (nvidia/cu13/bin/x86_64 on Windows, nvidia/cu13/lib on Linux).""" - pattern = "cublas64_13.dll" if WIN else "libcublas.so.13*" +def cuda_lib_dirs(toolkit=13): + """Where pip put NVIDIA's CUDA libraries (nvidia/cu13/bin/x86_64 on Windows, nvidia/cu13/lib on Linux). + toolkit 12: the CUDA 12 wheels' cuBLAS and runtime, in two folders (nvidia/cublas/bin, nvidia/cuda_runtime/bin).""" + if int(toolkit) == 12: + patterns = ("cublas64_12.dll", "cudart64_12.dll") if WIN else ("libcublas.so.12*", "libcudart.so.12*") + else: + patterns = ("cublas64_13.dll",) if WIN else ("libcublas.so.13*",) dirs = [] for sp in {Path(p) for p in sys.path if p.endswith("site-packages")}: - for hit in (sp / "nvidia").rglob(pattern) if (sp / "nvidia").is_dir() else []: - if hit.parent not in dirs: - dirs.append(hit.parent) + for pattern in patterns: + for hit in (sp / "nvidia").rglob(pattern) if (sp / "nvidia").is_dir() else []: + if hit.parent not in dirs: + dirs.append(hit.parent) return [str(d) for d in dirs] # ------------------------------------------------------------------------------------------------ AMD # The RX 7900 XT / XTX (gfx1100) and the RX 9070 series / Radeon AI PRO R9700 (gfx1201) on Linux, through the HIP # backend (docs/AMD_HIP.md); the RX 7800 XT / 7700 XT (gfx1101, #254) and the RX 9060 XT (gfx1200, #256) were run by -# their owners; the RX 6800 / 6900 series (gfx1030, #311) runs but is unvalidated. There is no ready-made AMD engine: ROCm comes from AMD's TheRock Python wheels into .venv (no sudo; +# their owners; the RX 6800 / 6900 series (gfx1030, #311) and the RX 6700 XT (gfx1031, #524) run but are unvalidated. There is no ready-made AMD engine: ROCm comes from AMD's TheRock Python wheels into .venv (no sudo; # a system ROCm 7 in /opt/rocm is used when it has hipcc and hipBLAS) and the engine is compiled here for the cards. # No images yet. ROCM_INDEXES = {"gfx1100": "https://rocm.nightlies.amd.com/v2/gfx110X-dgpu/", # TheRock's wheels per GPU family "gfx1101": "https://rocm.nightlies.amd.com/v2/gfx110X-dgpu/", "gfx1200": "https://rocm.nightlies.amd.com/v2/gfx120X-all/", "gfx1201": "https://rocm.nightlies.amd.com/v2/gfx120X-all/", - "gfx1030": "https://rocm.nightlies.amd.com/v2/gfx103X-all/"} + "gfx1030": "https://rocm.nightlies.amd.com/v2/gfx103X-all/", + "gfx1031": "https://rocm.nightlies.amd.com/v2/gfx103X-all/"} ROCM_VERSION = os.environ.get("STRATA_ROCM_VERSION", "7.10.0a20251120") # what Strata's HIP build was tested with ROCM_SYSTEM_MIN = (7, 0) # an older system ROCm is passed over for the wheels (gfx1201 needs ROCm 6.4 or newer) -AMD_ARCHS = ("gfx1100", "gfx1101", "gfx1200", "gfx1201", "gfx1030") +AMD_ARCHS = ("gfx1100", "gfx1101", "gfx1200", "gfx1201", "gfx1030", "gfx1031") AMD_NAMES = {"gfx1100": "AMD Radeon RX 7900 series (gfx1100)", # when sysfs has no product name "gfx1101": "AMD Radeon RX 7800 XT / 7700 XT (gfx1101)", "gfx1200": "AMD Radeon RX 9060 series (gfx1200)", "gfx1201": "AMD Radeon RX 9070 series / AI PRO R9700 (gfx1201)", - "gfx1030": "AMD Radeon RX 6800 / 6900 series (gfx1030)"} + "gfx1030": "AMD Radeon RX 6800 / 6900 series (gfx1030)", + "gfx1031": "AMD Radeon RX 6700 XT series (gfx1031)"} AMD_CARDS = ("the RX 7900 XT / XTX (gfx1100), RX 7800 XT / 7700 XT (gfx1101), RX 9060 XT (gfx1200) and " - "RX 9070 / 9070 XT / Radeon AI PRO R9700 (gfx1201), and the RX 6800 / 6900 series (gfx1030, unvalidated)") + "RX 9070 / 9070 XT / Radeon AI PRO R9700 (gfx1201), and the RX 6800 / 6900 series (gfx1030) and RX 6700 XT " + "(gfx1031, #524), both unvalidated") def rocm_index(arch): @@ -1587,7 +1860,9 @@ def build_engine_hip(gpu, llama, vision="none") -> Path: src, vsrc = source_hash(ENGINE_SOURCES), source_hash(VISION_SOURCES) archs = sorted(set(gpu.get("archs") or [gpu["arch"]])) has_archs = set(archs) <= set(meta.get("archs", [])) - engine_ok = meta.get("backend") == "hip" and (eng / EXE).exists() and meta.get("src") == src and has_archs + floor = cpu_floor(cpu_info()[1]) # "" on an AVX2 CPU: the normal engine + engine_ok = meta.get("backend") == "hip" and (eng / EXE).exists() and meta.get("src") == src and has_archs and \ + (meta.get("isa_floor") or "") == floor vision_ok = vision == "none" or ((eng / VEXE).exists() and meta.get("vision_src") == vsrc) if engine_ok and vision_ok: ok("engine already built for this PC") @@ -1615,10 +1890,10 @@ def build_engine_hip(gpu, llama, vision="none") -> Path: f"-DCMAKE_HIP_COMPILER={root / 'llvm' / 'bin' / 'clang++'}", f"-DCMAKE_HIP_COMPILER_ROCM_ROOT={root}", "-DCMAKE_PREFIX_PATH=" + ";".join([str(root), *libs]), f"-DCMAKE_HIP_FLAGS=--rocm-path={root} --rocm-device-lib-path={bitcode}", - f"-DSTRATA_GGML_DIR={llama}"], None, "") + f"-DSTRATA_GGML_DIR={llama}", *isa_floor_defs(floor, ROOT / "build-hip", meta)], None, "") shutil.copy2(ROOT / "build-hip" / EXE, eng / EXE) meta = {"source": "local-hip", "backend": "hip", "version": source_version(), "archs": archs, "vision": "none", - "lib_dirs": dirs, "src": src} + "lib_dirs": dirs, "src": src, **({"isa_floor": floor} if floor else {})} if vision != "none": return build_vision_cpu(eng, stamp, meta, llama, vsrc) stamp.write_text(json.dumps(meta, indent=1)) @@ -1668,10 +1943,12 @@ def prebuilt_bases(url_base) -> list[str]: return [PREBUILT_TAG_URL.format(version=source_version()), base] -def get_prebuilt(url_base, gpu, vision, updating=False) -> Path | None: +def get_prebuilt(url_base, gpu, vision, updating=False, toolkit=13) -> Path | None: """The ready-made engine in engine/ (kept between runs), or None when there is none for this PC. - updating: called to replace an installed engine, which starts instead when this fails (no compile).""" - eng = ROOT / "engine" + updating: called to replace an installed engine, which starts instead when this fails (no compile). + toolkit 12: the experimental CUDA 12 engine (CUDA12_ASSET) in engine-cuda12/.""" + eng = engine_dir(toolkit) + asset = CUDA12_ASSET if int(toolkit) == 12 else PREBUILT_ASSET info = eng / "BUILD.json" if info.exists() and (eng / EXE).exists() and json.loads(info.read_text()).get("backend") != "hip": meta = json.loads(info.read_text()) @@ -1692,13 +1969,14 @@ def get_prebuilt(url_base, gpu, vision, updating=False) -> Path | None: info.unlink() if not url_base: return None - z = ROOT / "engine" / PREBUILT_ASSET + eng.mkdir(exist_ok=True) + z = eng / asset bases = prebuilt_bases(url_base) for i, base in enumerate(bases): if not base.startswith(("http://", "https://")): break try: # not published (yet), or no internet: compile instead - req = urllib.request.Request(base + PREBUILT_ASSET, method="HEAD", headers={"User-Agent": "strata-setup"}) + req = urllib.request.Request(base + asset, method="HEAD", headers={"User-Agent": "strata-setup"}) urllib.request.urlopen(req, timeout=60).close() break except OSError as e: @@ -1707,9 +1985,9 @@ def get_prebuilt(url_base, gpu, vision, updating=False) -> Path | None: continue warn(f"no ready-made engine at {base} ({e})" + ("" if updating else ": compiling instead")) return None - say(" Downloading the ready-made Strata engine ...") - download(base + PREBUILT_ASSET, z, "Strata engine") - tmp = ROOT / "engine" / "_unpack" + say(" Downloading the ready-made Strata engine" + (" (CUDA 12, experimental)" if int(toolkit) == 12 else "") + " ...") + download(base + asset, z, "Strata engine") + tmp = eng / "_unpack" shutil.rmtree(tmp, ignore_errors=True) with zipfile.ZipFile(z) as f: f.extractall(tmp) @@ -1752,11 +2030,17 @@ def get_prebuilt(url_base, gpu, vision, updating=False) -> Path | None: return eng -def update_installed_engine(url_base) -> None: +def update_installed_engine(url_base, toolkit=None) -> None: """An installed ready-made engine older than MIN_ENGINE is replaced before the model starts, so a plain START-HERE.bat on an existing install picks up a new release. If that cannot happen (no internet, the model - still running, no ready-made engine for this GPU) the installed engine is kept and starts as before.""" - eng = ROOT / "engine" + still running, no ready-made engine for this GPU) the installed engine is kept and starts as before. + toolkit None: engine/, then the experimental CUDA 12 engine in engine-cuda12/ when one is installed.""" + if toolkit is None: + update_installed_engine(url_base, 13) + if (engine_dir(12) / "BUILD.json").exists(): + update_installed_engine(url_base, 12) + return + eng = engine_dir(toolkit) info = eng / "BUILD.json" if not info.exists() or not (eng / EXE).exists(): return @@ -1814,14 +2098,19 @@ def update_installed_engine(url_base) -> None: if gpu is None: raise RuntimeError("no NVIDIA GPU found") gpu = {**gpu, "archs": sorted({int(gpu["arch"]), *(int(x) for x in meta.get("archs", []))})} - build_engine(gpu, vision, False, get_llama_cpp()) + if int(toolkit) == 12: # the cards it was compiled for (the main GPU may be newer) + gpu["archs"] = sorted({int(x) for x in meta.get("archs", [])}) or gpu["archs"] + build_engine(gpu, vision, False, get_llama_cpp(), toolkit=toolkit) except (Exception, SystemExit) as e: warn(f"could not compile the updated engine{'' if isinstance(e, SystemExit) else f' ({e})'}: starting the installed one") return new = None if gpu is not None: try: - new = get_prebuilt(url_base, gpu, "gpu", updating=True) + if int(toolkit) == 12: # the cards the CUDA 12 engine serves, not the newest one + gpu = {**gpu, "archs": [x for x in (int(a) for a in meta.get("archs", [])) if x < CUDA13_MIN_ARCH] + or [int(gpu["arch"])]} + new = get_prebuilt(url_base, gpu, "gpu", updating=True, toolkit=toolkit) except Exception as e: # a failed download must not stop the model from starting warn(f"updating the engine failed ({e})") if new is None: @@ -1829,25 +2118,36 @@ def update_installed_engine(url_base) -> None: info.write_text(meta_text) # get_prebuilt drops it before downloading: put it back warn(f"could not update the engine: starting the installed {meta.get('version')}") return - pip_install(CUDA_WHEELS, "NVIDIA CUDA libraries (cuBLAS, CUDA runtime; ~0.4 GB)") + pip_cuda_libs(toolkit) + + +def pip_cuda_libs(toolkit=13) -> None: + """NVIDIA's cuBLAS and CUDA runtime for a ready-made engine, from pip: CUDA 13's, or the CUDA 12 engine's.""" + if int(toolkit) == 12: + pip_install(CUDA12_WHEELS, "NVIDIA CUDA 12 libraries for the experimental engine (cuBLAS, CUDA runtime; ~0.7 GB)") + else: + pip_install(CUDA_WHEELS, "NVIDIA CUDA libraries (cuBLAS, CUDA runtime; ~0.4 GB)") def install_build_tools(gpu, yes): """The compiler and the CUDA toolkit, installed for the user (asks once). Returns (nvcc, vcvars).""" archs = [int(x) for x in gpu.get("archs", [gpu["arch"]])] - # #295: Pascal / Volta (STRATA_EXPERIMENTAL_SM60=1) need a CUDA 12.x toolkit - CUDA 13 cannot build sm_60/sm_70 - old = min(archs) < 75 + # #295: Pascal / Volta need a CUDA 12.x toolkit - CUDA 13 cannot build sm_60/sm_70; gpu["toolkit"] = 12: the + # experimental CUDA 12 engine for any cards (--cuda 12, docs/OLDER_GPUS.md) + old = int(gpu.get("toolkit") or (12 if min(archs) < CUDA13_MIN_ARCH else 13)) == 12 + need12 = (12, 8) if max(archs) >= 120 else (12, 0) # sm_120 needs CUDA 12.8 or newer if old and max(archs) >= 120: - fail("one engine cannot be built for both an RTX 50 card (CUDA 13) and a Pascal/Volta card (CUDA 12.x)", - "choose the cards of one kind with --gpu / --gpus") + warn("an RTX 50 card (sm_120) in a CUDA 12 engine: engines built with CUDA 12.8 crashed on long prompts there " + "(#220, #224); the RTX 50 card alone (--gpu N) runs the ready-made CUDA 13 engine") nvcc, cuda_v = find_nvcc(below=(13, 0)) if old else find_nvcc() - if old and (nvcc is None or cuda_v < (12, 0)): - fail("the experimental Pascal/Volta build (STRATA_EXPERIMENTAL_SM60=1) needs the NVIDIA CUDA Toolkit 12.x " - "(CUDA 13 cannot compile for these cards)", - "install CUDA 12.9 (or another 12.x; it can sit next to a newer one) from " - "https://developer.nvidia.com/cuda-toolkit-archive and run it again") + if old and (nvcc is None or cuda_v < need12): + fail("the experimental CUDA 12 engine (Pascal / Volta, or --cuda 12) is compiled here with the NVIDIA CUDA " + f"Toolkit {need12[0]}.{need12[1]} or a newer 12.x (CUDA 13 cannot compile for these cards)" + + (f"; found CUDA {cuda_v[0]}.{cuda_v[1]}" if nvcc else ""), + "install CUDA 12.9 (it can sit next to a newer one) from https://developer.nvidia.com/cuda-toolkit-archive " + "and run it again (STRATA_NVCC= picks one toolkit)") # RTX 50 (sm_120): CUDA 13.0 - an engine built with 12.8 crashed in the prompt path on Linux (#220) - need_cuda = (13, 0) if max(archs) >= 120 else (12, 0) + need_cuda = need12 if old else (13, 0) if max(archs) >= 120 else (12, 0) vcvars = find_vcvars() if WIN else None have_cc = vcvars is not None if WIN else shutil.which("g++") is not None missing = [] @@ -1943,9 +2243,18 @@ def source_hash(parts) -> str: return h.hexdigest()[:16] -def engine_defs(archs) -> list: - """Extra CMake definitions for the engine: the experimental Pascal/Volta build (#295) for cards below sm_75.""" - return ["-DSTRATA_EXPERIMENTAL_SM60=ON"] if min(int(x) for x in archs) < 75 else [] +def isa_floor_defs(floor: str, bdir: Path, meta: dict) -> list: + """The experimental older-CPU build's CMake definition (STRATA_ISA_FLOOR, CMakeLists.txt), none for the normal + build. A build folder configured for another floor is configured afresh: ggml's CPU options are cached there.""" + if (meta.get("isa_floor") or "") != floor and (bdir / "CMakeCache.txt").exists(): + (bdir / "CMakeCache.txt").unlink() + return [f"-DSTRATA_ISA_FLOOR={floor}"] if floor else [] + + +def engine_defs(archs, toolkit=13) -> list: + """Extra CMake definitions for the engine: the experimental Pascal/Volta build (#295) for cards below sm_75, and + for every CUDA 12 engine (the same build as the ready-made CUDA 12 one: it admits the older cards).""" + return ["-DSTRATA_EXPERIMENTAL_SM60=ON"] if min(int(x) for x in archs) < 75 or int(toolkit) == 12 else [] def prebuilt_vision(meta: dict, gpu: dict, vision: str) -> str: @@ -1962,10 +2271,15 @@ def prebuilt_vision(meta: dict, gpu: dict, vision: str) -> str: return vision -def build_engine(gpu, vision, yes, llama) -> Path: +def build_engine(gpu, vision, yes, llama, toolkit=None) -> Path: """Compile the engine (and, for images, the encoder) for this GPU; the results go to engine/. A compiled - engine whose source files changed since (a `git pull`) is compiled again: only the changed files, a few minutes.""" - eng = ROOT / "engine" + engine whose source files changed since (a `git pull`) is compiled again: only the changed files, a few minutes. + toolkit 12 (default: 12 for a card older than CUDA 13 supports): the experimental CUDA 12 engine, in + engine-cuda12/ with its own build folders.""" + if toolkit is None: + toolkit = 12 if min(int(x) for x in gpu.get("archs", [gpu["arch"]])) < CUDA13_MIN_ARCH else 13 + t12 = int(toolkit) == 12 + eng = engine_dir(toolkit) eng.mkdir(exist_ok=True) stamp = eng / "BUILD.json" meta = json.loads(stamp.read_text()) if stamp.exists() else {} @@ -1977,37 +2291,43 @@ def build_engine(gpu, vision, yes, llama) -> Path: # a card the engine has no code for (a GPU added with --gpus, #128) needs a compile even when the source is the # same; the compile keeps the generations it was built for new_arch = local and not set(archs) <= built - engine_ok = local and (eng / EXE).exists() and meta.get("src") == src and not new_arch + floor = cpu_floor(cpu_info()[1]) # "" on an AVX2 CPU: the normal engine + engine_ok = local and (eng / EXE).exists() and meta.get("src") == src and not new_arch and \ + (meta.get("isa_floor") or "") == floor vision_ok = not want_vision or ((eng / VEXE).exists() and (not local or meta.get("vision_src") == vsrc)) if engine_ok and vision_ok: ok("engine already built for this PC") return eng if local: archs = sorted(built | set(archs)) - nvcc, vcvars = install_build_tools({**gpu, "archs": archs}, yes) + nvcc, vcvars = install_build_tools({**gpu, "archs": archs, "toolkit": toolkit}, yes) cuda_archs = ";".join(str(x) for x in archs) + bdir, vdir = (ROOT / "build-cuda12", ROOT / "build-vision-cuda12") if t12 else (ROOT / "build", ROOT / "build-vision") if not engine_ok: say(" Compiling the engine for " + ", ".join(f"sm_{x}" for x in archs) + " (a card it had no code for; " "10-20 minutes, once) ..." if new_arch else " The engine's source changed: compiling it again (only what changed, a few minutes) ..." if local and (eng / EXE).exists() else " Compiling the Strata engine for your GPU (10-20 minutes, once) ...") - cmake_build(ROOT, ROOT / "build", "strata", + cmake_build(ROOT, bdir, "strata", ["-DSTRATA_ENABLE_CUDA=ON", "-DSTRATA_BUILD_TESTS=OFF", f"-DCMAKE_CUDA_ARCHITECTURES={cuda_archs}", - f"-DCMAKE_CUDA_COMPILER={nvcc}", f"-DSTRATA_GGML_DIR={llama}", *engine_defs(archs)], - vcvars, "build-strata.bat") - shutil.copy2(ROOT / "build" / EXE, eng / EXE) + f"-DCMAKE_CUDA_COMPILER={nvcc}", f"-DSTRATA_GGML_DIR={llama}", *engine_defs(archs, toolkit), + *isa_floor_defs(floor, bdir, meta)], + vcvars, "build-strata-cuda12.bat" if t12 else "build-strata.bat") + shutil.copy2(bdir / EXE, eng / EXE) if not vision_ok: say(" Compiling the image encoder" + (" with CUDA (10-20 minutes, once) ..." if vision == "gpu" else " ...")) defs = [f"-DLLAMA_DIR={llama}", f"-DSTRATA_VISION_CUDA={'ON' if vision == 'gpu' else 'OFF'}"] if vision == "gpu": defs += [f"-DCMAKE_CUDA_ARCHITECTURES={cuda_archs}", f"-DCMAKE_CUDA_COMPILER={nvcc}"] - cmake_build(ROOT / "tools" / "vision", ROOT / "build-vision", "strata-vision", defs, vcvars, "build-vision.bat") - shutil.copy2(ROOT / "build-vision" / "bin" / VEXE, eng / VEXE) + cmake_build(ROOT / "tools" / "vision", vdir, "strata-vision", defs, vcvars, + "build-vision-cuda12.bat" if t12 else "build-vision.bat") + shutil.copy2(vdir / "bin" / VEXE, eng / VEXE) bindir = Path(nvcc).parent # the toolkit's own libraries (bin, bin/x64, lib64) dirs = [str(d) for d in (bindir, bindir / "x64", bindir.parent / "lib64") if d.is_dir()] stamp.write_text(json.dumps({"source": "local", "version": source_version(), "archs": archs, - "vision": vision, - "cuda_dirs": dirs, "src": src, "vision_src": vsrc if want_vision else None}, indent=1)) + "vision": vision, **({"toolkit": 12} if t12 else {}), + "cuda_dirs": dirs, "src": src, "vision_src": vsrc if want_vision else None, + **({"isa_floor": floor} if floor else {})}, indent=1)) ok(f"engine compiled: {eng / EXE}") return eng @@ -2369,6 +2689,86 @@ def write_config(path: Path, cfg: dict): os.replace(tmp, path) +# #629: the run config's keys setup writes itself (and rewrites on every setup run); any other key is the user's - a +# "sampling" or "mcp_servers" block, "allowed_hosts", "cors_origins", "open_browser" - and is kept when setup runs again +SETUP_KEYS = frozenset({"exe", "args", "cwd", "tokenizer", "model_name", "log", "lib_dirs", "port", "backend", "env", + "gpu", "gpus_asked", "layer_split", "host", "api_key", "draft_vocab", "vision"}) +SETUP_ENV = frozenset({"STRATA_HIPBLASLT_TUNING", "STRATA_RESIDENT_PIN"}) # the "env" entries setup writes +SETUP_VISION = frozenset({"exe", "mmproj", "model", "gpu", "max_tokens", "threads"}) + + +def carry_over(old: dict, cfg: dict) -> list[str]: + """#629: setup run again for an installed model keeps what the user added to its run config: every key setup does + not write (`SETUP_KEYS`), the "env" entries setup does not write, and in "vision" the keys setup does not write + plus an mmproj of their own (a Q8_0 one, #625) that still exists. `cfg` (the new config) is updated in place; + the names of what was kept are returned. Engine options added by hand to "args" are not merged (setup chooses + those): `args_dropped` names them.""" + kept = [] + for k, v in old.items(): + if k not in SETUP_KEYS and k not in cfg: + cfg[k] = v + kept.append(k) + env = {k: v for k, v in (old.get("env") or {}).items() if k not in SETUP_ENV and k not in (cfg.get("env") or {})} \ + if isinstance(old.get("env"), dict) else {} + if env: + cfg["env"] = {**(cfg.get("env") or {}), **env} + kept += [f"env {k}" for k in env] + ov, nv = old.get("vision"), cfg.get("vision") + if isinstance(ov, dict) and isinstance(nv, dict): + for k, v in ov.items(): + if k not in SETUP_VISION and k not in nv: + nv[k] = v + kept.append(f"vision {k}") + mm = ov.get("mmproj") # a file of the user's own: not the one setup downloads + if isinstance(mm, str) and Path(mm).name != Path(str(nv.get("mmproj"))).name and Path(mm).is_file(): + nv["mmproj"] = mm + kept.append("vision mmproj") + return kept + + +def args_dropped(old: dict, cfg: dict) -> list[str]: + """#629: the engine options of the earlier run config that the new one has no more (by flag name): options added + by hand, which a setup run does not carry over - the start of the line that names them.""" + def flags(c): + a = c.get("args") if isinstance(c.get("args"), list) else [] + return [str(x) for x in a if str(x).startswith("--")] + new = set(flags(cfg)) + return list(dict.fromkeys(f for f in flags(old) if f not in new)) + + +def write_setup_config(cfg_path: Path, cfg: dict, source: Path | None = None) -> None: + """#629: setup's run config, written over an earlier one for the same model without losing what the user added + to it: the keys setup does not write are carried over (carry_over), and the earlier file is kept as + strata-.json.bak when it changes. `source`: an earlier install's config to carry the keys over from when + this folder has none yet (a copy set up like the last one). A line says what was kept, one what was not.""" + old_path = cfg_path if cfg_path.is_file() else source + old = None + if old_path is not None and old_path.is_file(): + try: + old = json.loads(old_path.read_text(encoding="utf-8-sig")) + except (OSError, ValueError): + pass + if not isinstance(old, dict): + old = None + kept = carry_over(old, cfg) if old is not None else [] + bak = None + if cfg_path.is_file() and old != cfg: + bak = cfg_path.with_name(cfg_path.name + ".bak") + try: + shutil.copyfile(cfg_path, bak) + except OSError as e: + warn(f"could not keep a copy of the earlier {cfg_path.name} ({e.strerror or e})") + bak = None + write_config(cfg_path, cfg) + if kept: + ok(f"kept from your earlier {old_path.name}: " + ", ".join(kept)) + if bak is not None: + dropped = args_dropped(old, cfg) if old is not None else [] + say(f" the earlier run config is kept as {bak.name}" + ( + f"; engine options it had that this one has not (setup chooses those): {' '.join(dropped)}" + if dropped else "")) + + def readable_config(path: Path) -> bool: """#459: a config that parses as a JSON object; any other gets a one-line warning naming it.""" text = None @@ -2415,7 +2815,7 @@ def choices_from_config(cfg_path: Path) -> dict: "vision": ("gpu" if vis.get("gpu") else "cpu") if isinstance(vis, dict) else "none", "esp": ("on" if Path(esp_path).name == ESP_VECTOR.name else esp_path) if esp_path else "off", "host": cfg.get("host"), "api_key": cfg.get("api_key"), "port": cfg.get("port"), "gpu": cfg.get("gpu"), - "layer_split": cfg.get("layer_split"), + "layer_split": cfg.get("layer_split"), "cuda": 12 if config_toolkit(cfg) == 12 else None, # #493: --vram-reserve-mib given at setup (images write the default 700 themselves) "vram_reserve_mib": int(val("--vram-reserve-mib")) if (val("--vram-reserve-mib") or "").isdigit() and ( vis is None or int(val("--vram-reserve-mib")) != VISION["gpu"]["reserve_mib"]) else None} @@ -2479,11 +2879,53 @@ def is_wsl() -> bool: return sys.platform.startswith("linux") and "microsoft" in platform.uname().release.lower() +def rotational_disk(path) -> str | None: + """#605 (Linux): the disk's name when `path` is on a rotational disk (sysfs queue/rotational), else None.""" + if WIN: + return None + try: + st = os.stat(path) + p = Path(f"/sys/dev/block/{os.major(st.st_dev)}:{os.minor(st.st_dev)}").resolve() + for q in (p, p.parent): # a partition has its disk's queue + f = q / "queue" / "rotational" + if f.exists(): + return q.name if f.read_text().strip() == "1" else None + except (OSError, ValueError, AttributeError): + pass + return None + + +def hip_config_cards(sel) -> list[dict]: + """#566: the AMD cards a HIP config runs on - its "gpu" (one index or a list) in HIP's numbering, as amd_gpus + lists them; no "gpu": the supported card with the most VRAM, as setup picks it. Each card's name carries its + architecture (an RX 7900 XTX and an RX 7900 XT are both gfx1100; a card without a product name in sysfs is known + by its arch only). A card that is not found is {} (the key then says "?").""" + amd = amd_gpus() + if sel is None: + usable = [g for g in amd if amd_problem(g) is None] + sel = max(usable, key=lambda x: (round(x["vram_gb"]), -x["index"]))["index"] if usable else None + byid = {g["index"]: g for g in amd} + cards = [] + for i in (sel if isinstance(sel, list) else [sel]): + g = byid.get(i) + if g is None: + cards.append({}) + continue + arch = g.get("arch") or "" + name = g.get("name") or "?" + cards.append({**g, "name": name if not arch or arch in name else f"{name} ({arch})"}) + return cards + + def hardware_key(cfg: dict) -> str: """What a calibration is valid for: this GPU, CPU and RAM, and the model with its context and images setting - (the context's KV cache and the image encoder take VRAM from the expert cache).""" + (the context's KV cache and the image encoder take VRAM from the expert cache). #566: a HIP config's cards are + AMD's (hip_config_cards) - nvidia-smi's list named them "?" (or another card with that number) before.""" sel = cfg.get("gpu") - gl = [gpu_info(i) or {} for i in sel] if isinstance(sel, list) else [gpu_info(sel) or {}] + if cfg.get("backend") == "hip": + gl = hip_config_cards(sel) + else: + gl = [gpu_info(i) or {} for i in sel] if isinstance(sel, list) else [gpu_info(sel) or {}] g = {"name": " + ".join(x.get("name", "?") for x in gl), "vram_gb": sum(x.get("vram_gb", 0) for x in gl)} a = cfg.get("args", []) ctx = a[a.index("--max-context") + 1] if "--max-context" in a else "?" @@ -2532,6 +2974,16 @@ def saved_calibration(cfg: dict) -> dict | None: return (load_settings().get("calibration") or {}).get(hardware_key(cfg)) +def setup_calibration(cfg: dict, hip: bool) -> dict | None: + """The calibration a (re-)install applies to its new config: the one saved for this PC and model. #566: on Linux + HIP too - hardware_key now names the AMD cards, so a `./setup.sh --calibrate` run is matched to its card and + model. Windows HIP keeps the defaults for now (not tried there). Setup still offers the tuning itself on NVIDIA + only: each control is verified on HIP first.""" + if hip and WIN: + return None + return saved_calibration(cfg) + + def upgrade_config(cfg_path: Path, cfg: dict) -> dict: """Configs written before v0.1.13 read prompts in fixed 2048-token chunks; the engine now picks the chunk itself (`--prefill auto`: up to 8192, as the free VRAM allows - about 2x faster on long prompts). Under WSL, @@ -2602,6 +3054,8 @@ def settings_summary(cfg: dict, port=None) -> str: srv = [f"{cfg.get('host', '127.0.0.1')}:{port or cfg.get('port', 8080)}"] if cfg.get("api_key"): srv.append("api key set") + if cfg.get("open_browser") is False: # #609 + srv.append("no browser") for k in ("gpu", "layer_split", "draft_vocab", "fit_max_tokens", "reasoning_budget_tokens", "anthropic_thinking"): if cfg.get(k) is not None: v = cfg[k] @@ -2630,8 +3084,9 @@ def start(cfg_path: Path, port: int | None, gpu: int | list | None = None, open_ if keep and any(cfg.get(k) != v for k, v in keep.items()): # #179: a --host/--api-key on a start was ignored cfg.update(keep) write_config(cfg_path, cfg) - ok("saved for this model: " + ", ".join("api key" if k == "api_key" else f"{k.replace('_', ' ')} {v}" - for k, v in keep.items())) + ok("saved for this model: " + ", ".join( + "api key" if k == "api_key" else ("the browser opens" if v else "no browser") if k == "open_browser" + else f"{k.replace('_', ' ')} {v}" for k, v in keep.items())) cfg_path.touch() # the most recently used model if "--mtp" in cfg["args"][:-1]: refresh_draft_vocab(Path(cfg["args"][cfg["args"].index("--mtp") + 1]), cfg.get("draft_vocab", "cjk")) @@ -2668,6 +3123,10 @@ def start(cfg_path: Path, port: int | None, gpu: int | list | None = None, open_ ok(f"GPU: {g['name']} ({g['vram_gb']:.0f} GB, AMD)") else: found = gpus() + global OLD_GPUS # Pascal / Volta: named on this start, or this model's CUDA 12 engine + OLD_GPUS = OLD_GPUS or old_gpus_opt_in(found, gpu if isinstance(gpu, list) else [gpu] if gpu is not None else + cfg.get("gpu") if isinstance(cfg.get("gpu"), list) else [cfg.get("gpu")], + 12 if config_toolkit(cfg) == 12 else None) if cfg.get("backend") == "hip": pass elif isinstance(gpu, list): # --gpus: saved, this model runs on these cards from now on @@ -2675,6 +3134,7 @@ def start(cfg_path: Path, port: int | None, gpu: int | list | None = None, open_ cfg["gpu"], cfg["gpus_asked"] = gpu, True cfg["layer_split"] = layer_split or cfg.get("layer_split") or "auto" split_budget(cfg) # #498: before it is saved (it stops when the RAM is short) + recommend_remote_expert_opt(cfg) write_config(cfg_path, cfg) gpu = None elif gpu is not None: # --gpu N: this start only, on that card @@ -2699,7 +3159,8 @@ def start(cfg_path: Path, port: int | None, gpu: int | list | None = None, open_ if g is not None: cfg = ensure_engine_for([g], cfg_path, cfg, yes) ok("GPU: " + gpu_name(g)) - if open_browser: + browser = open_browser and cfg.get("open_browser") is not False # #609: "open_browser": false, --no-browser + if browser: cmd.append("--open") gb = 0.0 if "--native" in cfg["args"]: @@ -2719,7 +3180,8 @@ def start(cfg_path: Path, port: int | None, gpu: int | list | None = None, open_ say(f" Starting {cfg.get('model_name', 'the model')}: it loads {size} into RAM and locks part of it for the " "GPU.") say(" While it does, YOUR PC CAN BE SLOW OR STOP RESPONDING FOR 1-3 MINUTES (longer the first time after a") - say(" restart). That is normal: please wait and don't close this window - the browser opens when it is ready.") + say(" restart). That is normal: please wait and don't close this window - " + ( + "the browser opens when it is ready." if browser else "the server says when it is ready.")) say(" Later, closing this window stops the model.") say(" " + "-" * 100) for n, line in enumerate(textwrap.wrap(f"Settings ({cfg_path.name}): {settings_summary(cfg, port)}", 100, @@ -2737,7 +3199,8 @@ def start(cfg_path: Path, port: int | None, gpu: int | list | None = None, open_ OLD_DRAFT_VOCABS = {"369151522226a5edaa5f12cfd1e2ae7db8f4fbdbd222f3dcf327dced9597fb25"} # to 0.1.26: 27 Han tokens -DRAFT_VOCABS = {"cjk": "draft_vocab.bin", "en": "draft_vocab_en.bin", "cyrillic": "draft_vocab_cyrillic.bin"} +DRAFT_VOCABS = {"cjk": "draft_vocab.bin", "en": "draft_vocab_en.bin", "cyrillic": "draft_vocab_cyrillic.bin", + "fr": "draft_vocab_fr.bin"} def saved_draft_vocab(cfg_path: Path) -> str | None: @@ -2750,7 +3213,32 @@ def saved_draft_vocab(cfg_path: Path) -> str | None: return v if v in DRAFT_VOCABS else None -DRAFT_VOCAB_MIB = {"cjk": 348, "cyrillic": 193, "en": 133} # the draft head's VRAM per subset (IQ3_S: the largest) +def vision_tokens(asked: int | None, vision: str, earlier: Path | None) -> int: + """#625: the most image tokens a picture becomes (the config's vision.max_tokens): --vision-tokens N, else what an + earlier config of this model chose for the same encoder device (a setup run again keeps it), else the device's + default (VISION). More is allowed with a note on the time it takes: setup recommends, it does not cap.""" + default = VISION[vision]["max_tokens"] + if asked is None and earlier is not None: + try: + v = json.loads(earlier.read_text(encoding="utf-8-sig")).get("vision") + except (OSError, ValueError, AttributeError): + v = None + mt = v.get("max_tokens") if isinstance(v, dict) and bool(v.get("gpu")) == (vision == "gpu") else None + if isinstance(mt, int) and mt > 0 and mt != default: + asked = mt + if asked is None: + return default + note = "" + if vision == "cpu" and asked > default: + note = (" - on the CPU a picture takes longer to encode the more tokens it gets (several seconds more at " + "1,024 than at 300)") + elif asked > VISION["gpu"]["max_tokens"]: + note = " - more than the encoder's default needs more VRAM and context per picture" + ok(f"images: up to {asked} image tokens per picture (--vision-tokens; default {default}){note}") + return asked + + +DRAFT_VOCAB_MIB = {"cjk": 348, "cyrillic": 193, "fr": 151, "en": 133} # the draft head's VRAM per subset (IQ3_S: the largest) SMALL_DRAFT_VRAM_GB = 14 # #474: below this the default subset's head can be what does not fit @@ -2792,6 +3280,60 @@ def small_card_note(ctx: int, draft_vocab: str | None) -> list[str]: return lines +PARALLEL_MAX = 8 # #465: the engine's batch window holds at most 8 requests +PARALLEL_SHARE = 0.2 # #465: the slots' sessions may take this share of the VRAM the expert cache would hold +PARALLEL_HELD = 0.5 # #465: ... and only where the cache still holds this share of the experts beside them +PARALLEL_COST_NOTE = ("parallel N reduces waiting for several users but costs about 10-25% speed per request on this " + "card") + + +def parallel_slot_gb(ctx: int, kv: str, streaming: bool) -> float: + """#465: the VRAM one batch slot's session takes: its KV cache (12 QSA layers; with KV streaming only the 32K + positions the attention reads stay in VRAM) and the DeltaNet state (~0.17 GB). Measured: 0.56 GiB at 32K int8.""" + kv_tok = 12 * (576 if kv == "q4_0" else 1056) + return (min(ctx, 32768) if streaming else ctx) * kv_tok / 1e9 + 0.17 + + +def parallel_recommend(vram_gbs, arena_gb: float, ctx: int, kv: str, streaming: bool) -> int: + """#465: how many requests at once ("parallel") to recommend: 0 = none (one at a time). Only where the experts + mostly fit in VRAM - the expert cache (each card's VRAM less ~5 GB, every card of a layer split) still holds + PARALLEL_HELD of the model's experts beside the slots' sessions, which take at most PARALLEL_SHARE of it, up to 4. + Where the experts mostly run on the CPU a batch reads about as many experts as the requests one by one and every + slot's VRAM is expert cache lost: measured on a 12 GB RTX 5070 (Q2_0, 32K), a request alone 11-24% slower with 2-4 + slots, 4 requests together 63 tok/s against 71 one after the other (docs/BATCHING.md).""" + if isinstance(vram_gbs, (int, float)): + vram_gbs = [vram_gbs] + cache_gb = sum(max(0.0, v - 5) for v in vram_gbs) + slot = parallel_slot_gb(ctx, kv, streaming) + best = 0 + for n in (2, 3, 4): + if n * slot <= PARALLEL_SHARE * cache_gb and (cache_gb - n * slot) >= PARALLEL_HELD * arena_gb: + best = n + return best + + +def parallel_note(asked: int | None, vram_gbs, arena_gb: float, ctx: int, kv: str, streaming: bool) -> list[str]: + """#465: what setup says about "parallel": the recommendation (or, where it would cost speed, why it is left at + one), or how the asked count compares with it (kept as asked: recommend, never force).""" + rec = parallel_recommend(vram_gbs, arena_gb, ctx, kv, streaming) + slot = parallel_slot_gb(ctx, kv, streaming) + if asked is None or asked <= 1: + if not rec: + return [f"Several requests at once: left at one at a time - {PARALLEL_COST_NOTE} (docs/BATCHING.md)."] + return [f"Several requests at once (opt-in): --parallel {rec} decodes up to {rec} together instead of one " + f"after the other (each takes ~{slot:.1f} GB of VRAM from the expert cache; docs/BATCHING.md)."] + lines = [f"parallel requests: {asked} at once (each takes ~{slot:.1f} GB of VRAM from the expert cache, " + f"{asked * slot:.1f} GB in all)"] + if asked > PARALLEL_MAX: + lines.append(f"the engine runs at most {PARALLEL_MAX} at once; it will use {PARALLEL_MAX}") + if not rec: + lines.append(f"recommended for this card: one at a time - {PARALLEL_COST_NOTE}; kept as you chose") + elif asked > rec: + lines.append(f"recommended for this card: {rec} - more slots leave fewer experts in VRAM, which can make every " + "request slower; kept as you chose") + return lines + + DESKTOP_RESERVE_MIB = 3072 # #560 #516: what kept a KDE/Wayland desktop alive beside a full expert cache @@ -2825,7 +3367,7 @@ def refresh_draft_vocab(rt: Path, choice: str = "cjk") -> None: """The draft layer's token subset in the MTP folder: `cjk` (data/draft_vocab.bin, since 0.1.27, #137), `en` (data/draft_vocab_en.bin, the English/code subset before it: ~110 MiB less VRAM, English answers 1-2% faster) or `cyrillic` (data/draft_vocab_cyrillic.bin: English/code and the whole Cyrillic script, for Ukrainian, Russian, - Bulgarian, Serbian... answers). + Bulgarian, Serbian... answers) or `fr` (data/draft_vocab_fr.bin: English/code and the tokens of French text, #597). Copied when missing or when a shipped subset other than the chosen one is there; a subset made by hand is kept.""" new, dst = ROOT / "data" / DRAFT_VOCABS.get(choice, "draft_vocab.bin"), rt / "draft_vocab.bin" if not new.exists() or not rt.is_dir(): @@ -2837,7 +3379,8 @@ def refresh_draft_vocab(rt: Path, choice: str = "cjk") -> None: if old not in shipped or old == hashlib.sha256(new.read_bytes()).hexdigest(): return ok("draft layer: the token subset " + {"cjk": "with Chinese, Japanese and Korean", - "cyrillic": "with the Cyrillic script"}.get(choice, + "cyrillic": "with the Cyrillic script", + "fr": "for French"}.get(choice, "for English and code (less VRAM)")) shutil.copyfile(new, dst) @@ -2845,28 +3388,72 @@ def refresh_draft_vocab(rt: Path, choice: str = "cjk") -> None: def ensure_engine_for(cards, cfg_path: Path, cfg: dict, yes: bool) -> dict: """The installed engine must have code for every card the model starts on: a card added later (--gpus with an older or newer generation, #128) or a new GPU in the PC otherwise stops the start with 'no kernel image'. Such a - card gets the engine compiled for all of them, before the start.""" - missing = [g for g in cards if not engine_runs_on(g)] + card gets the engine compiled for all of them, before the start. A Pascal / Volta card added to a model on the + CUDA 13 engine moves the model to the experimental CUDA 12 engine (CUDA 13 has no code for it).""" + tk = config_toolkit(cfg) + if tk == 13 and any(int(g["arch"]) < CUDA13_MIN_ARCH for g in cards): + return use_cuda12(cards, cfg_path, cfg, yes) + missing = [g for g in cards if not (engine_runs_on(g) if tk == 13 else engine_runs_on(g, tk))] if not missing: return cfg - info = ROOT / "engine" / "BUILD.json" + eng = engine_dir(tk) + info = eng / "BUILD.json" meta = json.loads(info.read_text()) say() say(" The installed engine has no code for " + ", ".join(f"{g['name']} (sm_{g['arch']})" for g in missing) + ": it is compiled for " + ("these cards" if len(cards) > 1 else "it") + " now.") main = gpu_info(cards[0]["index"]) archs = sorted({int(x) for x in meta.get("archs", [])} | {int(g["arch"]) for g in cards}) - vision = meta.get("vision") or ("gpu" if (ROOT / "engine" / VEXE).exists() else "none") - build_engine({**main, "archs": archs}, vision, yes, get_llama_cpp()) + vision = meta.get("vision") or ("gpu" if (eng / VEXE).exists() else "none") + build_engine({**main, "archs": archs}, vision, yes, get_llama_cpp(), toolkit=tk) dirs = json.loads(info.read_text()).get("cuda_dirs") or [] cfg["lib_dirs"] = dirs + [d for d in cfg.get("lib_dirs") or [] if d not in dirs] write_config(cfg_path, cfg) return cfg -def write_run_script(model, cfg_path, port): +def get_cuda12_engine(url_base, gpu, vision, yes, build=False) -> Path: + """The experimental CUDA 12 engine for these cards (gpu["archs"]): the ready-made one (Windows) with NVIDIA's + CUDA 12 libraries, or compiled here with a CUDA 12.x toolkit (Linux, --build, or no ready-made one).""" + eng = None if build else get_prebuilt(url_base, gpu, vision, toolkit=12) + if eng is not None and json.loads((eng / "BUILD.json").read_text()).get("source") != "local": + pip_cuda_libs(12) + if vision != "none" and not (eng / VEXE).exists(): + eng = None + return eng if eng is not None else build_engine(gpu, vision, yes, get_llama_cpp(), toolkit=12) + + +def engine_lib_dirs(eng: Path, toolkit=13) -> list: + """The library folders a CUDA engine loads from: its own (a compiled one: the toolkit's), else pip's wheels.""" + meta = json.loads((eng / "BUILD.json").read_text()) + return meta.get("lib_dirs") or meta.get("cuda_dirs") or cuda_lib_dirs(toolkit) + + +def use_cuda12(cards, cfg_path: Path, cfg: dict, yes: bool) -> dict: + """A model on the CUDA 13 engine now has a Pascal / Volta card (a --gpus at start): the model's config moves to + the experimental CUDA 12 engine (one engine per model; its other models keep theirs).""" + old = min(int(g["arch"]) for g in cards) + say() + warn(f"sm_{old} is older than CUDA 13 supports (it dropped Pascal and Volta): this model moves to the experimental " + "CUDA 12 engine (docs/OLDER_GPUS.md; START-HERE.bat --setup --cuda 13 and newer cards only moves it back)") + main = gpu_info(cards[0]["index"]) or cards[0] + vision = "gpu" if cfg.get("vision") else "none" + eng = get_cuda12_engine(os.environ.get("STRATA_PREBUILT_URL", PREBUILT_URL), + {**main, "archs": sorted({int(g["arch"]) for g in cards})}, vision, yes) + cfg["exe"] = str(eng / EXE) + cfg["cuda"] = 12 + cfg["lib_dirs"] = engine_lib_dirs(eng, 12) + if cfg.get("vision") and (eng / VEXE).exists(): + cfg["vision"]["exe"] = str(eng / VEXE) + write_config(cfg_path, cfg) + ok(f"engine: {eng / EXE} (CUDA 12, experimental)") + return cfg + + +def write_run_script(model, cfg_path, port, open_browser=True): + """run-.bat / .sh: the server with this config; `open_browser` False (#609: --no-browser) leaves --open out.""" serve = [sys.executable, str(ROOT / "serve" / "server.py"), "--engine", "strata", "--config", str(cfg_path), - "--port", str(port), "--open"] + "--port", str(port)] + (["--open"] if open_browser else []) if WIN: script = ROOT / f"run-{model.lower()}.bat" script.write_text("@echo off\r\ntitle Strata " + model + "\r\ncd /d \"" + str(ROOT) + "\"\r\n" + @@ -2917,6 +3504,32 @@ def resolve_rope(ctx: int, scaling, scale, trained: int = 262144): # ------------------------------------------------------------------------------------------------ main +def sycl_setup(argv) -> int: + """--backend sycl: the Intel Arc engine (the SYCL port in sycl/, PR #423), experimental. There is no ready-made + Intel engine: it is compiled from source on the PC (docs/INTEL_ARC.md), then sycl/setup_intel.py runs this setup + with the Intel steps swapped in. Nothing of the CUDA / HIP paths is used or changed.""" + say() + warn("Intel Arc (--backend sycl) is EXPERIMENTAL: a community port of the engine, not tested by the Strata " + "maintainers (no Intel card here). Expect rough edges; issues with your card and driver versions help.") + if WIN: + fail("the Intel Arc engine has no Windows setup yet (no ready-made Intel engine either)", + "run it on Linux (Ubuntu 24.04 with Intel's GPU driver and oneAPI): docs/INTEL_ARC.md") + say(" There is no ready-made Intel engine: it is built from source with Intel oneAPI (icpx + oneMKL),") + say(" docs/INTEL_ARC.md. Setup continues with sycl/setup_intel.py.") + rest, skip = [], False + for x in argv: # setup_intel.py drives this setup through its AMD path + if skip: + skip = False + elif x == "--backend": + skip = True + elif not x.startswith("--backend="): + rest.append(x) + script = ROOT / "sycl" / "setup_intel.py" + if not script.exists(): + fail(f"{script} is missing", "use a full Strata checkout (git clone) - docs/INTEL_ARC.md") + return subprocess.call([sys.executable, str(script), *rest]) + + def main() -> int: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--family", choices=list(FAMILIES), help="qwen = Qwen3.8-Flash-Next, swift = Swift 1.5") @@ -2934,6 +3547,10 @@ def main() -> int: "precise) or k8v4 (hybrid: INT8 K + 4-bit V, 816 B/cell)") ap.add_argument("--vision", choices=["yes", "no", "none", "gpu", "cpu"], help="let the model read images (yes = the encoder on the GPU)") + ap.add_argument("--vision-tokens", type=int, metavar="N", + help="the most image tokens a picture becomes (default 1024 with the encoder on the GPU, 300 on " + "the CPU): more reads small text and charts better, and takes longer to encode; remembered " + "for this model") ap.add_argument("--experimental-speed-projection", metavar="on|off|GGUF", help="EXPERIMENTAL, off by default: the control vector in data/experimental-speed-projection " "(or another GGUF) as a projection on layers 4-44; see docs/DETAILS.md") @@ -2943,11 +3560,19 @@ def main() -> int: ap.add_argument("--gpus", help="several GPUs sharing one model, as nvidia-smi numbers them (AMD: as setup lists " "them): \"0,2\", or \"all\" (every card that can); the first is the main one. " "Saved, also when starting (see docs/MULTI_GPU.md)") - ap.add_argument("--layer-split", help="with --gpus: where each later GPU's layers start (\"18\", \"16,32\"); " - "default auto, placed from each GPU's free VRAM") + ap.add_argument("--layer-split", help="with --gpus: where each later GPU's layers start (\"18\", \"16,32\"), one " + "rising number per GPU after the first - not layers per card; default " + "auto, placed from each GPU's free VRAM") + ap.add_argument("--no-remote-expert-opt", action="store_true", + help="with two or more GPUs: leave out --remote-expert-opt, which setup adds there (#578)") ap.add_argument("--host", help="where the server listens: 127.0.0.1 = this PC only (default), 0.0.0.0 = also other " "devices on your network (issue #26; set --api-key too)") ap.add_argument("--api-key", help="require this key from clients (recommended with --host 0.0.0.0)") + ap.add_argument("--no-browser", dest="browser", action="store_false", default=None, + help="do not open the chat page in the browser when the model is ready (for a harness or an app " + "that uses the API; remembered for this model, also in run-.bat/.sh)") + ap.add_argument("--browser", dest="browser", action="store_true", + help="open the chat page again when the model is ready (the default; undoes --no-browser)") ap.add_argument("--data-dir", help="where the model files go (~70-120 GB): default Strata-data next to this folder, " "remembered for every Strata folder on this PC") ap.add_argument("--models-dir", help="where the GGUF files go (default: /models)") @@ -2960,6 +3585,10 @@ def main() -> int: help="update the installed engine, Python packages and model settings as a start would, without " "starting the model (UPDATE.bat / update.sh run it after a git pull)") ap.add_argument("--build", action="store_true", help="compile the engine instead of using the ready-made one") + ap.add_argument("--cuda", choices=["12", "13", "auto"], default=os.environ.get("STRATA_CUDA") or None, + help="NVIDIA: the CUDA toolkit of this model's engine. auto (default): CUDA 13, the ready-made " + "engine; CUDA 12 (experimental) when a chosen card is older than CUDA 13 supports (Pascal, " + "Volta). 12 also runs with an older driver (Windows 528+, Linux 525+). docs/OLDER_GPUS.md") ap.add_argument("--prebuilt", default=os.environ.get("STRATA_PREBUILT_URL", PREBUILT_URL), help="where the ready-made engine is (a URL folder or a local folder)") ap.add_argument("--check", action="store_true", help="only check this PC and exit") @@ -2968,28 +3597,37 @@ def main() -> int: ap.add_argument("--draft-vocab", choices=list(DRAFT_VOCABS), help="the draft layer's tokens: cjk = with Chinese, Japanese and Korean (default), en = English " "and code only (~110 MiB less VRAM, English answers 1-2%% faster), cyrillic = English, code " - "and the Cyrillic script (Ukrainian, Russian... answers decode ~30%% faster)") + "and the Cyrillic script (Ukrainian, Russian... answers decode ~30%% faster), fr = English, " + "code and French (French answers: 18%% more drafts accepted)") ap.add_argument("--low-ram", choices=["auto", "on", "off", "resident", "mmap"], default="auto", help="read the model's experts from one file in its folder instead of copying them all into RAM " "(for a PC with a big GPU and little RAM); auto: when the experts would not fit the RAM. In " "this mode the experts the GPU does not hold are copied into RAM once when they fit (resident), " "else read through the OS file cache (mmap); resident / mmap force one of the two") ap.add_argument("--resident-budget-gib", type=float, metavar="N", - help="UD-Q4_K_XL: the GiB of its experts kept in RAM (default: the RAM less 24 GB, 40 on 64 GB; " + help="UD-Q4_K_XL, UD-IQ4_XS: the GiB of its experts kept in RAM (default: the RAM less 24 GB, 40 on 64 GB; " "more is kept as you choose, with a note)") ap.add_argument("--vram-reserve-mib", type=int, metavar="N", help="VRAM in MiB the engine leaves free for other programs (a game, another model; the engine's " "default: 700); the expert cache takes that much less") + ap.add_argument("--parallel", type=int, metavar="N", + help="up to N requests decode together (batch slots, opt-in; default: one at a time, the others " + "wait). Each slot takes VRAM from the expert cache; setup says what it recommends") ap.add_argument("--kv-streaming", choices=["auto", "on", "off"], default="auto", help="from a 64K context: keep the KV cache in RAM and only the attention's window in VRAM (more " "experts fit on the GPU); auto: when the RAM has room for it") - ap.add_argument("--backend", choices=["cuda", "hip"], + ap.add_argument("--backend", choices=["cuda", "hip", "sycl"], help="cuda = NVIDIA (default), hip = AMD RX 7900 / 7800 / 7700 XT, RX 9060 XT / 9070 / AI PRO R9700 on " - "Linux or Windows (chosen by itself when the PC has no NVIDIA card Strata can use)") + "Linux or Windows (chosen by itself when the PC has no NVIDIA card Strata can use), " + "sycl = Intel Arc, EXPERIMENTAL: Linux, built from source (docs/INTEL_ARC.md)") ap.add_argument("--skip-build", action="store_true", help=argparse.SUPPRESS) a = ap.parse_args() + if a.backend == "sycl": # Intel Arc: the SYCL port's own setup (sycl/setup_intel.py) + return sycl_setup(sys.argv[1:]) if a.resident_budget_gib is not None and not a.resident_budget_gib > 0: ap.error("--resident-budget-gib takes a number of GiB above 0, e.g. --resident-budget-gib 32") + if a.vision_tokens is not None and a.vision_tokens < 1: + ap.error("--vision-tokens takes a number of image tokens, 1 or more, e.g. --vision-tokens 768") if a.vram_reserve_mib is not None and a.vram_reserve_mib < 0: ap.error("--vram-reserve-mib takes a number of MiB, 0 or more, e.g. --vram-reserve-mib 2048") if a.gpu is not None: # --gpu 0,2 means --gpus 0,2 (a user tried it: issue report) @@ -3010,6 +3648,7 @@ def main() -> int: if a.update: # #475: UPDATE.bat / update.sh - never starts the model return update_install(have, a) explicit = a.setup or a.model or a.family or a.check or a.no_start + adopted = None # #629: the earlier install this copy is set up like if not have and not explicit: # a new copy of Strata (an update unzipped elsewhere): set it prev = previous_config(elsewhere, load_settings()) # up like the last one, from the files already here if prev is not None: @@ -3018,6 +3657,7 @@ def main() -> int: say(f" Found your earlier install in {prev.parent} ({prev.stem[len('strata-'):]}): setting up this " "copy the same way - the model files are reused, nothing big is downloaded.") a.family, a.model, a.context = ch["family"], ch["model"], a.context or ch["context"] + adopted = prev a.kv = a.kv or ch["kv"] a.vision = a.vision or ch["vision"] a.experimental_speed_projection = a.experimental_speed_projection or ch["esp"] @@ -3025,13 +3665,15 @@ def main() -> int: a.port = a.port or ch["port"] if a.vram_reserve_mib is None: # #493: an explicit reserve set up before a.vram_reserve_mib = ch.get("vram_reserve_mib") + if a.cuda is None and ch.get("cuda") == 12: # the experimental CUDA 12 engine, as before + a.cuda = "12" if isinstance(ch.get("gpu"), list): # a layer split: set up across the same cards again a.gpus = a.gpus or ",".join(str(g) for g in ch["gpu"]) a.layer_split = a.layer_split or ch.get("layer_split") else: a.gpu = a.gpu if a.gpu is not None else ch.get("gpu") a.yes = True - global GPU_PICK + global GPU_PICK, OLD_GPUS # starting an installed model: --gpus 0,2 (or all) saves those cards for it and starts on them (it used to start # on the first one alone unless given with --setup), --gpu N runs this start on one card; neither: the saved # choice, and asked once when the PC has cards that could share the model @@ -3052,14 +3694,14 @@ def main() -> int: + ("keeps" if a.no_start else "starts with") + " the default settings") return 0 if a.no_start else start(pick_cfg, a.port, run_gpu, yes=a.yes, layer_split=a.layer_split, keep={"host": a.host, "api_key": a.api_key, "draft_vocab": a.draft_vocab, - "vram_reserve_mib": a.vram_reserve_mib}) + "vram_reserve_mib": a.vram_reserve_mib, "open_browser": a.browser}) if have and not (a.setup or a.model or a.family or a.check or a.no_start): if not a.build: update_installed_engine(a.prebuilt) if len(have) == 1: return start(have[0], a.port, run_gpu, yes=a.yes, layer_split=a.layer_split, keep={"host": a.host, "api_key": a.api_key, "draft_vocab": a.draft_vocab, - "vram_reserve_mib": a.vram_reserve_mib}) + "vram_reserve_mib": a.vram_reserve_mib, "open_browser": a.browser}) say() for i, c in enumerate(have, 1): say(f" {i}) {json.loads(c.read_text(encoding='utf-8-sig')).get('model_name', c.stem)}") @@ -3068,14 +3710,20 @@ def main() -> int: if pick <= len(have): return start(have[pick - 1], a.port, run_gpu, yes=a.yes, layer_split=a.layer_split, keep={"host": a.host, "api_key": a.api_key, "draft_vocab": a.draft_vocab, - "vram_reserve_mib": a.vram_reserve_mib}) + "vram_reserve_mib": a.vram_reserve_mib, "open_browser": a.browser}) # ---- 1. the PC step(1, "checking your PC") found = gpus() amd = amd_gpus() - nv_ok = any(gpu_problem(g) is None for g in found) amd_ok = [g for g in amd if amd_problem(g) is None] + # older NVIDIA GPUs (Pascal / Volta): the experimental CUDA 12 engine, when chosen (docs/OLDER_GPUS.md) + OLD_GPUS = OLD_GPUS or old_gpus_opt_in(found, named_gpus(a.gpu, a.gpus), a.cuda, + other=bool(amd_ok) or a.backend == "hip") + if OLD_GPUS and a.backend != "hip": + warn(f"older NVIDIA GPUs (Pascal / Volta) can be used ({OLD_GPUS}): experimental, through a second engine " + "built with CUDA 12 (docs/OLDER_GPUS.md)") + nv_ok = any(gpu_problem(g) is None for g in found) hip = a.backend == "hip" or (a.backend is None and not nv_ok and bool(amd_ok)) if a.backend is None and nv_ok and amd_ok: # both kinds of card: asked (a first run on such a PC used to take NVIDIA without mentioning the Radeon) @@ -3089,6 +3737,7 @@ def main() -> int: hip = ask("Which cards?", ["1", "2"], "1", a.yes or a.check) == "2" if a.check and not hip: say(f" (the AMD card: {'START-HERE.bat' if WIN else './setup.sh'} --backend hip)") + cuda_tk = 13 # NVIDIA: the toolkit of this model's engine (cuda_choice) if hip: # AMD: compiled here; Windows: ready-made if WIN and a.gpus: fail("several AMD cards sharing one model (--gpus) is Linux-only for now", "use one card: --gpu N") @@ -3136,9 +3785,15 @@ def main() -> int: if multi: ok("GPUs: " + " + ".join(gpu_name(x) for x in chosen) + " together (the model's layers are split across them)") ok(f"GPU: {gpu['name']}, {gpu['vram_gb']:.1f} GB VRAM, compute capability {cc(gpu)}, driver {gpu['driver']}") - if driver_major(gpu) < MIN_DRIVER: - fail(f"the NVIDIA driver is too old ({gpu['driver']}; {MIN_DRIVER} or newer is needed)", - "update it with the NVIDIA App or from https://www.nvidia.com/drivers, restart, and run this again") + cuda_tk, why = cuda_choice(gpu["archs"], a.cuda) # one engine per model: its oldest card decides + if why: + (warn if cuda_tk == 13 or str(a.cuda) == "12" else ok)(f"CUDA {cuda_tk}: {why}") + min_driver = CUDA12_MIN_DRIVER if cuda_tk == 12 else MIN_DRIVER + if driver_major(gpu) < min_driver: + fail(f"the NVIDIA driver is too old ({gpu['driver']}; {min_driver} or newer is needed)", + "update it with the NVIDIA App or from https://www.nvidia.com/drivers, restart, and run this again" + + ("" if cuda_tk == 12 else f" (or --cuda 12: the experimental CUDA 12 engine runs with driver " + f"{CUDA12_MIN_DRIVER} or newer, docs/OLDER_GPUS.md)")) if gpu["vram_gb"] < 11: warn("less than 12 GB of VRAM: Strata will run, but most experts stay on the CPU and it will be slow") ram = ram_gb() @@ -3165,14 +3820,28 @@ def main() -> int: "the model may not start or may use less VRAM. Set it to \"System managed\": System > About > " "Advanced system settings > Performance > Advanced > Virtual memory") ok(f"CPU: {cpu} ({'AVX-512' if avx512 else 'AVX2' if avx2 else 'no AVX2'})") + floor = cpu_floor(avx2) + if floor == "unsupported": + fail("this CPU has neither AVX2 nor SSE4.2; Strata needs at least SSE4.2 (Intel Nehalem, 2008, or newer)") if not avx2: - fail("this CPU has no AVX2; Strata needs at least AVX2") + # #394 #595 #623: the ready-made engine is AVX2; an older CPU gets one compiled here, whose CPU experts run on + # ggml-cpu's kernels for this CPU. Experimental: measured only on newer CPUs with the older path forced, and by + # users on a few Xeons. A warning, not a stop. + warn(f"this CPU has no AVX2: Strata support for it is EXPERIMENTAL and slow. Setup compiles the engine on this " + f"PC for {'AVX' if floor == 'avx' else 'SSE4.2'} (STRATA_ISA_FLOOR={floor}; 10-20 minutes, once), and the " + "CPU's share of the experts runs on ggml-cpu's kernels, a few times slower than on an AVX2 CPU. " + "See \"Older CPUs\" in docs/INSTALL.md") + if hip and WIN: + fail("the older-CPU engine is compiled from source, and setup compiles the AMD engine on Linux only", + "use Linux for an AMD card on this CPU, or an NVIDIA card") + a.build = True if a.check: say() for m, d in MODELS.items(): verdict = "fits" if ram >= d["ram_gb"] else "tight" if ram >= d["ram_gb"] - 8 else "does not fit" if d.get("budget"): - verdict = (f"EXPERIMENTAL, fits with {resident_budget_gib(m, ram)} GiB of its experts in RAM, the rest " + verdict = (("EXPERIMENTAL, " if d.get("experimental") else "") + + f"fits with {resident_budget_gib(m, ram)} GiB of its experts in RAM, the rest " "read from the SSD" if ram >= d["ram_gb"] else "does not fit") if hip: # #429: not run on AMD yet (its prompt kernels are CUDA-only) verdict += " - NVIDIA only so far, untested on AMD" @@ -3200,6 +3869,7 @@ def main() -> int: say(f" Its license: {fam['license']}") say() names = [m for m in MODELS if family in MODELS[m].get("families", ("qwen", "swift"))] + names.sort(key=lambda m: bool(MODELS[m].get("experimental"))) # an experimental size last, never the default if a.model and a.model not in names: # #444: say which family has that size, and (with --gguf-dir) which files Strata can run at all elsewhere_fams = [f for f in FAMILIES if f in MODELS[a.model].get("families", ("qwen", "swift"))] @@ -3225,9 +3895,10 @@ def main() -> int: # Unsloth's UD-Q4_K_XL: a RAM budget of experts, the rest from the GGUF on the SSD - not the low-RAM mode (no # experts.bin: it would be another 77 GB on the disk), and one GPU (the budget mode has no layer split) unless # the RAM holds the GGUFs and 24 GB more: then several, without the budget, if asked for (#498) - warn(f"{model} is EXPERIMENTAL (docs/UNSLOTH_Q4.md): most of its experts are read from the SSD while it " - "answers, so it is several times slower than the 2-3-bit models; quality checked against llama.cpp") - if hip: + if MODELS[model].get("experimental"): + warn(f"{model} is EXPERIMENTAL (docs/UNSLOTH_Q4.md): most of its experts are read from the SSD while it " + "answers, so it is several times slower than the 2-3-bit models; quality checked against llama.cpp") + if hip and MODELS[model].get("nvidia_only"): # #429 (jkuepker): checked before the 111 GB download. The HIP engine has no prompt kernels for its # Q4_K / Q5_K experts (STRATA_MMQ_KQUANTS is CUDA-only) and it has not been run on AMD: asked, not refused confirm_risk(f"{model} has not been run on AMD cards yet: its prompt kernels are NVIDIA-only, so on " @@ -3252,7 +3923,8 @@ def main() -> int: if a.low_ram not in ("auto", "off"): warn(f"--low-ram {a.low_ram} does not apply to {model}: it always reads part of its experts from the files") elif a.resident_budget_gib is not None: - warn(f"--resident-budget-gib is for UD-Q4_K_XL: {model} keeps all of its experts in RAM or in the low-RAM mode") + warn(f"--resident-budget-gib is for UD-Q4_K_XL and UD-IQ4_XS: {model} keeps all of its experts in RAM or in " + "the low-RAM mode") low_ram = budget is None and (a.low_ram in ("on", "resident", "mmap") or (a.low_ram == "auto" and low_ram_needed(model, ram))) if low_ram and multi and not low_ram_together(a, model, ram, gpu, chosen): @@ -3321,7 +3993,7 @@ def main() -> int: kv = ["int8", "q4_0"][int(ask("KV cache?", ["1", "2"], "1", a.yes)) - 1] if ctx > 8192: ok(f"KV cache: {'8-bit' if kv == 'int8' else '4-bit (Hadamard-rotated)'}") - if fam.get("vision") is False: + if MODELS[model].get("vision", fam.get("vision")) is False: # UD-IQ4_XS: images, unlike UD-Q4_K_XL vision = "none" if a.vision not in (None, "no", "none"): warn(f"images are not available with {model} yet: off") @@ -3388,7 +4060,7 @@ def main() -> int: if family == "swift" else f"the experimental speed projection is not tested with {model}: left off") models_dir = Path(a.gguf_dir) if a.gguf_dir else Path(a.models_dir) / tag shards = gguf_dir_shards(models_dir, fam, model) if a.gguf_dir else \ - [models_dir / fam["file"].format(q=model, i=i) for i in range(1, fam.get("shards", 2) + 1)] + [models_dir / model_file(fam, model, i) for i in range(1, model_shards(fam, model) + 1)] problem = gguf_dir_problem(models_dir, shards[0], fam, model) if a.gguf_dir else None if problem: # #444: files Strata cannot run, or another choice's files fail(*problem) @@ -3433,24 +4105,26 @@ def main() -> int: gpu = hip_card(eng, gpu, amd) a.gpu = gpu["index"] if gpu["count"] > 1 else a.gpu else: - eng = None if a.build or hip else get_prebuilt(a.prebuilt, gpu, vision) + eng = None if a.build or hip else get_prebuilt(a.prebuilt, gpu, vision, **({"toolkit": 12} if cuda_tk == 12 + else {})) if eng is not None and not hip and json.loads((eng / "BUILD.json").read_text()).get("source") != "local": - pip_install(CUDA_WHEELS, "NVIDIA CUDA libraries (cuBLAS, CUDA runtime; ~0.4 GB)") + pip_cuda_libs(cuda_tk) if vision != "none" and not (eng / VEXE).exists(): warn("the ready-made engine has no image encoder: compiling it") eng = None else: vision = prebuilt_vision(json.loads((eng / "BUILD.json").read_text()), gpu, vision) if eng is None: - eng = build_engine_hip(gpu, llama, vision) if hip else build_engine(gpu, vision, a.yes, llama) + eng = build_engine_hip(gpu, llama, vision) if hip else build_engine(gpu, vision, a.yes, llama, toolkit=cuda_tk) meta = json.loads((eng / "BUILD.json").read_text()) if hip and WIN: # the ready-made engine's rocm/bin, first on the engine's PATH lib_dirs = [str(d) for d in hip_lib_dirs(eng)] else: - lib_dirs = meta.get("lib_dirs") or meta.get("cuda_dirs") or cuda_lib_dirs() + lib_dirs = meta.get("lib_dirs") or meta.get("cuda_dirs") or cuda_lib_dirs(cuda_tk) engine_ver = tuple(int(x) for x in str(meta.get("version", "0")).split(".")[:3] if x.isdigit()) - if budget is not None and engine_ver < UNSLOTH_ENGINE: # checked before the 111 GB download - fail(f"{model} needs engine {'.'.join(map(str, UNSLOTH_ENGINE))} or newer; this one is {meta.get('version')}", + need_engine = MODELS[model].get("engine", UNSLOTH_ENGINE) + if budget is not None and engine_ver < need_engine: # checked before the 94-111 GB download + fail(f"{model} needs engine {'.'.join(map(str, need_engine))} or newer; this one is {meta.get('version')}", "update Strata (or compile the engine with --build) and run setup again") ok(f"engine: {eng / EXE}") @@ -3480,7 +4154,7 @@ def main() -> int: pass download(fam["hf"].format(q=model) + s.name, s) check_shards(shards) - for s in shards: # the experimental Unsloth file: pinned sizes and SHA-256 + for s in shards: # the Unsloth files: pinned sizes and SHA-256 if s.name in fam.get("sha256", {}): verify_sha256(s, *fam["sha256"][s.name]) ok("model files present") @@ -3532,7 +4206,7 @@ def main() -> int: "--out", str(mtp / "mtp-q2_0.gguf")], env=env) run([sys.executable, str(ROOT / "tools" / "mtp_rt.py"), "--gguf", str(mtp / "mtp-q2_0.gguf"), "--out", str(rt)], env=env) - # a setup run again without --draft-vocab keeps the subset this model's config chose before (cyrillic, en) + # a setup run again without --draft-vocab keeps the subset this model's config chose before (cyrillic, fr, en) draft_vocab = a.draft_vocab or saved_draft_vocab(ROOT / f"strata-{tag.lower()}.json") refresh_draft_vocab(rt, draft_vocab or "cjk") ok(f"MTP draft layer: {rt}") @@ -3561,6 +4235,19 @@ def main() -> int: "the OS file cache (run setup again after the next engine update)") if low_ram: # the experts from the pack's experts.bin: the ones the GPU does not hold copied into RAM, or mapped args += ["--resident-experts" if resident else "--mmap-experts"] + disk = None if is_wsl() else rotational_disk(ple) # #605 (WSL's virtual disk says rotational) + if disk: + tensor = next((t for t in GGUFFile(ple).tensors if t.name == "per_layer_token_embd.weight"), None) + size = getattr(tensor, "expected_bytes", lambda: None)() + table_gb = size / 1e9 if size else 28.8 + if ram >= MODELS[model]["ram_gb"] + table_gb + 4: + args += ["--ple-io", "ram"] + ok(f"the model is on a rotational disk ({disk}): its {table_gb:.0f} GB n-gram table is kept in RAM " + "(--ple-io ram) - read from the disk at random, it can stall prompts for minutes (#605)") + else: + warn(f"the model is on a rotational disk ({disk}): its n-gram table is read from it at random, which can " + f"stall prompts for minutes (#605). An SSD is recommended; with ~{table_gb:.0f} GB more RAM, " + "--ple-io ram in the config's args keeps the table in RAM instead") # KV streaming: from 64K up the whole KV cache lives in RAM and only the part the attention reads (32K positions # per layer) stays in VRAM; the VRAM it frees holds more experts (+6% at 128K, +23% at 262K with Q2_0). It # costs ~13.7 KB of RAM per context token with 8-bit KV (1.7 GB at 128K), 7.5 KB with 4-bit, so only when it fits. @@ -3597,12 +4284,20 @@ def main() -> int: elif budget is not None and budget > resident_budget_gib(model, ram, kv_ram_gb): warn(f"the KV cache's {kv_ram_gb:.1f} GB of RAM come on top of your {budget:g} GiB RAM budget (setup " f"would take them out of it: {resident_budget_gib(model, ram, kv_ram_gb)} GiB); kept as you chose") + elif ctx >= 65536: # #620: say why, so a regenerated config that lost --kv-resident is not a surprise + ok(f"KV streaming off: it needs ~{kv_ram_gb:.1f} GB of RAM beside the ~{MODELS[model]['ram_gb']} GB {model} " + f"uses, and this PC has {ram:.0f}; the KV cache stays in VRAM (fewer cached experts). --kv-streaming on " + "turns it on anyway") elif a.kv_streaming == "on": warn("--kv-streaming on: a context under 64K is not streamed (the attention's window holds all of it): off") if budget is not None and not q4_split: # UD-Q4_K_XL: the experts read from the GGUF in place, the most-used N args += ["--resident-budget-gib", f"{budget:g}"] # GiB kept in RAM (#498: a layer split has no budget) if vision != "none": args += ["--vision", "--vram-reserve-mib", str(VISION[vision]["reserve_mib"])] + if vision == "gpu" and a.vram_reserve_mib is None and 0 < gpu.get("vram_gb", 0.0) <= 12.5: + # a tip only (recommend, never force): on a 12 GB card the encoder's 700 MiB can leave ~200 MiB free + print(f" tip: images on a {gpu['vram_gb']:.0f} GB card can leave little VRAM free; if a request stalls, " + f"run setup again with --vram-reserve-mib {VISION_GPU_SMALL_RESERVE_MIB}") if a.vram_reserve_mib is not None: # #493: VRAM left free for other programs (only when given) if "--vram-reserve-mib" in args: i = args.index("--vram-reserve-mib") + 1 @@ -3629,6 +4324,8 @@ def main() -> int: cfg = {"exe": str(eng / EXE), "args": args, "cwd": str(ROOT), "tokenizer": str(pack / "tokenizer"), "model_name": f"{fam['name']}-{model.lower()}", "log": str(ROOT / f"strata-{tag.lower()}.log"), "lib_dirs": lib_dirs, "port": port} + if cuda_tk == 12: # the experimental CUDA 12 engine (engine-cuda12/) + cfg["cuda"] = 12 if hip: cfg["backend"] = "hip" # the dense prompt GEMMs through hipBLASLt with kernels measured on this GPU generation (tools/hip; +40-60% @@ -3646,26 +4343,46 @@ def main() -> int: cfg["gpu"] = multi cfg["layer_split"] = a.layer_split or "auto" ok(f"layer split across GPUs {multi} ({cfg['layer_split']})") + recommend_remote_expert_opt(cfg, off=a.no_remote_expert_opt) if a.host: cfg["host"] = a.host if a.api_key: cfg["api_key"] = a.api_key if draft_vocab: cfg["draft_vocab"] = draft_vocab + if a.browser is not None: # #609: only when given (else an earlier choice is carried over) + cfg["open_browser"] = a.browser + # #465: requests at once - written only when given (else an earlier "parallel" is carried over); a recommendation + streaming = "--kv-resident" in args + if a.parallel is not None: + if a.parallel >= 2: + cfg["parallel"] = a.parallel + for i, line in enumerate(parallel_note(a.parallel, [g.get("vram_gb", 0.0) for g in chosen], + MODELS[model]["arena_gb"], ctx, kv, streaming)): + (ok if i == 0 else warn)(line) + else: + cfg["parallel"] = 1 + ok("parallel requests: one at a time (--parallel 1)") if vision != "none": + old_cfg = ROOT / f"strata-{tag.lower()}.json" + vt = vision_tokens(a.vision_tokens, vision, old_cfg if old_cfg.is_file() else adopted) cfg["vision"] = {"exe": str(eng / VEXE), "mmproj": str(mmproj), "model": str(shards[0]), - "gpu": vision == "gpu", "max_tokens": VISION[vision]["max_tokens"]} + "gpu": vision == "gpu", "max_tokens": vt} if vision == "cpu": cfg["vision"]["threads"] = max(1, (os.cpu_count() or 8) // 2) + elif a.vision_tokens is not None: + warn("--vision-tokens: images are off for this model, so it is not used") cfg_path = ROOT / f"strata-{tag.lower()}.json" - cal = None if hip else saved_calibration(cfg) # tools/calibrate.py is NVIDIA-only for now + cal = setup_calibration(cfg, hip) # #566: Linux HIP too; the tuning is offered on NVIDIA only if cal is not None: sys.path.insert(0, str(ROOT / "tools")) import calibrate as CAL cfg["args"] = CAL.apply(cfg["args"], cal.get("settings") or {}) ok("the settings tuned for this PC earlier are used" + (f" ({cal['date']})" if cal.get("date") else "")) - write_config(cfg_path, cfg) - script = write_run_script(tag, cfg_path, port) + else: # #642: measured counts (a calibration) win over the rule + cfg["args"] = recommend_pool_workers(cfg["args"]) + write_setup_config(cfg_path, cfg, adopted if adopted is not None and adopted.name == cfg_path.name else None) + script = write_run_script(tag, cfg_path, port, cfg.get("open_browser") is not False) # offered only when someone answers: --yes installs and adopted earlier installs are not held up by it if cal is None and not hip and not a.no_start and not a.yes and ask( "Tune Strata for this PC now? It measures a few engine settings (about 5-10 minutes; the PC is busy " @@ -3685,6 +4402,10 @@ def main() -> int: say(f" Next time: just run {'START-HERE.bat' if WIN else './setup.sh'} (or {script.name}) - it starts right away") if vision != "none": say(" Images: send them in the chat page, in chat.py (/image ) or over the API") + if a.parallel is None: # #465: the opt-in, said once (nothing changes) + for line in parallel_note(None, [g.get("vram_gb", 0.0) for g in chosen], MODELS[model]["arena_gb"], ctx, kv, + "--kv-resident" in cfg["args"]): + say(" " + line) if tuned is False: # #447: a failed tuning is repeated here, not only above say(" Tuning: FAILED (the reason is above): the default settings stay - " f"{'START-HERE.bat' if WIN else './setup.sh'} --calibrate tries again") diff --git a/src/core/conversation_cache_test.cpp b/src/core/conversation_cache_test.cpp index f10b729b5a..f544dd743e 100644 --- a/src/core/conversation_cache_test.cpp +++ b/src/core/conversation_cache_test.cpp @@ -2,6 +2,7 @@ #include #include +#include using namespace strata::core; @@ -209,5 +210,96 @@ int main() { check(!disabled.put(image({1,2,3})) && !no_slots.put(image({1,2,3})), "disabled cache stores nothing"); check(disabled.best(a,{},true).tokens == 0, "disabled cache has no matches"); } + { + // layer-split parking: checkpoints moved apart into stage parts and put back, no running state copied + auto running = [](std::initializer_list ids, uint8_t salt, size_t parts) { + ConversationCheckpoint c; + c.ids = ids; + c.gdn.assign(256, salt); + c.tails.assign(32, uint8_t(salt + 1)); + c.used = salt; + for (size_t k = 0; k < parts; ++k) { + ConversationCheckpoint p; + p.gdn.assign(128, uint8_t(salt + 10 + k)); + c.stage_parts.push_back(std::move(p)); + } + return c; + }; + std::vector checks; + checks.push_back(running({1, 2}, 3, 2)); + checks.push_back(running({1, 2, 3}, 5, 1)); // an incomplete set: kept aside, put back in place + checks.push_back(running({1, 2, 3, 4}, 7, 2)); + const auto original = checks; + const uint8_t* gdn0 = checks[0].gdn.data(); + const uint8_t* part1 = checks[2].stage_parts[1].gdn.data(); + auto split = conversation_checkpoints_split(std::move(checks), 2); + check(checks.empty() && split.stage0.size() == 2 && split.rest.size() == 1 && split.parts.size() == 2 && + split.parts[0].size() == 2 && split.parts[1].size() == 2, "split: complete checkpoints apart, the other aside"); + check(split.stage0[0].stage_parts.empty() && split.parts[1][1].ids == original[2].ids && + split.parts[1][1].used == original[2].used && split.parts[0][0].gdn == original[0].stage_parts[0].gdn, + "split: every stage part carries its checkpoint's ids and LRU stamp"); + check(split.stage0[0].gdn.data() == gdn0 && split.parts[1][1].gdn.data() == part1, + "split moves the running state (same buffers, no copy)"); + std::vector back; + check(conversation_checkpoints_merge(std::move(split), back) && back.size() == 3, "merge puts them back"); + bool same = true; + for (size_t j = 0; j < back.size(); ++j) { + same = same && back[j].ids == original[j].ids && back[j].gdn == original[j].gdn && + back[j].tails == original[j].tails && back[j].used == original[j].used && + back[j].stage_parts.size() == original[j].stage_parts.size(); + for (size_t k = 0; same && k < back[j].stage_parts.size(); ++k) + same = back[j].stage_parts[k].gdn == original[j].stage_parts[k].gdn; + } + check(same, "merge(split(x)) == x, in the original order"); + check(back[0].gdn.data() == gdn0 && back[2].stage_parts[1].gdn.data() == part1, "merge moves them back too"); + + auto torn = conversation_checkpoints_split(std::vector(original), 2); + torn.parts[1].pop_back(); + check(!conversation_checkpoints_merge(std::move(torn), back) && back.empty(), "a stage short of a part is refused"); + auto mixed = conversation_checkpoints_split(std::vector(original), 2); + mixed.parts[0][0].ids = {9, 9}; + check(!conversation_checkpoints_merge(std::move(mixed), back) && back.empty(), "a part of another checkpoint is refused"); + // a restored image: stage 0's checkpoints and each stage's, with no record of an incomplete one + ConversationCheckpointSplit restored; + restored.stage0.push_back(running({4, 5}, 11, 0)); + restored.parts.resize(2); + for (auto& p : restored.parts) { auto c = running({4, 5}, 12, 0); p.push_back(std::move(c)); } + check(conversation_checkpoints_merge(std::move(restored), back) && back.size() == 1 && + back[0].stage_parts.size() == 2, "a restored image's parts are all whole"); + auto none = conversation_checkpoints_split({}, 3); + check(conversation_checkpoints_merge(std::move(none), back) && back.empty(), "no checkpoints: nothing to do"); + } + { + // the later stages' retained K/V, kept with the first stage's and limited together + auto kv = [](size_t n) { + std::vector v(2); + for (auto& l : v) l.k.resize(n, 1); + return v; + }; + ConversationCache cache(1 << 20, 4); + std::vector> stage_kv; + stage_kv.push_back(kv(1000)); + stage_kv.push_back(kv(2000)); + cache.retain(kv(500), 40, std::move(stage_kv)); + check(cache.retained_bytes() >= 2 * (500 + 1000 + 2000), "retained bytes count every stage"); + cache.limit_reuse(30); + auto r = cache.take_reuse(); + check(r.kv.size() == 2 && r.stages.size() == 2 && r.unchanged_tokens == 30 && + r.stages[0].unchanged_tokens == 30 && r.stages[1].unchanged_tokens == 30 && + r.stages[1].captured_tokens == 40, "every stage's reuse is limited to the first rewrite"); + check(cache.retained_bytes() == 0, "taken once"); + std::vector> big; + big.push_back(kv(1 << 20)); + cache.retain(kv(500), 40, std::move(big)); + check(cache.retained_bytes() == 0, "a stage's K/V over the budget drops the whole reuse"); + std::vector> one; + one.push_back(kv(100)); + cache.retain(kv(500), 40, std::move(one)); + cache.limit_reuse(0); + check(cache.retained_bytes() == 0, "a rewrite from the start drops every stage's reuse"); + cache.retain(kv(500), 40); + r = cache.take_reuse(); + check(r.kv.size() == 2 && r.stages.empty(), "no layer split: no stage reuse, as before"); + } std::printf("conversation_cache_test: %d checks passed\n", checks); } diff --git a/src/core/conversation_snapshot_test.cpp b/src/core/conversation_snapshot_test.cpp index da95113c65..1f1de53c04 100644 --- a/src/core/conversation_snapshot_test.cpp +++ b/src/core/conversation_snapshot_test.cpp @@ -187,6 +187,47 @@ void full_session(int fmt, int mode, int experts) { check(conversation_kv_verify(incremental.kv.back(),draft.state,g,70,false,fingerprint,err),"incremental draft authoritative and ring read-back"); ids.resize(65); } + { + // a layer split's later stage: the same image WITHOUT the draft layer's K/V (draft == nullptr) + check(conversation_snapshot_restore(a,ss,g,draft.state,err)==ConversationRestore::restored,"restore A before stage images"); + size_t with_draft=0,without=0; + check(conversation_snapshot_bytes(view,ss,g,draft.state,with_draft,err) && + conversation_snapshot_bytes(view,ss,g,nullptr,without,err) && without0 && incremental.bytes()<=peak && equal(incremental.kv[0],fresh.kv[0]) && + incremental.live.gdn==fresh.live.gdn,"an incremental stage capture reuses pages and equals a full one"); + SavedConversation split_image=stage; + split_image.stage_images.push_back(stage); + check(!conversation_snapshot_validate(split_image,ss,g,draft.state,err), + "a layer split's image is refused by the whole-session form"); + ConversationKvReuse wrong{a.kv,65,65,{}}; + check(!conversation_snapshot_capture_bytes(wrong,view,ss,g,nullptr,peak,err),"a draft image's K/V is not a stage's reuse"); + } + check(conversation_snapshot_restore(a,ss,g,draft.state,err)==ConversationRestore::restored,"restore A after stage images"); check(conversation_checkpoint_restore(a.checkpoints[0],ss,g,err),"restore early running checkpoint"); std::vector spare(sizes.dead); cuda_check(cudaMemcpy(spare.data(),main.state.idx_pooled,sizes.dead,cudaMemcpyDeviceToHost)); diff --git a/src/core/conversation_state.cpp b/src/core/conversation_state.cpp index 3e97827cb1..981beca16b 100644 --- a/src/core/conversation_state.cpp +++ b/src/core/conversation_state.cpp @@ -193,7 +193,7 @@ bool conversation_checkpoint_restore(const ConversationCheckpoint& c, SessionSta } bool conversation_snapshot_bytes(const ConversationView& view, const SessionState& ss, - const ModelGeometry& g, const QsaState& draft, size_t& bytes, std::string& error) { + const ModelGeometry& g, const QsaState* draft, size_t& bytes, std::string& error) { bytes = 0; if (!view_validate(view, ss, g, error)) return false; ConversationStateSizes z; @@ -203,7 +203,7 @@ bool conversation_snapshot_bytes(const ConversationView& view, const SessionStat if (!product(ids, {view.ids.size(), sizeof(int32_t)}) || !product(images, {view.images.size(), sizeof(ConversationImageKey)}) || !product(checkpoints, {view.checkpoints.size(), sizeof(ConversationCheckpoint)}) || - !product(layers, {qsa + 1, sizeof(ConversationKv)}) || + !product(layers, {qsa + (draft ? 1 : 0), sizeof(ConversationKv)}) || !product(tails, {qsa, z.tail}) || !product(dead, {qsa, z.dead}) || !product(positions, {qsa, z.block_pos})) return fail(error, "snapshot metadata byte count overflow"); for (size_t n : {ids, images, checkpoints, layers, tails, dead, positions, z.gdn, ss.ple_hist ? z.ple : 0}) @@ -211,8 +211,8 @@ bool conversation_snapshot_bytes(const ConversationView& view, const SessionStat for (const auto& c : view.checkpoints) if (!metadata_bytes(c, bytes)) return fail(error, "checkpoint byte count overflow"); const int64_t upto = (int64_t) view.ids.size(); // view_validate bounds this by signed max_cells - for (uint64_t i = 0; i <= qsa; ++i) { - const auto& st = i == qsa ? draft : owned(ss, (size_t) i); + for (uint64_t i = 0; i < qsa + (draft ? 1 : 0); ++i) { + const auto& st = i == qsa ? *draft : owned(ss, (size_t) i); const size_t n = conversation_kv_bytes(st, g, upto, i != qsa); if (!n || !add(bytes, n)) return fail(error, "invalid or overflowing K/V byte estimate"); } @@ -221,16 +221,16 @@ bool conversation_snapshot_bytes(const ConversationView& view, const SessionStat bool conversation_snapshot_capture_bytes(const ConversationKvReuse& reuse, const ConversationView& view, const SessionState& ss, const ModelGeometry& g, - const QsaState& draft, size_t& bytes, std::string& error) { + const QsaState* draft, size_t& bytes, std::string& error) { if (!conversation_snapshot_bytes(view, ss, g, draft, bytes, error)) return false; if (reuse.kv.empty()) return true; - const size_t layers = owned_qsa(ss) + 1; + const size_t layers = owned_qsa(ss) + (draft ? 1 : 0); if (reuse.kv.size() != layers || reuse.unchanged_tokens < 0 || reuse.unchanged_tokens > reuse.captured_tokens || reuse.unchanged_tokens > int64_t(view.ids.size())) return fail(error, "invalid retained K/V prefix"); for (size_t i = 0; i < layers; ++i) { - const bool index = i + 1 != layers; - const auto& st = index ? owned(ss, i) : draft; + const bool index = i < owned_qsa(ss); + const auto& st = index ? owned(ss, i) : *draft; if (!conversation_kv_validate(reuse.kv[i], st, g, reuse.captured_tokens, index, error)) return false; const size_t fresh = conversation_kv_bytes(st, g, int64_t(view.ids.size()), index); size_t retained = 0; @@ -246,7 +246,7 @@ bool conversation_snapshot_capture_bytes(const ConversationKvReuse& reuse, const bool conversation_snapshot_save(SavedConversation& image, const ConversationView& view, const SessionState& ss, const ModelGeometry& g, - const QsaState& draft, std::string& error, + const QsaState* draft, std::string& error, ConversationKvReuse reuse, size_t* reused_bytes) { size_t estimate = 0; if (!conversation_snapshot_capture_bytes(reuse, view, ss, g, draft, estimate, error) || !sync(error)) return false; @@ -259,7 +259,7 @@ bool conversation_snapshot_save(SavedConversation& image, const ConversationView const int64_t unchanged = reuse.kv.empty() ? 0 : reuse.unchanged_tokens; captured.kv = std::move(reuse.kv); const size_t layers = owned_qsa(ss); - captured.kv.resize(layers + 1); + captured.kv.resize(layers + (draft ? 1 : 0)); if (!conversation_checkpoint_save(captured.live, ss, g, error)) return false; const int64_t upto = (int64_t) view.ids.size(); for (size_t j = 0; j < layers; ++j) @@ -267,15 +267,16 @@ bool conversation_snapshot_save(SavedConversation& image, const ConversationView unchanged, reused_bytes)) return false; // The draft's final cell may not have been computed when the output cap was // reached. Refresh that page even when the main prefix continued unchanged. - if (!conversation_kv_save(captured.kv.back(), draft, g, upto, false, error, - std::max(0, unchanged - 1), reused_bytes)) return false; + if (draft && !conversation_kv_save(captured.kv.back(), *draft, g, upto, false, error, + std::max(0, unchanged - 1), reused_bytes)) return false; image = std::move(captured); return true; } bool conversation_snapshot_validate(const SavedConversation& image, const SessionState& ss, - const ModelGeometry& g, const QsaState& draft, std::string& error) { - if (!image.live.stage_parts.empty()) return fail(error, "layer-split parking is not supported"); + const ModelGeometry& g, const QsaState* draft, std::string& error) { + if (!image.live.stage_parts.empty()) + return fail(error, "a running checkpoint's stage parts belong in the stage images, not in live.stage_parts"); if (image.geometry != geometry_key(g)) return fail(error, "incompatible runtime geometry"); // an image holds exactly one carve's running state and K/V: same layer range or nothing if (image.layer_lo != ss.layer_lo || image.layer_hi != ss.layer_hi) @@ -283,23 +284,49 @@ bool conversation_snapshot_validate(const SavedConversation& image, const Sessio const ConversationView view{image.live.ids, image.live.imgs, image.checkpoints, image.cvec}; if (!view_validate(view, ss, g, error) || !conversation_checkpoint_validate(image.live, ss, g, error)) return false; const size_t layers = owned_qsa(ss); - if (image.kv.size() != layers + 1) return fail(error, "invalid K/V layer count"); + if (image.kv.size() != layers + (draft ? 1 : 0)) return fail(error, "invalid K/V layer count"); const int64_t upto = (int64_t) image.live.ids.size(); for (size_t j = 0; j < layers; ++j) if (!conversation_kv_validate(image.kv[j], owned(ss, j), g, upto, true, error)) return false; - return conversation_kv_validate(image.kv.back(), draft, g, upto, false, error); + return !draft || conversation_kv_validate(image.kv.back(), *draft, g, upto, false, error); } ConversationRestore conversation_snapshot_restore(const SavedConversation& image, SessionState& ss, - const ModelGeometry& g, const QsaState& draft, std::string& error) { + const ModelGeometry& g, const QsaState* draft, std::string& error) { if (!conversation_snapshot_validate(image, ss, g, draft, error)) return ConversationRestore::invalid; if (!sync(error)) return ConversationRestore::transfer_failed; const int64_t upto = (int64_t) image.live.ids.size(); for (size_t j = 0; j < owned_qsa(ss); ++j) if (!conversation_kv_restore(image.kv[j], owned(ss, j), g, upto, true, error)) return ConversationRestore::transfer_failed; - if (!conversation_kv_restore(image.kv.back(), draft, g, upto, false, error) || + if ((draft && !conversation_kv_restore(image.kv.back(), *draft, g, upto, false, error)) || !conversation_checkpoint_restore(image.live, ss, g, error)) return ConversationRestore::transfer_failed; return ConversationRestore::restored; } + +// the draft layer's K/V included (the stage that owns the draft head, or no layer split) +bool conversation_snapshot_bytes(const ConversationView& view, const SessionState& ss, const ModelGeometry& g, + const QsaState& draft, size_t& bytes, std::string& error) { + return conversation_snapshot_bytes(view, ss, g, &draft, bytes, error); +} +bool conversation_snapshot_capture_bytes(const ConversationKvReuse& reuse, const ConversationView& view, + const SessionState& ss, const ModelGeometry& g, const QsaState& draft, + size_t& bytes, std::string& error) { + return conversation_snapshot_capture_bytes(reuse, view, ss, g, &draft, bytes, error); +} +bool conversation_snapshot_save(SavedConversation& image, const ConversationView& view, const SessionState& ss, + const ModelGeometry& g, const QsaState& draft, std::string& error, + ConversationKvReuse reuse, size_t* reused_bytes) { + return conversation_snapshot_save(image, view, ss, g, &draft, error, std::move(reuse), reused_bytes); +} +bool conversation_snapshot_validate(const SavedConversation& image, const SessionState& ss, const ModelGeometry& g, + const QsaState& draft, std::string& error) { + // the whole-session form: a layer split's image (stage images) is restored stage by stage, never through here + if (!image.stage_images.empty()) return fail(error, "a layer split's image restored as a single session"); + return conversation_snapshot_validate(image, ss, g, &draft, error); +} +ConversationRestore conversation_snapshot_restore(const SavedConversation& image, SessionState& ss, + const ModelGeometry& g, const QsaState& draft, std::string& error) { + return conversation_snapshot_restore(image, ss, g, &draft, error); +} } // namespace strata::core diff --git a/src/core/device.cu b/src/core/device.cu index 98fc071714..fbc7030731 100644 --- a/src/core/device.cu +++ b/src/core/device.cu @@ -212,7 +212,7 @@ std::string gpu_arch_problem(int ordinal) { } std::string device_code_error() { -#if defined(STRATA_USE_HIP) +#if defined(STRATA_USE_HIP) || defined(STRATA_HIP_GFX906) 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 @@ -263,7 +263,17 @@ DeviceInfo device_info(int ordinal) { // supported arch is enforced by CMake; RUNNING on an older card is caught here, because a binary can be carried // to a machine with an older card and would otherwise silently take whatever path the driver chose. The HIP // backend checks the card against the architectures the binary was compiled for (and wave32). -#if defined(STRATA_USE_HIP) +#if defined(STRATA_HIP_GFX906) + // AMD gfx906 (the STRATA_HIP_GFX906 compat build, not STRATA_USE_HIP): the build targets one gfx arch + // (CMAKE_HIP_ARCHITECTURES, wave64); a card of another arch fails at the first kernel launch with + // hipErrorInvalidDeviceFunction. gcnArchName says which it is. + { + std::string arch(p.gcnArchName); + if (const size_t colon = arch.find(':'); colon != std::string::npos) arch.resize(colon); + d.arch = arch; + } + if (p.gcnArchName[0] != 0) d.name += std::string(" (") + p.gcnArchName + ")"; +#elif defined(STRATA_USE_HIP) d.arch = base_arch(p.gcnArchName); if (const std::string why = arch_problem(p, ordinal); !why.empty()) throw CudaError(why, -1); #else diff --git a/src/core/expert_cache.cpp b/src/core/expert_cache.cpp index dc64443679..b8df2faf4b 100644 --- a/src/core/expert_cache.cpp +++ b/src/core/expert_cache.cpp @@ -1,7 +1,16 @@ // src/core/expert_cache.cpp - R4's slot storage and residency table. Read the header first. #include "strata/core/expert_cache.hpp" +// #533's segmented cache uses CUDA's virtual memory management (cuMem*): not on HIP, neither the RDNA backend nor +// the gfx906 compat build (PR #638), which compiles this file as HIP without STRATA_USE_HIP +#if defined(STRATA_USE_HIP) || defined(STRATA_HIP_GFX906) +#define STRATA_EC_NO_VMM 1 +#endif + #include +#if !defined(STRATA_EC_NO_VMM) +#include // #533: the virtual memory management types (the functions come through the runtime's entry points) +#endif #include #include @@ -142,6 +151,239 @@ bool write_expert_profile(const std::string& path, int64_t n_layers, int64_t n_e ExpertCache::~ExpertCache() { close(); } +// ---- #533: the segmented arena (--vram-elastic). The driver API's virtual memory functions, looked up through the +// runtime (no link against the driver library): one address range for the whole arena, backed by physical segments, +// and the tail's segments unmapped / mapped again later. Nothing here runs unless a segment size was set. +#if !defined(STRATA_EC_NO_VMM) +namespace { +struct Vmm { + CUresult (CUDAAPI* device_get)(CUdevice*, int) = nullptr; + CUresult (CUDAAPI* attribute)(int*, CUdevice_attribute, CUdevice) = nullptr; + CUresult (CUDAAPI* granularity)(size_t*, const CUmemAllocationProp*, CUmemAllocationGranularity_flags) = nullptr; + CUresult (CUDAAPI* reserve)(CUdeviceptr*, size_t, size_t, CUdeviceptr, unsigned long long) = nullptr; + CUresult (CUDAAPI* address_free)(CUdeviceptr, size_t) = nullptr; + CUresult (CUDAAPI* create)(CUmemGenericAllocationHandle*, size_t, const CUmemAllocationProp*, + unsigned long long) = nullptr; + CUresult (CUDAAPI* release)(CUmemGenericAllocationHandle) = nullptr; + CUresult (CUDAAPI* map)(CUdeviceptr, size_t, size_t, CUmemGenericAllocationHandle, unsigned long long) = nullptr; + CUresult (CUDAAPI* unmap)(CUdeviceptr, size_t) = nullptr; + CUresult (CUDAAPI* set_access)(CUdeviceptr, size_t, const CUmemAccessDesc*, size_t) = nullptr; + bool ok = false; +}; + +template bool entry(const char* name, F& f) { + void* p = nullptr; + cudaDriverEntryPointQueryResult q{}; +#if CUDART_VERSION >= 12050 + const cudaError_t e = cudaGetDriverEntryPointByVersion(name, &p, 12000, cudaEnableDefault, &q); +#else + const cudaError_t e = cudaGetDriverEntryPoint(name, &p, cudaEnableDefault, &q); +#endif + if (e != cudaSuccess || q != cudaDriverEntryPointSuccess || p == nullptr) { + (void) cudaGetLastError(); + return false; + } + f = reinterpret_cast(p); + return true; +} + +const Vmm& vmm() { + static const Vmm v = [] { + Vmm x; + x.ok = entry("cuDeviceGet", x.device_get) && entry("cuDeviceGetAttribute", x.attribute) && + entry("cuMemGetAllocationGranularity", x.granularity) && entry("cuMemAddressReserve", x.reserve) && + entry("cuMemAddressFree", x.address_free) && entry("cuMemCreate", x.create) && + entry("cuMemRelease", x.release) && entry("cuMemMap", x.map) && entry("cuMemUnmap", x.unmap) && + entry("cuMemSetAccess", x.set_access); + return x; + }(); + return v; +} + +CUmemAllocationProp device_prop(int dev) { + CUmemAllocationProp prop{}; + prop.type = CU_MEM_ALLOCATION_TYPE_PINNED; + prop.location.type = CU_MEM_LOCATION_TYPE_DEVICE; + prop.location.id = dev; + return prop; +} + +// one segment: a physical allocation mapped at `va`, readable and writable by this device +bool map_segment(const Vmm& v, int dev, CUdeviceptr va, size_t bytes, unsigned long long& handle) { + const CUmemAllocationProp prop = device_prop(dev); + CUmemGenericAllocationHandle h = 0; + if (v.create(&h, bytes, &prop, 0) != CUDA_SUCCESS) return false; + if (v.map(va, bytes, 0, h, 0) != CUDA_SUCCESS) { + v.release(h); + return false; + } + CUmemAccessDesc access{}; + access.location.type = CU_MEM_LOCATION_TYPE_DEVICE; + access.location.id = dev; + access.flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE; + if (v.set_access(va, bytes, &access, 1) != CUDA_SUCCESS) { + v.unmap(va, bytes); + v.release(h); + return false; + } + handle = (unsigned long long) h; + return true; +} +} // namespace +#endif + +bool ExpertCache::open_segmented(uint64_t want, std::string& err) { +#if defined(STRATA_EC_NO_VMM) + (void) want; + err = "ExpertCache: --vram-elastic (a segmented expert cache) is CUDA-only for now"; + return false; +#else + const Vmm& v = vmm(); + int dev = 0, supported = 0; + CUdevice cu = 0; + if (!v.ok || cudaGetDevice(&dev) != cudaSuccess || v.device_get(&cu, dev) != CUDA_SUCCESS || + v.attribute(&supported, CU_DEVICE_ATTRIBUTE_VIRTUAL_MEMORY_MANAGEMENT_SUPPORTED, cu) != CUDA_SUCCESS || + !supported) { + err = "ExpertCache: --vram-elastic needs the driver's virtual memory management, which this GPU or driver " + "does not offer"; + return false; + } + const CUmemAllocationProp prop = device_prop(dev); + size_t gran = 0; + if (v.granularity(&gran, &prop, CU_MEM_ALLOC_GRANULARITY_RECOMMENDED) != CUDA_SUCCESS || gran == 0) { + err = "ExpertCache: cannot read the driver's allocation granularity"; + return false; + } + const uint64_t g = (uint64_t) gran; + const uint64_t total = (want + g - 1) / g * g; + seg_ = (int64_t) (((uint64_t) seg_req_ + g - 1) / g * g); + CUdeviceptr va = 0; + if (v.reserve(&va, (size_t) total, 0, 0, 0) != CUDA_SUCCESS) { + err = "ExpertCache: cannot reserve the address range of the segmented expert cache"; + return false; + } + base_ = reinterpret_cast(va); + reserved_ = total; + for (uint64_t at = 0; at < total; at += (uint64_t) seg_) { + segs_.push_back(0); + seg_size_.push_back((int64_t) std::min((uint64_t) seg_, total - at)); + } + for (size_t i = 0; i < segs_.size(); ++i) { + if (!map_segment(v, dev, va + (CUdeviceptr) ((uint64_t) i * (uint64_t) seg_), (size_t) seg_size_[i], + segs_[i])) { + char buf[200]; + std::snprintf(buf, sizeof buf, "ExpertCache: cudaMalloc failed: segment %zu of %zu (%.2f GiB) of the " + "segmented cache could not be allocated", i + 1, segs_.size(), + (double) seg_size_[i] / 1073741824.0); + err = buf; // "cudaMalloc failed": the auto cache's smaller-retry path reads it as an allocation failure + release_segmented(); + return false; + } + mapped_segs_ = (int64_t) i + 1; + } + return true; +#endif +} + +void ExpertCache::release_segmented() { +#if !defined(STRATA_EC_NO_VMM) + const Vmm& v = vmm(); + if (base_ != nullptr) cudaDeviceSynchronize(); + const CUdeviceptr va = reinterpret_cast(base_); + for (size_t i = 0; i < segs_.size(); ++i) + if (segs_[i] != 0) { + v.unmap(va + (CUdeviceptr) ((uint64_t) i * (uint64_t) seg_), (size_t) seg_size_[i]); + v.release((CUmemGenericAllocationHandle) segs_[i]); + } + if (base_ != nullptr && reserved_ > 0) v.address_free(va, (size_t) reserved_); +#endif + segs_.clear(); + seg_size_.clear(); + mapped_segs_ = 0; + reserved_ = 0; + base_ = nullptr; +} + +int64_t ExpertCache::mapped_bytes() const { + if (segs_.empty()) return base_ != nullptr ? full_bytes() : 0; + int64_t b = 0; + for (int64_t i = 0; i < mapped_segs_; ++i) b += seg_size_[(size_t) i]; + return b; +} + +int64_t ExpertCache::slots_within(int64_t bytes) const { + if (bytes >= full_bytes()) return slots_; + if (bytes <= 0) return 0; + if (off_.empty()) return blob_ > 0 ? bytes / blob_ : 0; + // off_[i + 1] is slot i's end: count the slots whose end is at most `bytes` + return (int64_t) (std::upper_bound(off_.begin() + 1, off_.end(), (uint64_t) bytes) - (off_.begin() + 1)); +} + +bool ExpertCache::shrink(int64_t keep_bytes, std::string& err) { + if (segs_.empty()) { + err = "the expert cache is not segmented (the engine needs --vram-elastic)"; + return false; + } +#if defined(STRATA_EC_NO_VMM) + (void) keep_bytes; + return false; +#else + const Vmm& v = vmm(); + int64_t keep = 0, at = 0; // the segments [0, keep) hold the first keep_bytes + while (keep < (int64_t) segs_.size() && at < keep_bytes) at += seg_size_[(size_t) keep++]; + if (keep >= mapped_segs_) return true; + if (cudaDeviceSynchronize() != cudaSuccess) { + err = std::string("the device failed before the cache shrank: ") + cudaGetErrorString(cudaGetLastError()); + return false; + } + const CUdeviceptr va = reinterpret_cast(base_); + for (int64_t i = mapped_segs_ - 1; i >= keep; --i) { + const CUdeviceptr p = va + (CUdeviceptr) ((uint64_t) i * (uint64_t) seg_); + if (v.unmap(p, (size_t) seg_size_[(size_t) i]) != CUDA_SUCCESS || + v.release((CUmemGenericAllocationHandle) segs_[(size_t) i]) != CUDA_SUCCESS) { + err = "the driver refused to release an expert-cache segment"; + live_slots_ = slots_within(mapped_bytes()); + return false; + } + segs_[(size_t) i] = 0; + mapped_segs_ = i; + } + live_slots_ = slots_within(mapped_bytes()); + return true; +#endif +} + +bool ExpertCache::grow(int64_t want_bytes, std::string& err) { + if (segs_.empty()) { + err = "the expert cache is not segmented (the engine needs --vram-elastic)"; + return false; + } +#if defined(STRATA_EC_NO_VMM) + (void) want_bytes; + return false; +#else + const Vmm& v = vmm(); + int dev = 0; + cudaGetDevice(&dev); + const CUdeviceptr va = reinterpret_cast(base_); + int64_t at = mapped_bytes(); + while (mapped_segs_ < (int64_t) segs_.size() && at + seg_size_[(size_t) mapped_segs_] <= want_bytes) { + const size_t i = (size_t) mapped_segs_; + if (!map_segment(v, dev, va + (CUdeviceptr) ((uint64_t) i * (uint64_t) seg_), (size_t) seg_size_[i], + segs_[i])) { + (void) cudaGetLastError(); + err = "the driver has no VRAM for another expert-cache segment"; + live_slots_ = slots_within(mapped_bytes()); + return false; + } + at += seg_size_[i]; + ++mapped_segs_; + } + live_slots_ = slots_within(mapped_bytes()); + return true; +#endif +} + #if defined(STRATA_USE_HIP) bool ExpertCache::ensure_blocking_staging(std::size_t bytes, std::string& err) { if (bytes <= blocking_staging_bytes_) return true; @@ -193,7 +435,9 @@ bool ExpertCache::open(int64_t n_slots, int64_t n_layers, int64_t n_expert, int6 } } - if (cudaMalloc((void**) &base_, (size_t) want) != cudaSuccess) { + if (seg_req_ > 0) { // #533: --vram-elastic: physical segments behind one address range (zeroed below) + if (!open_segmented(want, err)) return false; + } else if (cudaMalloc((void**) &base_, (size_t) want) != cudaSuccess) { base_ = nullptr; char buf[256]; std::snprintf(buf, sizeof buf, "ExpertCache: cudaMalloc(%.2f GiB) failed: %s", @@ -211,6 +455,7 @@ bool ExpertCache::open(int64_t n_slots, int64_t n_layers, int64_t n_expert, int6 residency_.assign((size_t) (n_layers * n_expert), kNotResident); slots_ = n_slots; + live_slots_ = n_slots; n_layers_ = n_layers; n_expert_ = n_expert; blob_ = blob_bytes; @@ -247,6 +492,7 @@ bool ExpertCache::open_sized(const std::vector& slot_bytes, int64_t n_l // one allocation of the summed size, through the uniform path's checks: n "slots" of 1 byte if (!open((int64_t) off.back(), n_layers, n_expert, 1, err)) return false; slots_ = (int64_t) slot_bytes.size(); + live_slots_ = slots_; blob_ = mx; off_ = std::move(off); #if defined(STRATA_USE_HIP) @@ -273,12 +519,15 @@ void ExpertCache::close() { blocking_staging_bytes_ = 0; #endif off_.clear(); - if (base_ != nullptr) { + if (!segs_.empty()) { + release_segmented(); + } else if (base_ != nullptr) { cudaFree(base_); base_ = nullptr; } residency_.clear(); slots_ = 0; + live_slots_ = 0; n_layers_ = 0; n_expert_ = 0; blob_ = 0; diff --git a/src/core/expert_source.cpp b/src/core/expert_source.cpp index 56072fc5f6..ceef0285ff 100644 --- a/src/core/expert_source.cpp +++ b/src/core/expert_source.cpp @@ -10,6 +10,7 @@ #include "strata/kernels/elementwise.hpp" #include "strata/kernels/quantize_act.hpp" #include "strata/kernels/s2_expert_grouped.hpp" +#include "strata/kernels/cpu/kq_avx1.hpp" #include "strata/kernels/cpu/kq_avx2.hpp" #include @@ -217,18 +218,25 @@ bool read_cgroup_memory_stat(const std::filesystem::path& path, uint64_t current } #endif -bool available_memory_bytes(uint64_t& bytes) { +} // namespace + +namespace detail { + +bool host_available_memory(HostMemory& m, const std::string& meminfo, const std::string& self_cgroup, + const std::string& cgroup_root) { + m = HostMemory{}; #if defined(_WIN32) + (void) meminfo; (void) self_cgroup; (void) cgroup_root; MEMORYSTATUSEX status{}; status.dwLength = sizeof(status); if (!GlobalMemoryStatusEx(&status)) return false; - bytes = (uint64_t) status.ullAvailPhys; - return bytes > 0; + m.available = (uint64_t) status.ullAvailPhys; + return m.available > 0; #elif defined(__linux__) // MemAvailable includes reclaimable page cache, unlike _SC_AVPHYS_PAGES. - std::ifstream info("/proc/meminfo"); + std::ifstream info(meminfo); std::string line; - bytes = 0; + uint64_t bytes = 0; while (std::getline(info, line)) { std::istringstream fields(line); std::string key, unit; @@ -237,17 +245,26 @@ bool available_memory_bytes(uint64_t& bytes) { value <= std::numeric_limits::max() / 1024) bytes = value * 1024; } if (bytes == 0) return false; - // Account for the tightest cgroup-v2 ancestor limit when its normal mount is visible. + // Account for the tightest cgroup ancestor limit when its normal mount is visible. // This is a point-in-time guard, not a reservation against concurrent allocations. - std::ifstream groups("/proc/self/cgroup"); + std::ifstream groups(self_cgroup); if (!groups) return false; - bool resolved_v2 = false; + const std::filesystem::path root(cgroup_root); + bool v2 = false; + std::string v1_path; // the memory controller's group (cgroup v1), when there is no v2 line while (std::getline(groups, line)) { - if (line.rfind("0::/", 0) != 0) continue; - const std::filesystem::path root("/sys/fs/cgroup"); + if (line.rfind("0::/", 0) != 0) { + // v1: "N:controller[,controller]:/path" + const size_t c1 = line.find(':'), c2 = c1 == std::string::npos ? c1 : line.find(':', c1 + 1); + if (c2 == std::string::npos) continue; + std::istringstream ctl(line.substr(c1 + 1, c2 - c1 - 1)); + for (std::string c; std::getline(ctl, c, ',');) + if (c == "memory") v1_path = line.substr(c2 + 1); + continue; + } auto path = (root / line.substr(4)).lexically_normal(); if (path.string().rfind(root.string(), 0) != 0 || !std::filesystem::is_directory(path)) return false; - resolved_v2 = true; + v2 = true; while (path.string().rfind(root.string(), 0) == 0) { std::ifstream limit_file(path / "memory.max"), current_file(path / "memory.current"); std::string limit; @@ -261,28 +278,59 @@ bool available_memory_bytes(uint64_t& bytes) { size_t consumed = 0; const uint64_t cap = std::stoull(limit, &consumed); if (consumed != limit.size()) return false; - detail::CgroupMemoryStat stat; + CgroupMemoryStat stat; if (!read_cgroup_memory_stat(path, current, stat)) return false; uint64_t cgroup_available = 0; - if (!detail::cgroup_available_bytes(cap, stat, cgroup_available)) return false; + if (!cgroup_available_bytes(cap, stat, cgroup_available)) return false; bytes = std::min(bytes, cgroup_available); + m.cgroup_limit = std::min(m.cgroup_limit, cap); } catch (...) { return false; } } if (path == root) break; path = path.parent_path(); } } - return resolved_v2; + if (!v2 && !v1_path.empty()) { + // #633: cgroup v1 (older Docker hosts, RHEL 7/8): the memory controller's group and its ancestors. An + // unlimited group says a number near 2^63 (rounded to its page size); a group whose files are not + // visible (no mount in this namespace) is skipped - MemAvailable alone, as with no cgroup at all. + const std::filesystem::path mroot = root / "memory"; + auto path = (mroot / v1_path.substr(v1_path.rfind('/', 0) == 0 ? 1 : 0)).lexically_normal(); + while (path.string().rfind(mroot.string(), 0) == 0) { + std::ifstream limit_file(path / "memory.limit_in_bytes"), usage_file(path / "memory.usage_in_bytes"); + uint64_t cap = 0, usage = 0; + if (limit_file >> cap && usage_file >> usage && cap < (1ull << 62)) { + bytes = std::min(bytes, usage < cap ? cap - usage : 0); + m.cgroup_limit = std::min(m.cgroup_limit, cap); + } + if (path == mroot) break; + path = path.parent_path(); + } + } + m.available = bytes; + return true; #else + (void) meminfo; (void) self_cgroup; (void) cgroup_root; const long pages = sysconf(_SC_AVPHYS_PAGES); const long page_bytes = sysconf(_SC_PAGESIZE); if (pages <= 0 || page_bytes <= 0 || (uint64_t) pages > std::numeric_limits::max() / (uint64_t) page_bytes) return false; - bytes = (uint64_t) pages * (uint64_t) page_bytes; - return bytes > 0; + m.available = (uint64_t) pages * (uint64_t) page_bytes; + return m.available > 0; #endif } +} // namespace detail + +namespace { + +bool available_memory_bytes(uint64_t& bytes) { + detail::HostMemory m; + if (!detail::host_available_memory(m)) return false; + bytes = m.available; + return bytes > 0; +} + } // namespace // ================================ THE FILE-BACKED SOURCE ================================ @@ -872,13 +920,14 @@ void FileExpertSource::fill_many(const std::vector& todo) { for (auto& t : th) t.join(); } -bool FileExpertSource::set_unbuffered(uint64_t ram_bytes, std::string& why) { +uint64_t FileExpertSource::expert_bytes() const { + uint64_t total = 0; + for (uint64_t b : layer_blob_bytes_) total += b * (uint64_t) n_expert_; + return total; +} + +bool FileExpertSource::open_direct(std::string& why) { #if defined(_WIN32) - if (base_ == nullptr || paths_.empty() || !direct_.empty()) { - why = !direct_.empty() ? "already unbuffered" : "no expert files open"; - return !direct_.empty(); - } - if (!experts_unbuffered(paths_, ram_bytes, why, /*cache_counts=*/false)) return false; for (const std::string& path : paths_) { const int wide = MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, nullptr, 0); std::vector w((size_t) (wide > 0 ? wide : 1), L'\0'); @@ -899,6 +948,24 @@ bool FileExpertSource::set_unbuffered(uint64_t ram_bytes, std::string& why) { for (uint64_t b : layer_blob_bytes_) stage_blob_ = std::max(stage_blob_, b); } return true; +#else + (void) why; + return false; +#endif +} + +bool FileExpertSource::set_unbuffered(uint64_t ram_bytes, std::string& why) { +#if defined(_WIN32) + if (base_ == nullptr || paths_.empty() || !direct_.empty()) { + why = !direct_.empty() ? "already unbuffered" : "no expert files open"; + return !direct_.empty(); + } + // #577: the file tier reads the experts outside the RAM copy, not every byte of the shards (dense weights, the + // PLE table), and the copy holds at most every expert - the budget asked for can be more than that + const uint64_t experts = expert_bytes(); + const uint64_t arena = std::min(ram_bytes, experts); + if (!experts_unbuffered(paths_, arena, why, /*cache_counts=*/false, experts - arena)) return false; + return open_direct(why); #else (void) ram_bytes; why = "through the file cache (not Windows)"; @@ -906,6 +973,39 @@ bool FileExpertSource::set_unbuffered(uint64_t ram_bytes, std::string& why) { #endif } +bool FileExpertSource::recheck_unbuffered(std::string& why) { +#if defined(_WIN32) + if (const char* env = std::getenv("STRATA_UNBUFFERED_LOAD"); env != nullptr && env[0] != '\0') { + why = std::string("STRATA_UNBUFFERED_LOAD=") + env; + return !direct_.empty(); + } + if (base_ == nullptr || paths_.empty()) { + why = "no expert files open"; + return false; + } + // #577: the RAM copy is built (and already out of the available RAM), so what the file cache would have to keep + // is exactly the experts outside it - the GPU cache's (refilled after a prompt borrowed their slots) and the + // ones neither holds + const uint64_t experts = expert_bytes(); + const uint64_t read = experts > complement_bytes_ ? experts - complement_bytes_ : 0; + std::string w; + const bool ub = experts_unbuffered(paths_, 0, w, /*cache_counts=*/false, read); + char head[96]; + std::snprintf(head, sizeof head, "re-checked with the RAM copy built (%.2f GiB): ", + (double) complement_bytes_ / 1073741824.0); + why = head + w; + if (ub == !direct_.empty()) return ub; + if (ub) return open_direct(why); + // through the file cache after all: the mapped reads take over (staged() stays true for the GGUF in place) + for (void* d : direct_) CloseHandle((HANDLE) d); + direct_.clear(); + return false; +#else + why = "through the file cache (not Windows)"; + return false; +#endif +} + bool FileExpertSource::read_direct(const Fill* fills, size_t n) const { #if defined(_WIN32) // NTFS runs the unbuffered reads of a file one at a time while the file is mapped or cached anywhere (the engine @@ -1120,8 +1220,35 @@ void RouterLookahead::run() { } const auto t0 = std::chrono::steady_clock::now(); want.clear(); - strata::kernels::cpu::bf16_rows_dot_multi(routers_[(size_t) layer].data(), (int) n_expert_, (int) n_embd_, - x_.data(), (int) nt, logits.data()); + // The router dot is AVX2 (kq_avx2.cpp); a CPU without AVX2 (the experimental older-CPU builds) takes the AVX1 + // one (kq_avx1.cpp) or, without AVX, the same sums in plain C++. From the Strata_Dirigo fork (rwkeyes): an + // AVX-only Xeon E5-2687W died here (vpmovzxwd) on its first request. An estimate only (which experts to + // prefetch); the order of the additions differs between the three, the output does not depend on it. + if (strata::kernels::cpu::cpu_avx2_ok()) { + strata::kernels::cpu::bf16_rows_dot_multi(routers_[(size_t) layer].data(), (int) n_expert_, (int) n_embd_, + x_.data(), (int) nt, logits.data()); + } else if (strata::kernels::cpu::cpu_avx1_ok()) { + strata::kernels::cpu::bf16_rows_dot_multi_avx1(routers_[(size_t) layer].data(), (int) n_expert_, + (int) n_embd_, x_.data(), (int) nt, logits.data()); + } else { + // a mul and an add, not std::fma: without an FMA instruction that is a libm call per element (74x + // slower on a Xeon E5-2665, measured by the fork) + const uint16_t* rw = routers_[(size_t) layer].data(); + for (int64_t r = 0; r < n_expert_; ++r) { + const uint16_t* wr = rw + (size_t) r * (size_t) n_embd_; + for (int64_t t = 0; t < nt; ++t) { + const float* xr = x_.data() + (size_t) t * (size_t) n_embd_; + float acc = 0.0f; + for (int64_t c = 0; c < n_embd_; ++c) { + const uint32_t bits = (uint32_t) wr[c] << 16; + float wf; + std::memcpy(&wf, &bits, sizeof wf); + acc += wf * xr[c]; + } + logits[(size_t) t * (size_t) n_expert_ + (size_t) r] = acc; + } + } + } for (int64_t t = 0; t < nt; ++t) { const float* lt = logits.data() + (size_t) (t * n_expert_); for (int64_t e = 0; e < n_expert_; ++e) order[(size_t) e] = (int32_t) e; @@ -1883,17 +2010,9 @@ void expert_pool_dispatch_multi(ExpertDispatch& d, const float* x_f, const int32 pt("begin"); d.src->begin_layer(d.layers, ids, n_tok * k); pt("begun"); - if (!d.usage.empty()) { - const uint64_t trace_now = d.first_miss_ns.empty() ? 0 : (uint64_t) std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count(); - for (int64_t i = 0; i < n_tok * k; ++i) { - if (ids[i] < 0 || ids[i] >= d.n_expert) continue; - const size_t ri = (size_t) d.layers * (size_t) d.n_expert + (size_t) ids[i]; - if (trace_now && d.host_res != nullptr && d.host_res[ri] < 0 && d.first_miss_ns[ri] == 0) - d.first_miss_ns[ri] = trace_now; - d.usage[ri] += 1.0f; - } - } + if (!d.usage.empty()) + for (int64_t i = 0; i < n_tok * k; ++i) + if (ids[i] >= 0 && ids[i] < d.n_expert) d.usage[(size_t) d.layers * (size_t) d.n_expert + (size_t) ids[i]] += 1.0f; // ---- plan v0.3 P6: the GPU's share, decided and published FIRST so the GPU starts while the CPU works. // Distinct experts in routing order; resident ones and the last pcie_num/256 of the missed ones go to the GPU. const int64_t n = n_tok * k; @@ -1924,22 +2043,9 @@ void expert_pool_dispatch_multi(ExpertDispatch& d, const float* x_f, const int32 int kd = -1; unsigned long long ptr = 0; if (e >= 0 && e < d.n_expert) { - const size_t ri = (size_t) d.layers * (size_t) d.n_expert + (size_t) e; - const int32_t slot = d.host_res[ri]; + const int32_t slot = d.host_res[(size_t) d.layers * (size_t) d.n_expert + (size_t) e]; if (slot >= 0) { kd = 0; - if (!d.first_resident_ns.empty() && d.first_resident_ns[ri] != 0 && !d.traced_hit_reported[ri]) { - const uint64_t now = (uint64_t) std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count(); - const uint64_t miss = d.first_miss_ns[ri]; - std::fprintf(stderr, - "ADAPT_TRACE gpu_hit layer=%lld expert=%d slot=%d resident_to_hit_us=%.3f miss_to_hit_us=%.3f\n", - (long long) d.layers, (int) e, (int) slot, - (double) (now - d.first_resident_ns[ri]) / 1000.0, - miss ? (double) (now - miss) / 1000.0 : -1.0); - std::fflush(stderr); - d.traced_hit_reported[ri] = 1; - } ptr = (unsigned long long) (d.cache_base + (d.cache_slot_off ? (size_t) d.cache_slot_off[slot] : (size_t) slot * (size_t) d.cache_blob)); } else if (d.peer != nullptr && d.peer->has(d.layers, e)) { @@ -2021,16 +2127,30 @@ void expert_pool_dispatch_multi(ExpertDispatch& d, const float* x_f, const int32 for (int64_t i = 0; i < n; ++i) if (d.remote[r]->owns(i)) kind[i] = 2; } } + static const bool dec_batch = [] { const char* v = std::getenv("STRATA_DEC_BATCH"); return v == nullptr || std::atoi(v) != 0; }(); + // the GPU's copy_rows_from_mapped zeroes the rows it computed itself (dec_batch); STRATA_VERIFY_DEVICE_PLAN's + // copy_or_zero_from_mapped copies every row, so there the host still zeroes them + static const bool device_plan = [] { const char* v = std::getenv("STRATA_VERIFY_DEVICE_PLAN"); return v != nullptr && std::atoi(v) != 0; }(); + const bool gpu_zeroes_hits = (d.plan != nullptr && n <= kMaxWindowEntries && n <= d.plan->cap && dec_batch && !device_plan); + bool any_cpu = false; + for (int64_t i = 0; i < n; ++i) + if (kind[i] < 0) { any_cpu = true; break; } const auto c1 = std::chrono::steady_clock::now(); - if (native && lay.fmt[(size_t) d.layers].gu_type == 42) // a native Q2_0 pack: the Q2_0 kernels' activations - for (int64_t t = 0; t < n_tok; ++t) act_quant_any(x_f + (size_t) t * H, H, d.act_multi[(size_t) t]); - else if (native) - for (int64_t t = 0; t < n_tok; ++t) - native_quant_act(lay.fmt[(size_t) d.layers], x_f + (size_t) t * H, d.nact_multi.data() + (size_t) t * kNativeActBytes); - else - for (int64_t t = 0; t < n_tok; ++t) act_quant_q8_1(x_f + (size_t) t * H, H, d.act_multi[(size_t) t]); + if (any_cpu) { + // #578 --remote-expert-opt: a token whose experts all run on a GPU (CUDA0 or a helper) needs no CPU activation + const bool ep = d.remote_count > 0 && d.remote[0]->optimized_decode(); + for (int64_t t = 0; t < n_tok; ++t) { + if (ep && std::all_of(kind + t * k, kind + (t + 1) * k, [](int32_t v) { return v >= 0; })) continue; + if (native && strata::kernels::cpu::q2_native_kernels(lay.fmt[(size_t) d.layers].gu_type)) // a native Q2_0 pack: the Q2_0 kernels' activations + act_quant_any(x_f + (size_t) t * H, H, d.act_multi[(size_t) t]); + else if (native) + native_quant_act(lay.fmt[(size_t) d.layers], x_f + (size_t) t * H, d.nact_multi.data() + (size_t) t * kNativeActBytes); + else + act_quant_q8_1(x_f + (size_t) t * H, H, d.act_multi[(size_t) t]); + } + } const auto c2 = std::chrono::steady_clock::now(); - { // CS-T: the experts the CPU computes, fetched together (the GGUF in place reads them on several threads) + if (any_cpu) { // CS-T: the experts the CPU computes, fetched together (the GGUF in place reads them on several threads) static thread_local std::vector miss; miss.clear(); for (int64_t i = 0; i < n_tok * k; ++i) @@ -2054,9 +2174,12 @@ void expert_pool_dispatch_multi(ExpertDispatch& d, const float* x_f, const int32 } if (kind[i] >= 0) { // CUDA0, PCIe, or a remote/peer result staged into this row below if (kind[i] == 0) ++d.cache_hits; - else if (kind[i] == 2 && d.peer != nullptr) ++d.peer_entries; - // multi-GPU: a direct peer launch is writing this row right now - zeroing it would race it - if (!(kind[i] == 2 && d.peer != nullptr && d.peer->launched_direct())) + else ++d.offload_entries; // #588: PCIe or another GPU + if (kind[i] == 2 && d.peer != nullptr) ++d.peer_entries; + // multi-GPU: a direct peer launch is writing this row right now - zeroing it would race it; + // dec_batch: copy_rows_from_mapped_kernel already zeroes kind 0/1 rows on the GPU + if (!((kind[i] == 2 && d.peer != nullptr && d.peer->launched_direct()) || + (gpu_zeroes_hits && (kind[i] == 0 || kind[i] == 1)))) std::memset(row, 0, (size_t) H * sizeof(float)); continue; } @@ -2086,8 +2209,10 @@ void expert_pool_dispatch_multi(ExpertDispatch& d, const float* x_f, const int32 } const auto c3 = std::chrono::steady_clock::now(); pt("run", njobs); - if (native) d.pool->run_split_multi_native(lay.fmt[(size_t) d.layers], d.jobs_multi.data(), njobs); - else d.pool->run_split_multi(d.jobs_multi.data(), njobs); + if (njobs > 0) { + if (native) d.pool->run_split_multi_native(lay.fmt[(size_t) d.layers], d.jobs_multi.data(), njobs); + else d.pool->run_split_multi(d.jobs_multi.data(), njobs); + } if (d.remote_count > 0) { static thread_local std::string remote_error; for (int r = 0; r < d.remote_count; ++r) @@ -2591,7 +2716,7 @@ bool ArenaExpertSource::open(const std::string& pack_dir, int64_t n_layers, int6 std::ifstream f(path, std::ios::binary | std::ios::ate); if (!f) { err = "ArenaExpertSource: cannot open " + path; return false; } const uint64_t got = (uint64_t) f.tellg(); - if (got != want) { + if (got != want && got != want + (uint64_t) blob) { // + one blob: STRATA_ARENA_MMAP's padded file char buf[400]; std::snprintf(buf, sizeof buf, "ArenaExpertSource: %s is %llu B but %lld layers x %lld experts (blobs up to %lld B) " @@ -2603,6 +2728,76 @@ bool ArenaExpertSource::open(const std::string& pack_dir, int64_t n_layers, int6 } } + // STRATA_ARENA_MMAP=1 (a small-RAM machine whose GPUs hold most experts): the arena is the pack's experts.bin + // mapped READ-ONLY - not locked, not pinned. The page cache keeps what the CPU pool and the prompt path + // actually read (the experts no VRAM cache holds) and gives back the rest under memory pressure; a page + // dropped is re-read from the file, so a result never depends on what is resident. The GPUs then get no + // mapped alias: run with --pcie-frac 0. The first start writes experts.bin (arena layout, padded by one blob + // so a whole-slot copy may start at any expert), later ones map it. +#if defined(_WIN32) + static const bool arena_mmap = false; // POSIX mmap/madvise: Linux only for now +#else + static const bool arena_mmap = [] { const char* v = std::getenv("STRATA_ARENA_MMAP"); return v && v[0] == '1'; }(); + if (arena_mmap) { + const uint64_t file_bytes = want + (uint64_t) blob; + uint64_t have = 0; + { + std::ifstream f(path, std::ios::binary | std::ios::ate); + if (f) have = (uint64_t) f.tellg(); + } + if (have == file_bytes) { + const int fd = ::open(path.c_str(), O_RDONLY); + void* v = fd >= 0 ? mmap(nullptr, (size_t) file_bytes, PROT_READ, MAP_SHARED, fd, 0) : MAP_FAILED; + if (fd >= 0) ::close(fd); + if (v == MAP_FAILED) { err = "ArenaExpertSource: mmap of " + path + " failed"; return false; } + map_ = v; + map_bytes_ = file_bytes; + base_ = (const uint8_t*) v; + pinned_bytes_ = 0; + dev_slice_.clear(); + slice_bytes_ = 0; + blobs_ = n_layers * n_expert; + n_expert_ = n_expert; + reads_ = 0; + note_ = "mapped read-only from " + path + " (STRATA_ARENA_MMAP: not locked, not pinned)"; + gib_per_s_ = 0.0; + load_seconds_ = load_read_s_ = load_copy_s_ = 0.0; + return true; + } + } +#endif + + // #633: THE RAM BEFORE THE ALLOCATION. On Linux the arena is an anonymous mapping that succeeds whatever the host + // has; its pages are committed as the load writes them, so a container whose memory limit is below the arena was + // killed by the OOM killer part-way through the load, without a message. A hard cgroup limit below it is + // certain to end that way: refused, with the numbers. Less RAM available than the arena (another program, an + // engine still exiting) is only a warning - the OS may make room - per the recommend-not-force rule. + ram_warning_.clear(); + // (a shared arena, --shared-expert-arena, may already be in RAM for another engine: not checked) + if (detail::HostMemory hm; shared_arena_file.empty() && detail::host_available_memory(hm)) { + const uint64_t need = want + (uint64_t) blob; + const double gib = 1073741824.0; + char buf[512]; + if (hm.cgroup_limit < need) { + std::snprintf(buf, sizeof buf, + "ArenaExpertSource: the expert arena needs %.2f GiB of RAM but this process's memory limit " + "(cgroup memory.max / memory.limit_in_bytes) is %.2f GiB: it would be killed while loading. " + "Raise the container's limit, or run with less RAM: --mmap-experts with " + "--resident-budget-gib N keeps only the hottest experts in RAM", + (double) need / gib, (double) hm.cgroup_limit / gib); + err = buf; + return false; + } + if (hm.available < need) { + std::snprintf(buf, sizeof buf, + "the expert arena needs %.2f GiB of RAM but %.2f GiB is available (%.2f GiB short): the load " + "may swap or be stopped by the OS. Close other programs (or wait for an engine that is " + "still exiting), or run with less RAM: --mmap-experts with --resident-budget-gib N", + (double) need / gib, (double) hm.available / gib, (double) (need - hm.available) / gib); + ram_warning_ = buf; + } + } + uint64_t pack_hash = 0; if (!shared_arena_file.empty() && !shared_arena_pack_hash(pack_dir, path, gguf_, lay, pack_hash, err)) return false; @@ -2643,7 +2838,7 @@ bool ArenaExpertSource::open(const std::string& pack_dir, int64_t n_layers, int6 return false; } // #285: unbuffered when the drive is read anyway and the file cache could not keep the experts for the next - // start either (a 64 GB PC); otherwise the buffered readers, which a warm restart serves from the cache. + // start either (a 64 GB PC); otherwise the buffered readers, which a warm restart serves from the cache std::vector files; if (from_gguf) { for (int64_t l = 0; l < n_layers; ++l) @@ -2656,20 +2851,19 @@ bool ArenaExpertSource::open(const std::string& pack_dir, int64_t n_layers, int6 } std::string why; const bool unbuffered = experts_unbuffered(files, want + (uint64_t) blob, why); - const int readers = unbuffered ? std::max(threads, 16) : threads; + const int readers = unbuffered ? std::max(threads, 16) : threads; // 16 keep a PCIe 5 drive's queue full if (from_gguf) { st = load_experts_gguf(gguf_, a->data(), lay, readers, unbuffered); } else { if (unbuffered) st = load_experts_direct(path, a->data(), loff, lbytes, readers, /*chunk=*/8u << 20); - if (!unbuffered || (!st.ok && st.error.empty())) + if (!unbuffered || (!st.ok && st.error.empty())) // unaligned ranges: the buffered reader st = load_experts_ranges(path, a->data(), loff, lbytes, threads, /*chunk=*/8u << 20); } - std::fprintf(stderr, "strata generate: expert arena read %s (%s)\n", - unbuffered ? "unbuffered" : "through the file cache", why.c_str()); + std::fprintf(stderr, "strata generate: expert arena read %s (%s)\n", unbuffered ? "unbuffered" : "through the file cache", + why.c_str()); if (!st.ok) { delete a; - err = "ArenaExpertSource: the expert load was refused: " + - (st.error.empty() ? std::string("unknown") : st.error); + err = "ArenaExpertSource: the expert load was refused: " + (st.error.empty() ? std::string("unknown") : st.error); return false; } if (st.bytes != want) { @@ -2677,6 +2871,31 @@ bool ArenaExpertSource::open(const std::string& pack_dir, int64_t n_layers, int6 err = "ArenaExpertSource: the load read " + std::to_string(st.bytes) + " B of " + std::to_string(want); return false; } + // the first start: write experts.bin for the mapped starts after this one - only when the drive has room for it + // and 2 GiB more (a full drive fails other writes too); otherwise this start says so and runs pinned as before + std::error_code space_ec; + const uint64_t free_disk = arena_mmap && from_gguf ? (uint64_t) std::filesystem::space( + std::filesystem::path(path).parent_path(), space_ec).available : 0; + const bool room = !space_ec && free_disk >= want + (uint64_t) blob + (2ull << 30); + if (arena_mmap && from_gguf && !room) + std::fprintf(stderr, "strata generate: STRATA_ARENA_MMAP: NOT writing %s - %.1f GiB free on that drive, it needs " + "%.1f GiB plus 2 GiB to spare; this start keeps the arena in RAM\n", path.c_str(), + (double) free_disk / 1073741824.0, (double) (want + (uint64_t) blob) / 1073741824.0); + if (arena_mmap && from_gguf && room) { + const std::string tmp = path + ".tmp"; + std::FILE* f = std::fopen(tmp.c_str(), "wb"); + bool ok = f != nullptr && std::fwrite(a->data(), 1, (size_t) want, f) == (size_t) want; + if (ok) { + std::vector pad((size_t) blob, 0); + ok = std::fwrite(pad.data(), 1, pad.size(), f) == pad.size(); + } + if (f) ok = std::fclose(f) == 0 && ok; + if (ok) ok = std::rename(tmp.c_str(), path.c_str()) == 0; + if (!ok) std::remove(tmp.c_str()); + std::fprintf(stderr, "strata generate: STRATA_ARENA_MMAP: %s %s (the next start maps it)\n", + ok ? "wrote" : "could NOT write", path.c_str()); + } + if (!shared_arena_file.empty() && !a->publish_shared_population()) { delete a; err = "ArenaExpertSource: could not publish shared arena population readiness"; @@ -2715,6 +2934,11 @@ bool ArenaExpertSource::open(const std::string& pack_dir, int64_t n_layers, int6 } void ArenaExpertSource::close() { +#if !defined(_WIN32) + if (map_ != nullptr) munmap(map_, (size_t) map_bytes_); +#endif + map_ = nullptr; + map_bytes_ = 0; if (arena_ != nullptr) { delete (PinnedArena*) arena_; arena_ = nullptr; @@ -2725,6 +2949,39 @@ void ArenaExpertSource::close() { reused_shared_population_ = false; } +void ArenaExpertSource::prefetch(int64_t layer, int64_t expert) { +#if defined(_WIN32) + (void) layer; (void) expert; +#else + if (map_ == nullptr || layer < 0 || expert < 0 || expert >= n_expert_) return; + const auto& lay = strata::kernels::cpu::expert_layout(); + const uint64_t off = lay.blob_offset(layer, expert) & ~(uint64_t) 4095; + const uint64_t end = lay.blob_offset(layer, expert) + lay.blob_bytes(layer); + madvise((uint8_t*) map_ + off, (size_t) (end - off), MADV_WILLNEED); +#endif +} + +// Only the pages wholly inside the blob: a page shared with a neighbour the CPU may still read stays. MADV_DONTNEED +// unmaps them from this process; they stay in the page cache as clean, unmapped pages - the first the kernel takes +// back under pressure, and a minor fault away when the CPU needs the expert again (an adaptive swap evicts it). +// Dropping them from the cache too (POSIX_FADV_DONTNEED) made every eviction a re-read from the disk: ~200 major +// faults a second in decode, +8 ms a window. The memory the arena used to hold was never the cache itself but +// ROCclr's pin-in-place locks on it (see main) - locked pages are the ones the kernel cannot take back. +uint64_t ArenaExpertSource::release(int64_t layer, int64_t expert) { +#if defined(_WIN32) + (void) layer; (void) expert; + return 0; +#else + if (map_ == nullptr || layer < 0 || expert < 0 || expert >= n_expert_) return 0; + const auto& lay = strata::kernels::cpu::expert_layout(); + const uint64_t off = (lay.blob_offset(layer, expert) + 4095) & ~(uint64_t) 4095; + const uint64_t end = (lay.blob_offset(layer, expert) + lay.blob_bytes(layer)) & ~(uint64_t) 4095; + if (end <= off) return 0; + if (madvise((uint8_t*) map_ + off, (size_t) (end - off), MADV_DONTNEED) != 0) return 0; + return end - off; +#endif +} + bool ArenaExpertSource::pinned(int64_t layer, int64_t expert) const { if (base_ == nullptr || layer < 0 || expert < 0 || expert >= n_expert_) return false; const auto& lay = strata::kernels::cpu::expert_layout(); diff --git a/src/core/layer.cpp b/src/core/layer.cpp index ff7fc17b58..33450f7d2f 100644 --- a/src/core/layer.cpp +++ b/src/core/layer.cpp @@ -387,6 +387,34 @@ if (db != nullptr && g_publish_kernel) { // write idempotent across replays of the same graph - a captured literal would ring the same number // forever and the host would never see a change. strata::kernels::doorbell_ring(db->d_seq, stream); } return true;} +// The verify window's routing: n tokens' logits in one BF16 projection and their top-k in one launch, each token +// bitwise what `moe_route` gives it (the projection is the same kernel per column, the top-k kernel is per token). +// Anything else (a native router, the canonical BF16 GEMVs, a doorbell) takes the per-token `moe_route`. +bool moe_route_window(const WeightTable& tables, const ModelGeometry& g, int64_t layer, int64_t k, const MoEBuffers& b, + const float* x, float* logits, int32_t* ids, float* weights, int n, void* stream, std::string& err) { + using namespace strata::kernels; + const bool batched = native_bf16_projections && !(native_router_enabled() && g.n_expert == 512 && k == 10) && + !std::getenv("STRATA_ROUTE_PER_TOKEN"); + if (!batched || n == 1) { + for (int t = 0; t < n; ++t) { + MoEBuffers mb = b; + mb.logits = logits + t * g.n_expert; mb.ids = ids + t * k; mb.weights = weights + t * k; + if (!moe_route(tables, g, layer, k, mb, x + t * g.n_embd, stream, err, nullptr)) return false; + } + return true; + } + const LayerView v(tables, layer); + const WeightRef* w_router = v.get("ffn_gate_inp.weight"); + if (w_router == nullptr) { err = v.name("ffn_gate_inp.weight") + " is missing"; return false; } + if (std::getenv("STRATA_ROUTE_PROJ_PER_TOKEN")) + for (int t = 0; t < n; ++t) + bf16_gemv_fp32_mmvf(x + t * g.n_embd, (const uint16_t*) w_router->data, logits + t * g.n_expert, g.n_embd, + g.n_expert, stream); + else + bf16_gemv_fp32_mmvf_cols(x, (const uint16_t*) w_router->data, logits, g.n_embd, g.n_expert, n, stream); + router_top10(logits, n, (int) g.n_expert, (int) k, ids, weights, stream); + return true; +} bool moe_shared(const WeightTable& tables, const ModelGeometry& g, int64_t layer, const MoEBuffers& b, const float* x, void* stream, std::string& err) { using namespace strata::kernels; const LayerView v(tables, layer); const WeightRef* w_ginp = v.get("ffn_gate_inp_shexp.weight"); const WeightRef* w_sgate = v.get("ffn_gate_shexp.weight"); const WeightRef* w_sup = v.get("ffn_up_shexp.weight"); const WeightRef* w_sdown = v.get("ffn_down_shexp.weight"); const char* missing = !w_ginp ? "ffn_gate_inp_shexp.weight" : !w_sgate ? "ffn_gate_shexp.weight" : !w_sup ? "ffn_up_shexp.weight" : !w_sdown ? "ffn_down_shexp.weight" : nullptr; if (missing) { err = v.name(missing) + " is missing"; return false; } // ---- the shared expert. Its three weights are quantized, so they need their planes. SForm f_gate, f_up, f_down; if (!sform_of(*w_sgate, f_gate, v.name("ffn_gate_shexp.weight"), err)) return false; if (!sform_of(*w_sup, f_up, v.name("ffn_up_shexp.weight"), err)) return false; if (!sform_of(*w_sdown, f_down, v.name("ffn_down_shexp.weight"), err)) return false; Planes p_gate, p_up, p_down; if (!plane_ptrs(*w_sgate, v.name("ffn_gate_shexp.weight"), p_gate, err)) return false; if (!plane_ptrs(*w_sup, v.name("ffn_up_shexp.weight"), p_up, err)) return false; if (!plane_ptrs(*w_sdown, v.name("ffn_down_shexp.weight"), p_down, err)) return false; diff --git a/src/core/mtp.cpp b/src/core/mtp.cpp index a53cc60a41..e9953e3002 100644 --- a/src/core/mtp.cpp +++ b/src/core/mtp.cpp @@ -26,6 +26,7 @@ #include "strata/kernels/sampler.hpp" #include "strata/kernels/shared_expert.hpp" #include "strata/kernels/verify_kernels.hpp" +#include "strata/kernels/ngram.hpp" #include #include @@ -37,6 +38,11 @@ #include #include #include +#if defined(_WIN32) +#include +#elif defined(__x86_64__) +#include +#endif namespace strata::core { namespace { @@ -146,6 +152,7 @@ bool MtpDrafter::load(const std::string& rt_dir, const ModelGeometry& g, Session cudaGetDevice(&device_); // a layer split's last stage on another GPU: the drafter lives there g_ = &g; ss_ = &ss; + if (ple_ss_ == nullptr) ple_ss_ = &ss; max_t_ = max_t; rt_dir_ = rt_dir; if (max_t < 1 || max_t > strata::kernels::kVerifyMaxT) { err = "mtp: max_t out of range"; return false; } @@ -580,8 +587,13 @@ bool MtpDrafter::record_forward(int T, int step_row0, cudaStream_t cs, std::stri nsw.down_type = GGML_Q8_0; nsw.down_data = q8("mlp.shared_expert.down_proj.weight"); nsw.q8_1 = xq_; const SForm none{}; + const bool need_bf16_x = !shared_expert_native_bf16_enabled(); + static const bool fuse_head_gr = [] { + const char* v = std::getenv("STRATA_FUSE_HEAD_GR"); + return v != nullptr && std::atoi(v) != 0; + }(); for (int t = 0; t < T; ++t) { - f32_to_bf16_bulk(mixed_ + t * N, x_bf16_, N, cs); + if (need_bf16_x) f32_to_bf16_bulk(mixed_ + t * N, x_bf16_, N, cs); shared_expert(nullptr, nullptr, x_bf16_, none, nullptr, nullptr, nullptr, none, nullptr, nullptr, nullptr, none, nullptr, nullptr, nullptr, bf16("mlp.shared_expert_gate.weight"), sh_scratch_, shared_ + t * N, N, g.n_ff, 32, cs, mixed_ + t * N, &nsw); @@ -589,14 +601,30 @@ bool MtpDrafter::record_forward(int T, int step_row0, cudaStream_t cs, std::stri native_moe_combine(parts_ + (size_t) t * K * N, w_ + t * K, shared_ + t * N, y_ + t * N, N, K, cs); else moe_combine(parts_ + (size_t) t * K * N, w_ + t * K, shared_ + t * N, y_ + t * N, N, K, cs); - gr_write(R_ + (size_t) t * HC * N, y_ + t * N, inj2_ + t * HC, gs, R_ + (size_t) t * HC * N, cs); + if (!fuse_head_gr) + gr_write(R_ + (size_t) t * HC * N, y_ + t * N, inj2_ + t * HC, gs, R_ + (size_t) t * HC * N, cs); } // ---- the final mixer and the main model's head - for (int t = 0; t < T; ++t) - gr_read(R_ + (size_t) t * HC * N, f32("hyper_connection_mixer.hc_norm.weight"), - bf16("hyper_connection_mixer.input_mix_weight_down.weight"), - bf16("hyper_connection_mixer.input_mix_weight_up.weight"), nullptr, EPS, gs, ss.block.gr, - sample_ + t * N, dummy_inj_, cs); + if (fuse_head_gr) { + FusedGrArgs fa[kFusedGrMaxT]; + for (int t = 0; t < T; ++t) { + fa[t].R = R_ + (size_t) t * HC * N; fa[t].R_out = R_ + (size_t) t * HC * N; fa[t].apply = true; + fa[t].bo_prev = y_ + t * N; fa[t].inj_prev = inj2_ + t * HC; + fa[t].w_norm = f32("hyper_connection_mixer.hc_norm.weight"); + fa[t].w_down = bf16("hyper_connection_mixer.input_mix_weight_down.weight"); + fa[t].w_up = bf16("hyper_connection_mixer.input_mix_weight_up.weight"); + fa[t].w_inject = nullptr; + fa[t].eps = EPS; fa[t].lo = lo_ + t * g.hc_lr; fa[t].rs = rs_ + t * HC; + fa[t].inject_out = dummy_inj_; fa[t].mixed = sample_ + t * N; + } + fused_gr_read_multi(fa, T, xn_, cs); + } else { + for (int t = 0; t < T; ++t) + gr_read(R_ + (size_t) t * HC * N, f32("hyper_connection_mixer.hc_norm.weight"), + bf16("hyper_connection_mixer.input_mix_weight_down.weight"), + bf16("hyper_connection_mixer.input_mix_weight_up.weight"), nullptr, EPS, gs, ss.block.gr, + sample_ + t * N, dummy_inj_, cs); + } native_quantize_q8_1(sample_, xq_, (int) N, T, cs); const bool sub = dhead_ != nullptr; const int64_t nv = sub ? n_dvocab_ : n_vocab_; @@ -829,6 +857,9 @@ bool MtpDrafter::draft(int T, const int32_t* tokens, int64_t p, int a, int32_t* if (T < 1 || T > max_t_ || a < 0 || a >= T) { err = "mtp: draft arguments out of range"; return false; } const bool cp = coupled_active_; // coupled draft sampling for this request: its own graphs if (!capture_round(T, cp, err)) return false; + const int max_steps = std::min(max_t_ - 1, max_drafts_); + for (int j = 1; j < max_steps; ++j) + if (!capture_step(j, cp, err)) return false; const Clock::time_point t0 = Clock::now(); const int64_t NH = g_->n_head; auto put = [&](int row, int64_t cell) { @@ -843,32 +874,99 @@ bool MtpDrafter::draft(int T, const int32_t* tokens, int64_t p, int a, int32_t* put(t, p + t); } put(2 * max_t_ - 1, coupled_draft_cell(p, a, 0)); // p + a: draft 0's cell + for (int j = 1; j < max_steps; ++j) + put(max_t_ + j - 1, coupled_draft_cell(p, a, j)); + for (int j = 0; j < max_steps; ++j) ((volatile int32_t*) h_out_)[j] = -1; h_row_[0] = a; h_row_[1] = 0; std::atomic_thread_fence(std::memory_order_seq_cst); - if (cudaGraphLaunch(cp ? round_exec_c_[T] : round_exec_[T], cs_) != cudaSuccess || - cudaStreamSynchronize(cs_) != cudaSuccess) { - err = std::string("mtp draft: ") + cudaGetErrorString(cudaGetLastError()); - return false; - } - drafts[0] = ((volatile int32_t*) h_out_)[0]; - float pj = ((volatile float*) h_prob_)[0]; - if (probs) probs[0] = pj; - int n = 1; - // the chain continues while the last draft is likely enough to be verified - for (int j = 1; j < std::min(max_t_ - 1, max_drafts_) && pj >= min_p; ++j) { - if (!capture_step(j, cp, err)) return false; - put(max_t_ + j - 1, coupled_draft_cell(p, a, j)); // p + a + j; coupled: drawn at counter cell + 1 - std::atomic_thread_fence(std::memory_order_seq_cst); - if (cudaGraphLaunch(cp ? step_exec_c_[j] : step_exec_[j], cs_) != cudaSuccess || - cudaStreamSynchronize(cs_) != cudaSuccess) { - err = std::string("mtp draft step: ") + cudaGetErrorString(cudaGetLastError()); + + SessionState* const pss = ple_ss_ ? ple_ss_ : ss_; + const bool do_ple = pss != nullptr && pss->ple.ready() && pss->ple.table != nullptr; + int32_t ple_prev[2] = {do_ple ? pss->ple_prev[0] : 0, do_ple ? pss->ple_prev[1] : 0}; + auto prefetch_ple = [&](int32_t tok) { + if (!do_ple || tok < 0) return; + uint32_t rows16[strata::kernels::PLE_N_HEADS]; + strata::kernels::ngram_rows(&tok, ple_prev, 1, pss->ple.consts, rows16); + ple_prev[0] = ple_prev[1]; + ple_prev[1] = tok; + pss->ple.table->prefetch_rows(rows16); + }; + + int n = 0; + if (min_p <= 0.0f && max_steps > 0) { + if (cudaGraphLaunch(cp ? round_exec_c_[T] : round_exec_[T], cs_) != cudaSuccess) { + err = std::string("mtp draft: ") + cudaGetErrorString(cudaGetLastError()); + return false; + } + for (int j = 1; j < max_steps; ++j) { + if (cudaGraphLaunch(cp ? step_exec_c_[j] : step_exec_[j], cs_) != cudaSuccess) { + err = std::string("mtp draft step: ") + cudaGetErrorString(cudaGetLastError()); + return false; + } + } + (void) cudaStreamQuery(cs_); + prefetch_ple(tokens[a]); + for (int j = 0; j + 1 < max_steps; ++j) { + uint32_t spins = 0; + while (((volatile int32_t*) h_out_)[j] < 0) { +#if defined(_WIN32) || defined(__x86_64__) + _mm_pause(); +#endif + if ((++spins & 1023u) == 0 && cudaStreamQuery(cs_) != cudaErrorNotReady) break; + } + drafts[j] = ((volatile int32_t*) h_out_)[j]; + if (probs) probs[j] = ((volatile float*) h_prob_)[j]; + prefetch_ple(drafts[j]); + } + if (cudaStreamSynchronize(cs_) != cudaSuccess) { + err = std::string("mtp draft: ") + cudaGetErrorString(cudaGetLastError()); + return false; + } + const int last = max_steps - 1; + drafts[last] = ((volatile int32_t*) h_out_)[last]; + if (probs) probs[last] = ((volatile float*) h_prob_)[last]; + prefetch_ple(drafts[last]); + n = max_steps; + } else if (max_steps > 0) { + if (cudaGraphLaunch(cp ? round_exec_c_[T] : round_exec_[T], cs_) != cudaSuccess) { + err = std::string("mtp draft: ") + cudaGetErrorString(cudaGetLastError()); + return false; + } + (void) cudaStreamQuery(cs_); + prefetch_ple(tokens[a]); + auto wait_step = [&](int j) { + uint32_t spins = 0; + while (((volatile int32_t*) h_out_)[j] < 0) { +#if defined(_WIN32) || defined(__x86_64__) + _mm_pause(); +#endif + if ((++spins & 1023u) == 0 && cudaStreamQuery(cs_) != cudaErrorNotReady) break; + } + }; + wait_step(0); + drafts[0] = ((volatile int32_t*) h_out_)[0]; + float pj = ((volatile float*) h_prob_)[0]; + if (probs) probs[0] = pj; + n = 1; + for (int j = 1; j < max_steps && pj >= min_p; ++j) { + if (cudaGraphLaunch(cp ? step_exec_c_[j] : step_exec_[j], cs_) != cudaSuccess) { + err = std::string("mtp draft step: ") + cudaGetErrorString(cudaGetLastError()); + return false; + } + (void) cudaStreamQuery(cs_); + prefetch_ple(drafts[j - 1]); + wait_step(j); + drafts[j] = ((volatile int32_t*) h_out_)[j]; + pj = ((volatile float*) h_prob_)[j]; + if (probs) probs[j] = pj; + ++n; + } + prefetch_ple(drafts[n - 1]); + if (cudaStreamSynchronize(cs_) != cudaSuccess) { + err = std::string("mtp draft: ") + cudaGetErrorString(cudaGetLastError()); return false; } - drafts[j] = ((volatile int32_t*) h_out_)[j]; - pj = ((volatile float*) h_prob_)[j]; - if (probs) probs[j] = pj; - ++n; } for (int j = n; j < max_t_ - 1; ++j) { drafts[j] = 0; if (probs) probs[j] = 0.0f; } if (n_drafts) *n_drafts = n; diff --git a/src/core/native_dense.cpp b/src/core/native_dense.cpp index 4a86d48c21..e0609fdc5a 100644 --- a/src/core/native_dense.cpp +++ b/src/core/native_dense.cpp @@ -1,4 +1,5 @@ #include "strata/core/native_dense.hpp" +#include #include "strata/core/weights.hpp" #include "strata/artifact/gguf_reader.hpp" #include "strata/kernels/native_mmvq.hpp" @@ -6,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +15,12 @@ namespace strata::core { namespace { +int g_layer_lb = -1, g_layer_le = -1; // set_layer_range; -1: every layer +bool in_range(const std::string& name) { + if (g_layer_lb < 0 || name.rfind("blk.", 0) != 0) return true; + const int l = std::atoi(name.c_str() + 4); + return l >= g_layer_lb && l < g_layer_le; +} bool eligible(const strata::TensorInfo& tensor, bool include_ple_key) { const auto& name = tensor.name; if (name.rfind("blk.", 0) != 0) return false; @@ -53,6 +61,7 @@ bool NativeDense::served_names(const std::vector& shards, bool incl } } +void NativeDense::set_layer_range(int lb, int le) { g_layer_lb = lb; g_layer_le = le; } bool NativeDense::keep_unquantized_ple_key(const std::string& pack_dir, std::set& skip, std::string& err) { const std::string key = "blk.1.ple_key.weight"; @@ -69,7 +78,12 @@ NativeDense::~NativeDense() { } bool NativeDense::load(const std::vector& shards, WeightTable& table, std::string& err, - bool include_ple_key) { + bool include_ple_key, int64_t layer_lo, int64_t layer_hi) { + auto outside = [&](const std::string& name) { // a blk.. tensor of another stage's layers + if (layer_hi < 0 || name.rfind("blk.", 0) != 0) return false; + const long l = std::strtol(name.c_str() + 4, nullptr, 10); + return l < layer_lo || l >= layer_hi; + }; if (scratch_ || !weights_.empty()) { err = "native dense: already loaded"; return false; } if (shards.empty()) { err = "native dense: at least one GGUF shard is required"; return false; } try { @@ -138,7 +152,8 @@ bool NativeDense::load(const std::vector& shards, WeightTable& tabl } } for (const auto& tensor : gguf.tensors()) { - if (!eligible(tensor, include_ple_key)) continue; + if (!eligible(tensor, include_ple_key) || outside(tensor.name)) continue; + if (!in_range(tensor.name) && tensor.name.find("ple") == std::string::npos) continue; if (!seen.insert(tensor.name).second) { err = "native dense: duplicate tensor " + tensor.name; return false; } diff --git a/src/core/pinned.cu b/src/core/pinned.cu index 8750adf96b..83e9b8adcf 100644 --- a/src/core/pinned.cu +++ b/src/core/pinned.cu @@ -225,9 +225,27 @@ void* reserve(uint64_t bytes, PageBacking& got, std::string& note, const std::st note = "MAP_HUGETLB unavailable (needed " + std::to_string(need) + " 2 MiB pages, vm.nr_hugepages=" + (have_pool ? std::to_string(pool) : std::string("?")) + "); using 4 KB pages"; } - void* p = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + // No hugetlb mapping: a 2 MiB-aligned anonymous mapping with MADV_HUGEPAGE, so transparent huge pages back + // it where THP is "madvise" (the common distro default). The CPU expert pool streams whole experts out of + // this arena; with 4 KB pages every 3 MB expert costs ~750 TLB misses. + constexpr uint64_t kAlign = 2ull << 20; + const uint64_t padded = bytes + kAlign; + void* raw = mmap(nullptr, padded, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); got = PageBacking::NormalPages; - return p == MAP_FAILED ? nullptr : p; + if (raw == MAP_FAILED) return nullptr; + const uintptr_t start = (uintptr_t) raw; + const uintptr_t aligned = (start + kAlign - 1) & ~(uintptr_t) (kAlign - 1); + if (aligned > start) munmap(raw, aligned - start); + const uintptr_t end = aligned + bytes, raw_end = start + padded; + if (raw_end > end) munmap((void*) end, raw_end - end); + void* p = (void*) aligned; + if (std::getenv("STRATA_NO_LARGEPAGES") == nullptr && madvise(p, bytes, MADV_HUGEPAGE) == 0) { + const std::string four_k = "; using 4 KB pages"; + if (note.size() >= four_k.size() && note.compare(note.size() - four_k.size(), four_k.size(), four_k) == 0) + note.resize(note.size() - four_k.size()); + note += "; transparent huge pages requested (MADV_HUGEPAGE)"; + } + return p; #endif } @@ -486,7 +504,7 @@ LoadStats load_experts_direct(const std::string& path, uint8_t* dst, const std:: std::mutex err_mu; std::string err; const int wide = MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, nullptr, 0); - std::vector wpath((size_t) std::max(wide, 1), L'\0'); + std::vector wpath((size_t) (std::max)(wide, 1), L'\0'); if (wide > 0) MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, wpath.data(), wide); auto worker = [&]() { HANDLE h = CreateFileW(wpath.data(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, @@ -537,7 +555,7 @@ LoadStats load_experts_direct(const std::string& path, uint8_t* dst, const std:: } bool experts_unbuffered(const std::vector& files, uint64_t arena_bytes, std::string& why, - bool cache_counts) { + bool cache_counts, uint64_t read_bytes) { const char* env = std::getenv("STRATA_UNBUFFERED_LOAD"); if (env != nullptr && env[0] != '\0') { why = std::string("STRATA_UNBUFFERED_LOAD=") + env; @@ -554,7 +572,7 @@ bool experts_unbuffered(const std::vector& files, uint64_t arena_by uint64_t seed = (uint64_t) GetTickCount64() * 6364136223846793005ull + 1442695040888963407ull; for (const std::string& f : files) { const int wide = MultiByteToWideChar(CP_UTF8, 0, f.c_str(), -1, nullptr, 0); - std::vector w((size_t) std::max(wide, 1), L'\0'); + std::vector w((size_t) (std::max)(wide, 1), L'\0'); if (wide > 0) MultiByteToWideChar(CP_UTF8, 0, f.c_str(), -1, w.data(), wide); HANDLE h = CreateFileW(w.data(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_RANDOM_ACCESS, nullptr); @@ -585,15 +603,21 @@ bool experts_unbuffered(const std::vector& files, uint64_t arena_by GlobalMemoryStatusEx(&ms); const uint64_t avail = ms.ullAvailPhys; // what the cache could keep beside the arena (~4 GiB for everything else) - const uint64_t room = avail > arena_bytes + (4ull << 30) ? avail - arena_bytes - (4ull << 30) : 0; - const bool keepable = room >= total_bytes; - char msg[200]; - std::snprintf(msg, sizeof msg, "%d of %d probe reads from the file cache; %.1f GiB available, %.1f GiB of files", - fast, n, (double) avail / (1ull << 30), (double) total_bytes / (1ull << 30)); + const uint64_t need = read_bytes == kAllFileBytes ? total_bytes : read_bytes; + const bool keepable = strata::platform::file_cache_keeps(avail, arena_bytes, need); + char msg[256]; + if (read_bytes == kAllFileBytes) + std::snprintf(msg, sizeof msg, "%d of %d probe reads from the file cache; %.1f GiB available, %.1f GiB of files", + fast, n, (double) avail / (1ull << 30), (double) total_bytes / (1ull << 30)); + else + std::snprintf(msg, sizeof msg, "%.1f GiB available, %.1f GiB of it still to be taken by the RAM copy, %.1f GiB " + "of experts read from the files: the file cache %s keep them", + (double) avail / (1ull << 30), (double) arena_bytes / (1ull << 30), (double) need / (1ull << 30), + keepable ? "can" : "cannot"); why = msg; return (!cached || !cache_counts) && !keepable; #else - (void) files; (void) arena_bytes; (void) cache_counts; + (void) files; (void) arena_bytes; (void) cache_counts; (void) read_bytes; why = "buffered (not Windows)"; return false; #endif diff --git a/src/core/remote_expert_opt.cu b/src/core/remote_expert_opt.cu new file mode 100644 index 0000000000..229781c805 --- /dev/null +++ b/src/core/remote_expert_opt.cu @@ -0,0 +1,194 @@ +#include "strata/core/remote_expert_opt.hpp" +#include "strata/core/remote_experts.hpp" +#include "strata/core/on_device.hpp" +#include "strata/kernels/cpu/expert_layout.hpp" +#include "strata/kernels/elementwise.hpp" +#include "strata/kernels/verify_kernels.hpp" + +#include +#include + +namespace strata::core { +namespace { +constexpr int64_t H = strata::kernels::cpu::H; +constexpr int K = 10, MAXT = strata::kernels::cpu::MAXT, CAP = MAXT * K; +struct ReduceMeta { int32_t row[CAP]; float weight[CAP]; }; + +bool check(cudaError_t status, std::string& err) { + if (status == cudaSuccess) return true; + err = std::string("remote expert decode optimization: ") + cudaGetErrorString(status); + return false; +} + +__global__ void reduce_experts(const float* parts, const ReduceMeta* meta, float* sum) { + const int t = blockIdx.y, c = blockIdx.x * blockDim.x + threadIdx.x; + if (c >= H) return; + float v = 0.0f; + for (int j = 0; j < K; ++j) { + const int i = t * K + j, r = meta->row[i]; + if (r >= 0) v += parts[(int64_t) r * H + c] * meta->weight[i]; + } + sum[(int64_t) t * H + c] = v; +} + +// Like the upstream mapped-row copy, also skip rows returned in the remote sum. +__global__ void copy_cpu_rows(float4* dst, const volatile float4* src, const int32_t* mask, + const int32_t* hit_rows, const int32_t* count) { + const int row = blockIdx.x; + __shared__ int skip; + if (threadIdx.x == 0) { + int s = mask[row]; + for (int i = 0; i < *count; ++i) s |= hit_rows[i] == row; + skip = s; + } + __syncthreads(); + const int64_t start = (int64_t) row * (H / 4); + for (int i = threadIdx.x; i < H / 4; i += blockDim.x) + dst[start + i] = skip ? make_float4(0, 0, 0, 0) : const_cast(src)[start + i]; +} +} // namespace + +void RemoteExpertOpt::attach(RemoteExperts& remote) { + remote.remote_opt_ = this; + peers_.push_back({&remote}); +} + +bool RemoteExpertOpt::init(std::string& err) { + void* mapped = nullptr; + if (!check(cudaHostAlloc((void**) &h_sum_, MAXT * (H + K) * sizeof(float), + cudaHostAllocPortable | cudaHostAllocMapped), err) || + !check(cudaHostGetDevicePointer(&mapped, h_sum_, 0), err)) return false; + m_sum_ = (float*) mapped; + h_mask_ = reinterpret_cast(h_sum_ + MAXT * H); + m_mask_ = reinterpret_cast(m_sum_ + MAXT * H); + for (auto& p : peers_) { + const OnDevice on(p.remote->device_); + if (!check(cudaMalloc((void**) &p.sum, MAXT * H * sizeof(float)), err)) return false; + } + return true; +} + +RemoteExpertOpt::~RemoteExpertOpt() { + for (auto& p : peers_) { + const OnDevice on(p.remote->device_); + if (p.remote->stream_) cudaStreamSynchronize(p.remote->stream_); + if (p.sum) cudaFree(p.sum); + p.remote->remote_opt_ = nullptr; + } + if (h_sum_) cudaFreeHost(h_sum_); +} + +size_t RemoteExpertOpt::metadata_bytes() { return sizeof(ReduceMeta); } + +void RemoteExpertOpt::begin(const float* weights, int token_begin, int tokens) { + weights_ = weights; + token_begin_ = token_begin; + tokens_ = tokens; + std::memset(h_sum_ + token_begin * H, 0, tokens * H * sizeof(float)); + std::memset(h_mask_ + token_begin * K, 0, tokens * K * sizeof(int32_t)); +} + +void RemoteExpertOpt::prepare(const RemoteExperts& remote, void* metadata) const { + auto* m = (ReduceMeta*) metadata; + std::fill(m->row, m->row + tokens_ * K, -1); + for (size_t i = 0; i < remote.original_row_.size(); ++i) + m->row[remote.original_row_[i]] = (int32_t) i; + std::memcpy(m->weight, weights_, tokens_ * K * sizeof(float)); +} + +bool RemoteExpertOpt::reduce(RemoteExperts& remote, const void* metadata, std::string& err) { + const auto& p = *std::find_if(peers_.begin(), peers_.end(), [&](const Peer& p) { return p.remote == &remote; }); + reduce_experts<<>>( + remote.d_out_, (const ReduceMeta*) metadata, p.sum); + return check(cudaMemcpyAsync(remote.h_out_, p.sum, tokens_ * H * sizeof(float), + cudaMemcpyDeviceToHost, remote.stream_), err); +} + +void RemoteExpertOpt::accumulate(const RemoteExperts& remote) { + for (int64_t i = 0; i < tokens_ * H; ++i) h_sum_[token_begin_ * H + i] += remote.h_out_[i]; + for (int i = 0; i < tokens_ * K; ++i) + h_mask_[token_begin_ * K + i] |= remote.owned_[(size_t) i] != 0; +} + +void RemoteExpertOpt::copy_rows(float* dst, const float* src, int token_begin, int tokens, + const int32_t* hit_rows, const int32_t* count, void* stream) const { + copy_cpu_rows<<>>( + (float4*) dst, (const volatile float4*) src, m_mask_ + token_begin * K, hit_rows, count); +} + +void RemoteExpertOpt::combine(float* dst, float* scratch, int token_begin, int tokens, + const uint32_t* skip, uint32_t ring, void* stream) const { + const float* sum = m_sum_ + token_begin * H; + if (skip) { + // An all-primary device-planned group bypasses the host: ignore its stale sum. + strata::kernels::copy_or_zero_from_mapped(scratch, sum, tokens * H, skip, ring, stream); + sum = scratch; + } + strata::kernels::add_inplace(dst, sum, tokens * H, stream); +} + +bool RemoteExpertOpt::owns(int64_t layer, int32_t expert) const { + for (const auto& p : peers_) + if (p.remote->cache_.slot_of(layer, expert) >= 0) return true; + return false; +} + +bool RemoteExpertOpt::adapt(const std::vector& usage, const std::vector& primary, + const std::vector>& pending, int max_swaps, + ExpertSource& source) { + if (max_swaps <= 0) return true; + const auto& lay = strata::kernels::cpu::expert_layout(); + std::vector resident(primary.size(), 0); + for (size_t i = 0; i < primary.size(); ++i) resident[i] = primary[i] >= 0; + for (const auto& [i, slot] : pending) resident[(size_t) i] = 1; + for (const auto& p : peers_) { + const auto& r = *p.remote; + for (int32_t l = 0; l < (int32_t) r.layers_present_.size(); ++l) + for (int32_t e = 0; e < r.n_expert_; ++e) + if (r.cache_.slot_of(l, e) >= 0) resident[(size_t) l * r.n_expert_ + e] = 1; + } + struct Swap { float gain; int32_t layer, in, out; }; + std::string err; + for (auto& p : peers_) { + auto& r = *p.remote; + std::vector swaps; + std::vector> candidates, victims; + for (int32_t l = 0; l < (int32_t) r.layers_present_.size(); ++l) { + candidates.clear(); victims.clear(); + const size_t base = (size_t) l * r.n_expert_; + float primary_cold = 1.0e30f; + for (int32_t e = 0; e < r.n_expert_; ++e) { + const float u = usage[base + e]; + if (primary[base + e] >= 0) primary_cold = std::min(primary_cold, u); + if (r.cache_.slot_of(l, e) >= 0) victims.emplace_back(u, e); + else if (!resident[base + e] && u >= 2.0f) candidates.emplace_back(u, e); + } + const size_t n = std::min(candidates.size(), victims.size()); + if (!n) continue; + std::sort(candidates.begin(), candidates.end(), [](auto& a, auto& b) { return a.first > b.first; }); + std::partial_sort(victims.begin(), victims.begin() + n, victims.end(), + [](auto& a, auto& b) { return a.first < b.first; }); + for (size_t i = 0; i < n; ++i) { + if (victims[i].first > primary_cold) break; + if (candidates[i].first < victims[i].first + 1.5f) break; + swaps.push_back({candidates[i].first - victims[i].first, l, candidates[i].second, victims[i].second}); + } + } + std::sort(swaps.begin(), swaps.end(), [](auto& a, auto& b) { return a.gain > b.gain; }); + if ((int) swaps.size() > max_swaps) swaps.resize((size_t) max_swaps); + if (swaps.empty()) continue; + const OnDevice on(r.device_); + for (const auto& s : swaps) + if (!r.cache_.fill_slot(r.cache_.slot_of(s.layer, s.out), source.blob(s.layer, s.in), + r.stream_, err, (int64_t) lay.blob_bytes(s.layer))) return false; + if (!check(cudaStreamSynchronize(r.stream_), err)) return false; + for (const auto& s : swaps) { + r.cache_.replace(s.layer, s.out, s.in); + const size_t base = (size_t) s.layer * r.n_expert_; + resident[base + s.out] = 0; + resident[base + s.in] = 1; + } + } + return true; +} +} // namespace strata::core diff --git a/src/core/remote_experts.cpp b/src/core/remote_experts.cpp index 53d8d7643c..1b6969e828 100644 --- a/src/core/remote_experts.cpp +++ b/src/core/remote_experts.cpp @@ -1,4 +1,5 @@ #include "strata/core/remote_experts.hpp" +#include "strata/core/remote_expert_opt.hpp" #include "strata/kernels/cpu/expert_layout.hpp" #include "strata/kernels/iq_kernels.hpp" @@ -120,7 +121,7 @@ void RemoteExperts::close() { bool RemoteExperts::open(int device, int slots, int64_t layers, int64_t experts, const std::vector>& ranked, const ExpertCache& primary, ExpertSource& source, - std::vector& claimed, std::string& err) { + std::vector& claimed, std::string& err, bool auto_size) { close(); int count = 0; if (!check(cudaGetDeviceCount(&count), "cudaGetDeviceCount", err, device)) return false; @@ -134,29 +135,34 @@ bool RemoteExperts::open(int device, int slots, int64_t layers, int64_t experts, device_ = device; n_expert_ = experts; const auto& lay = strata::kernels::cpu::expert_layout(); + size_t free_bytes = 0, total_bytes = 0; + if (!check(cudaMemGetInfo(&free_bytes, &total_bytes), "free memory", err, device)) { close(); return false; } + uint64_t needed = 0; std::vector> selected; - selected.reserve((size_t) slots); + selected.reserve((size_t) std::min(slots, layers * experts)); std::vector picked(claimed.size(), 0); for (const auto& pair : ranked) { if (pair.first < 0 || pair.first >= layers || pair.second < 0 || pair.second >= experts) continue; const size_t index = (size_t) pair.first * (size_t) experts + (size_t) pair.second; if (primary.slot_of(pair.first, pair.second) < 0 && !claimed[index] && !picked[index]) { + const uint64_t bytes = lay.native ? (lay.blob_bytes(pair.first) + 255) / 256 * 256 : lay.max_blob; + if (auto_size && needed + bytes + (512ull << 20) > free_bytes) break; selected.push_back(pair); + needed += bytes; picked[index] = 1; if ((int) selected.size() >= slots) break; } } - if (selected.empty()) { err = "CUDA" + std::to_string(device) + " experts: no unclaimed experts remain"; close(); return false; } + if (selected.empty()) { + err = "CUDA" + std::to_string(device) + (auto_size ? " experts: no unclaimed expert fits with 512 MiB free" + : " experts: no unclaimed experts remain"); + close(); return false; + } std::vector sizes; if (lay.native) { sizes.reserve(selected.size()); for (const auto& pair : selected) sizes.push_back((int64_t) lay.blob_bytes(pair.first)); } - size_t free_bytes = 0, total_bytes = 0; - if (!check(cudaMemGetInfo(&free_bytes, &total_bytes), "free memory", err, device)) { close(); return false; } - uint64_t needed = 0; - for (const auto& pair : selected) - needed += lay.native ? (lay.blob_bytes(pair.first) + 255) / 256 * 256 : lay.max_blob; // Leave room for the CUDA context, staging and later driver allocations, especially under WDDM. if (needed + (512ull << 20) > free_bytes) { err = "CUDA" + std::to_string(device) + " experts: slots leave less than 512 MiB free; reduce --expert-cache-device" + std::to_string(device); @@ -185,17 +191,18 @@ bool RemoteExperts::open(int device, int slots, int64_t layers, int64_t experts, const size_t scratch = std::max( (size_t) strata::kernels::moe_hit_grouped_scratch_bytes(CAP, H, FF), strata::kernels::native_expert_scratch_bytes(CAP, FF)); + const size_t meta_bytes = sizeof(RemoteMeta) + (remote_opt_ ? remote_opt_->metadata_bytes() : 0); const bool allocated = check(cudaStreamCreateWithFlags(&stream_, cudaStreamNonBlocking), "stream", err, device) && check(cudaHostAlloc((void**) &h_x_, (size_t) CAP * H * sizeof(float), cudaHostAllocPortable | cudaHostAllocMapped), "input staging", err, device) && check(cudaHostAlloc((void**) &h_out_, (size_t) CAP * H * sizeof(float), cudaHostAllocPortable | cudaHostAllocMapped), "result staging", err, device) && - check(cudaHostAlloc(&h_meta_, sizeof(RemoteMeta), cudaHostAllocPortable), "metadata staging", err, device) && + check(cudaHostAlloc(&h_meta_, meta_bytes, cudaHostAllocPortable), "metadata staging", err, device) && check(cudaMalloc((void**) &d_x_, (size_t) CAP * H * sizeof(float)), "input", err, device) && check(cudaMalloc((void**) &d_out_, (size_t) CAP * H * sizeof(float)), "result", err, device) && check(cudaMalloc((void**) &d_q8_, (size_t) CAP * (H / 32) * 36), "activation", err, device) && check(cudaMalloc((void**) &d_scales_, (size_t) CAP * (H / 32) * sizeof(float)), "activation scales", err, device) && check(cudaMalloc(&d_scratch_, scratch), "scratch", err, device) && - check(cudaMalloc(&d_meta_, sizeof(RemoteMeta)), "group metadata", err, device); + check(cudaMalloc(&d_meta_, meta_bytes), "group metadata", err, device); if (!allocated) { close(); return false; } // Zero-copy: the helper reads its input from, and writes its compact rows into, the pinned host buffers // directly - two copies fewer per layer, each of which is a PCIe round trip. STRATA_REMOTE_ZEROCOPY=0 copies. @@ -284,13 +291,16 @@ bool RemoteExperts::begin(int64_t layer, const float* x, const int32_t* ids, int std::memcpy(meta->dst, dst_.data(), dst_.size() * sizeof(dst_[0])); std::memcpy(meta->tok, tok_.data(), tok_.size() * sizeof(tok_[0])); meta->count = (int32_t) group_id_.size(); + const bool reduce = remote_opt_ && remote_opt_->active(); + if (reduce) remote_opt_->prepare(*this, meta + 1); DeviceScope scope(device_); if (!scope.ok) { err = scope.error(device_); return false; } groups_ = (int32_t) group_id_.size(); const cudaStream_t s = stream_; const bool staged = (zero_copy_ || check(cudaMemcpyAsync(d_x_, h_x_, (size_t) n_tok * H * sizeof(float), cudaMemcpyHostToDevice, s), "copy input", err, device_)) && - check(cudaMemcpyAsync(d_meta_, h_meta_, sizeof(RemoteMeta), cudaMemcpyHostToDevice, s), "copy group metadata", err, device_); + check(cudaMemcpyAsync(d_meta_, h_meta_, sizeof(RemoteMeta) + (reduce ? remote_opt_->metadata_bytes() : 0), + cudaMemcpyHostToDevice, s), "copy group metadata", err, device_); if (!staged) return false; const auto& lay = strata::kernels::cpu::expert_layout(); if (lay.native) { @@ -298,14 +308,16 @@ bool RemoteExperts::begin(int64_t layer, const float* x, const int32_t* ids, int const auto& fmt = lay.fmt[(size_t) layer]; auto L = strata::kernels::native_expert_layout(fmt.gu_type, fmt.d_type, fmt.n_embd, fmt.n_ff); strata::kernels::native_expert_grouped(L, d_ptr_, d_start_, d_count_, d_dst_, d_tok_, - groups_, (int64_t) dst_.size(), d_q8_, d_scratch_, zero_copy_ ? z_out_ : d_out_, s); + groups_, (int64_t) dst_.size(), d_q8_, d_scratch_, zero_copy_ && !reduce ? z_out_ : d_out_, s); } else { strata::kernels::quantize_q8_0_scaled(zero_copy_ ? z_x_ : d_x_, d_q8_, d_scales_, n_tok * H, s); strata::kernels::moe_grouped_s2(d_ptr_, d_start_, d_count_, d_dst_, d_tok_, - groups_, (int64_t) dst_.size(), d_q8_, d_scales_, d_scratch_, zero_copy_ ? z_out_ : d_out_, s); + groups_, (int64_t) dst_.size(), d_q8_, d_scales_, d_scratch_, zero_copy_ && !reduce ? z_out_ : d_out_, s); } - const uint64_t compact_bytes = (uint64_t) dst_.size() * H * sizeof(float); - if (!zero_copy_ && !check(cudaMemcpyAsync(h_out_, d_out_, (size_t) compact_bytes, cudaMemcpyDeviceToHost, s), + const uint64_t compact_bytes = (uint64_t) (reduce ? n_tok : dst_.size()) * H * sizeof(float); + if (reduce) { + if (!remote_opt_->reduce(*this, (RemoteMeta*) d_meta_ + 1, err)) return false; + } else if (!zero_copy_ && !check(cudaMemcpyAsync(h_out_, d_out_, (size_t) compact_bytes, cudaMemcpyDeviceToHost, s), "copy results", err, device_)) return false; ++launched_layers_; returned_bytes_ += compact_bytes; @@ -320,6 +332,7 @@ bool RemoteExperts::finish(float* out, std::string& err) { const auto w0 = std::chrono::steady_clock::now(); if (!check(cudaStreamSynchronize(stream_), "finish", err, device_)) return false; ms_wait_ += std::chrono::duration(std::chrono::steady_clock::now() - w0).count(); + if (remote_opt_ && remote_opt_->active()) { remote_opt_->accumulate(*this); return true; } for (size_t i = 0; i < original_row_.size(); ++i) std::memcpy(out + (size_t) original_row_[i] * H, h_out_ + i * H, (size_t) H * sizeof(float)); return true; diff --git a/src/core/session.cpp b/src/core/session.cpp index 14c61663bd..40a44bd126 100644 --- a/src/core/session.cpp +++ b/src/core/session.cpp @@ -547,7 +547,7 @@ bool SessionLoopScratch::init(size_t parts_bytes_in, std::string& err) { const std::vector cores = strata::kernels::cpu::physical_cores(false); if (!cores.empty()) { pinned_core = strata::kernels::cpu::pin_current_thread(cores[0]); - pinned = true; + pinned = pinned_core.valid; } return true; } @@ -558,7 +558,7 @@ void SessionLoopScratch::free() { if (pinned) { strata::kernels::cpu::restore_thread_affinity(pinned_core); pinned = false; - pinned_core = -1; + pinned_core = {}; } if (probe != nullptr) { cudaEventDestroy(probe); probe = nullptr; } if (y_miss != nullptr) { cudaFreeHost(y_miss); y_miss = nullptr; } diff --git a/src/core/verify.cpp b/src/core/verify.cpp index f956d73158..dd5362bacd 100644 --- a/src/core/verify.cpp +++ b/src/core/verify.cpp @@ -1,5 +1,6 @@ // src/core/verify.cpp - see include/strata/core/verify.hpp. #include "strata/core/verify.hpp" +#include "strata/core/remote_expert_opt.hpp" #if defined(_WIN32) #include #endif @@ -69,7 +70,29 @@ struct Bump { } }; +// #649 (HIP, opt-in A/Bs for the gfx1030 verify timeouts; CUDA never reads them) +// STRATA_VERIFY_COHERENT=1 the handshake words and rows in explicitly coherent (fine-grained) host memory +// STRATA_DOORBELL_STORE=1 the GPU stores each step's ring instead of read-modify-writing it over PCIe +bool env_on(const char* name) { + const char* e = std::getenv(name); + return e != nullptr && e[0] != 0 && e[0] != '0'; +} +#if defined(STRATA_USE_HIP) +const bool g_coherent = env_on("STRATA_VERIFY_COHERENT"); +const bool g_doorbell_store = env_on("STRATA_DOORBELL_STORE"); +#endif +const bool g_trace = env_on("STRATA_VERIFY_TRACE"); + bool mapped(size_t bytes, void** h, void** d) { +#if defined(STRATA_USE_HIP) + if (g_coherent) { + if (hipHostMalloc(h, bytes, hipHostMallocMapped | hipHostMallocCoherent | + (peer_portable() ? hipHostMallocPortable : 0)) != hipSuccess) + return false; + std::memset(*h, 0, bytes); + return cudaHostGetDevicePointer(d, *h, 0) == cudaSuccess; + } +#endif if (cudaHostAlloc(h, bytes, cudaHostAllocMapped | (peer_portable() ? cudaHostAllocPortable : 0)) != cudaSuccess) return false; // multi-GPU: portable only with a peer - the peer card writes its rows into them std::memset(*h, 0, bytes); return cudaHostGetDevicePointer(d, *h, 0) == cudaSuccess; @@ -122,6 +145,20 @@ void release_live_verifiers(std::FILE* f) { } if (f != nullptr) std::fflush(f); } +// #649: the host side of STRATA_VERIFY_TRACE - a ring of the last kTraceN handshake events of every verifier +struct TraceEv { + int64_t t_ns; + const void* who; + int64_t window, step, layer, aux; + uint32_t seq, flag, a, b; + const char* what; +}; +constexpr uint64_t kTraceN = 4096; +TraceEv g_trace_ring[kTraceN]; +std::atomic g_trace_next{0}; +int64_t trace_now_ns() { + return std::chrono::duration_cast(Clock::now().time_since_epoch()).count(); +} std::string released_note(bool drained) { return drained ? "; its GPU waits were released and the GPU finished (#267)" : "; its GPU waits were released but the GPU did not finish within 5 s (#267)"; @@ -136,6 +173,7 @@ const int64_t g_test_stall = [] { bool Verifier::release_gpu_waits(int timeout_ms) { released_.store(true); + trace_ev("RELEASE", -1, -1, timeout_ms); // the words the spin kernels read (wait_flag_ge, wait_flag_ge_or) are mapped host memory, so a store here // reaches them with no API call; UINT32_MAX is past every ring. (E-6's skip words are device memory, but // wait_flag_ge_or also returns on its flag.) A host function raising flag B later only raises. @@ -148,13 +186,76 @@ bool Verifier::release_gpu_waits(int timeout_ms) { for (cudaStream_t s : {cs_, copy_}) { if (s == nullptr) continue; while (cudaStreamQuery(s) == cudaErrorNotReady) { - if (ms_since(t0) > timeout_ms) return false; + if (ms_since(t0) > timeout_ms) { + trace_ev("RELEASE-NOT-DRAINED", -1, -1, (int64_t) ms_since(t0)); + return false; + } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } + trace_ev("RELEASE-DRAINED", -1, -1, (int64_t) ms_since(t0)); // aux: ms the GPU took to finish once released return true; } +void Verifier::trace_ev(const char* what, int64_t step, int64_t layer, int64_t aux) const { + if (!g_trace) return; + auto rd = [](const uint32_t* p) { return p ? *(const volatile uint32_t*) p : 0u; }; + TraceEv& e = g_trace_ring[g_trace_next.fetch_add(1) % kTraceN]; + e = {trace_now_ns(), this, windows, step, layer, aux, rd(h_seq_), rd(h_flag_), rd(h_flagA_), rd(h_flagB_), what}; +} + +void Verifier::trace_dump(std::FILE* f) const { + if (!g_trace || f == nullptr) return; + static const char* names[kProfPer] = {"pre", "hc-read0", "qkv gemv", "conv", "ab", "z", "rec", "kv-idx", + "k/v rope", "kv append", "q", "scores+topk", "kv-resolve", "attention", + "gate", "", "out-proj", "router+ring", "shared", "waitA", "VRAM hits", "waitB", + "PCIe grp", "waitCPU", "combine", "", "head", "", "", "", "", "", ""}; + const uint64_t end = g_trace_next.load(); + const uint64_t n = end < 160 ? end : 160; + std::fprintf(f, "strata verify trace (#649): the last %llu handshake events (host clock, us before the last; " + "words: seq = GPU rang, flag = CPU served, A = plan, B = copies)\n", (unsigned long long) n); + const int64_t t_last = n ? g_trace_ring[(end - 1) % kTraceN].t_ns : 0; + for (uint64_t i = end - n; i < end; ++i) { + const TraceEv& e = g_trace_ring[i % kTraceN]; + if (e.what == nullptr) continue; + std::fprintf(f, " %10.1f %-20s v%p window %lld step %lld layer %lld aux %lld | seq %u flag %u A %u B %u\n", + (double) (t_last - e.t_ns) / 1000.0, e.what, e.who, (long long) e.window, (long long) e.step, + (long long) e.layer, (long long) e.aux, e.seq, e.flag, e.a, e.b); + } + if (trace_h_ == nullptr || g_ == nullptr) return; + // the breadcrumbs: every stamp point the GPU passed in this window (ns of the GPU clock after the window's first) + unsigned long long t0 = ~0ull; + for (size_t i = 0; i < trace_n_; ++i) { + const unsigned long long t = *(const volatile unsigned long long*) (trace_h_ + i); + if (t != 0 && t < t0) t0 = t; + } + if (t0 == ~0ull) { + std::fprintf(f, "strata verify trace: no GPU breadcrumb in this window (the GPU never started it)\n"); + return; + } + int64_t last_l = -1; + for (int64_t l = 0; l <= g_->n_layers; ++l) + for (int i = 0; i < kProfPer; ++i) + for (int grp = 0; grp < 2; ++grp) + if (trace_h_[(size_t) ((l * kProfPer + i) * 2 + grp)] != 0) last_l = l; + std::fprintf(f, "strata verify trace: GPU breadcrumbs (us after the window's first), the GPU got as far as layer " + "%lld:\n", (long long) last_l); + for (int64_t l = std::max(0, last_l - 2); l <= std::min(g_->n_layers, last_l + 1); ++l) + for (int grp = 0; grp < 2; ++grp) { + std::string line; + char b[64]; + for (int i = 0; i < kProfPer; ++i) { + const unsigned long long t = trace_h_[(size_t) ((l * kProfPer + i) * 2 + grp)]; + if (t == 0) continue; + std::snprintf(b, sizeof b, " %s(%d)=%.1f", names[i], i, (double) (t - t0) / 1000.0); + line += b; + } + if (!line.empty()) + std::fprintf(f, " layer %lld group %d:%s\n", (long long) l, grp, line.c_str()); + } + std::fflush(f); +} + void Verifier::diag(std::FILE* f) const { auto rd = [](const uint32_t* p) { return p ? *(const volatile uint32_t*) p : 0u; }; // #251: outside a verify stage these are the LAST window's numbers (it finished), not the stalled work's @@ -163,6 +264,7 @@ void Verifier::diag(std::FILE* f) const { std::fprintf(f, " verify window%s: %d tokens at position %lld, host at layer step %u; the GPU rang %u; flags: " "served %u, plan (A) %u, copies (B) %u\n", current ? "" : " (last window, not the current stage)", last_t_, (long long) last_pos0_, cur_layer_ + 1, rd(h_seq_), rd(h_flag_), rd(h_flagA_), rd(h_flagB_)); + trace_dump(f); // #649: STRATA_VERIFY_TRACE=1 only } Verifier::~Verifier() { @@ -173,12 +275,22 @@ Verifier::~Verifier() { slot.compare_exchange_strong(me, nullptr); } if (cs_) cudaStreamSynchronize(cs_); + if (sh_cs_) cudaStreamSynchronize(sh_cs_); for (auto& e : exec_) if (e) cudaGraphExecDestroy(e); if (commit_exec_) cudaGraphExecDestroy(commit_exec_); + for (auto& kv : exec_bm_) + if (kv.second) cudaGraphExecDestroy(kv.second); + for (auto& kv : commit_bm_) + if (kv.second) cudaGraphExecDestroy(kv.second); + if (arena_b_) cudaFree(arena_b_); + if (h_commitb_) cudaFreeHost(h_commitb_); if (cs_) cudaStreamDestroy(cs_); + if (sh_cs_) cudaStreamDestroy(sh_cs_); if (copy_) { cudaStreamSynchronize(copy_); cudaStreamDestroy(copy_); } if (commit_done_) cudaEventDestroy(commit_done_); + if (ev_fork_) cudaEventDestroy(ev_fork_); + if (ev_join_) cudaEventDestroy(ev_join_); 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_}; @@ -298,6 +410,7 @@ bool Verifier::init(const WeightTable& wt, const ModelGeometry& g, SessionState& inj_ = b.take(T * HC); inj2_ = b.take(T * HC); lo_ = b.take(T * (uint64_t) g.hc_lr); rs_ = b.take(T * HC); xn_ = b.take(T * HC * N); xq_ = b.take(strata::kernels::native_q8_1_bytes(max_in, (int) T)); + sh_xq_ = b.take(strata::kernels::native_q8_1_bytes(max_in, (int) T)); qkv_L_ = b.take(nG * T * C); h_L_ = b.take(nG * T * C); gate_L_ = b.take(nG * T * HV); beta_L_ = b.take(nG * T * HV); z_ = b.take(T * ZV); y_ = b.take(T * ZV); y_dummy_ = b.take(T * ZV); @@ -331,6 +444,23 @@ bool Verifier::init(const WeightTable& wt, const ModelGeometry& g, SessionState& return false; } cudaMemset(arena_, 0, count.used); + if (g_trace && trace_h_ == nullptr) { // #649: the breadcrumbs, mapped so they read while the GPU hangs + trace_n_ = (size_t) (g.n_layers + 1) * kProfPer * 2; + if (!mapped(trace_n_ * 8, (void**) &trace_h_, (void**) &trace_m_)) { + (void) cudaGetLastError(); + trace_h_ = trace_m_ = nullptr; + trace_n_ = 0; + } +#if defined(STRATA_USE_HIP) + std::fprintf(stderr, "strata verify trace (#649): on; coherent words %s, doorbell %s, HIP_HOST_COHERENT=%s " + "HSA_ENABLE_SDMA=%s GPU_MAX_HW_QUEUES=%s\n", g_coherent ? "explicit" : "default", + g_doorbell_store ? "stored" : "incremented", std::getenv("HIP_HOST_COHERENT") ? std::getenv("HIP_HOST_COHERENT") : "-", + std::getenv("HSA_ENABLE_SDMA") ? std::getenv("HSA_ENABLE_SDMA") : "-", + std::getenv("GPU_MAX_HW_QUEUES") ? std::getenv("GPU_MAX_HW_QUEUES") : "-"); +#else + std::fprintf(stderr, "strata verify trace (#649): on\n"); +#endif + } prof_on_ = std::getenv("STRATA_VERIFY_PROFILE") != nullptr; if (prof_on_) { const size_t np = (size_t) g.n_layers * kProfPer + 4; @@ -347,31 +477,56 @@ bool Verifier::init(const WeightTable& wt, const ModelGeometry& g, SessionState& err = "verify: copy stream create failed"; return false; } - if (cudaStreamCreateWithFlags(&cs_, cudaStreamNonBlocking) != cudaSuccess) { + if (cudaStreamCreateWithFlags(&cs_, cudaStreamNonBlocking) != cudaSuccess || + cudaStreamCreateWithFlags(&sh_cs_, cudaStreamNonBlocking) != cudaSuccess) { err = "verify: stream create failed"; return false; } - if (cudaEventCreateWithFlags(&commit_done_, cudaEventDisableTiming) != cudaSuccess) { + if (cudaEventCreateWithFlags(&commit_done_, cudaEventDisableTiming) != cudaSuccess || + cudaEventCreateWithFlags(&ev_fork_, cudaEventDisableTiming) != cudaSuccess || + cudaEventCreateWithFlags(&ev_join_, cudaEventDisableTiming) != cudaSuccess) { err = "verify: event create failed"; return false; } + // Check whether 100% of experts across [lb_, le_) are resident in this stage's VRAM cache. + // When true, every layer plans on device and writes directly into parts_ without any CPU doorbells, + // wait_flag_ge spins, PCIe empty launches, or moe_hit_add copies. + all_resident_ = false; + if (hits.h_res != nullptr && hits.d_res != nullptr && hits.cache_base != nullptr) { + const char* v_ar = std::getenv("STRATA_VERIFY_ALL_RESIDENT"); + if (v_ar == nullptr || std::atoi(v_ar) != 0) { + bool all_ok = true; + for (int64_t l = lb_; l < le_ && all_ok; ++l) { + for (int64_t e = 0; e < g.n_expert; ++e) { + if (hits.h_res[l * g.n_expert + e] < 0) { + all_ok = false; + break; + } + } + } + all_resident_ = all_ok; + } + } // 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) { const char* v = std::getenv("STRATA_VERIFY_DEVICE_PLAN"); - device_plan_ = v != nullptr && std::atoi(v) != 0; + device_plan_ = !all_resident_ && (v != nullptr && std::atoi(v) != 0); } - if (device_plan_) { - bool ok2 = cudaMalloc((void**) &skip_, 64) == cudaSuccess && cudaMemset(skip_, 0, 64) == cudaSuccess; + if (all_resident_ || device_plan_) { + bool ok2 = true; + if (device_plan_) + ok2 = cudaMalloc((void**) &skip_, 64) == cudaSuccess && cudaMemset(skip_, 0, 64) == cudaSuccess; if (ok2 && hits.slot_off != nullptr && hits.n_slots > 0) { ok2 = cudaMalloc((void**) &slot_off_d_, (size_t) hits.n_slots * sizeof(unsigned long long)) == cudaSuccess && cudaMemcpy(slot_off_d_, hits.slot_off, (size_t) hits.n_slots * sizeof(unsigned long long), cudaMemcpyHostToDevice) == cudaSuccess; } - if (!ok2) { cudaGetLastError(); device_plan_ = false; } + if (!ok2) { cudaGetLastError(); all_resident_ = false; device_plan_ = false; } } - std::fprintf(stderr, "strata verify: window up to %d tokens, %.1f MiB of device buffers\n", max_t, - (double) count.used / 1048576.0); + std::fprintf(stderr, "strata verify: window up to %d tokens, %.1f MiB of device buffers%s\n", max_t, + (double) count.used / 1048576.0, + all_resident_ ? " (100% VRAM resident: zero-doorbell graph)" : ""); return true; } @@ -403,11 +558,18 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { const int64_t TS = (s.idx_block - 1) * ID; const bool ple_on = ss.ple.ready() && ple_stage(); auto Rt = [&](int t) { return R_ + (size_t) t * HC * N; }; - const int G = (split_ && T >= 2) ? 2 : 1; + const int G = (split_ && T >= 2 && !batch_rec_) ? 2 : 1; // a batch window is one group static const bool dec_batch = [] { const char* v = std::getenv("STRATA_DEC_BATCH"); return v == nullptr || std::atoi(v) != 0; }(); - auto stamp = [&](int64_t l, int i, int grp) { if (prof_on_ && grp == 0) gpu_stamp(prof_, (int) (l * kProfPer + i), cs); }; + auto stamp = [&](int64_t l, int i, int grp) { + if (prof_on_ && grp == 0) gpu_stamp(prof_, (int) (l * kProfPer + i), cs); + if (trace_m_ != nullptr) gpu_stamp(trace_m_, (int) ((l * kProfPer + i) * 2 + grp), cs); // #649 + }; const int tb_[2] = {0, (T + 1) / 2}, te_[2] = {G == 2 ? (T + 1) / 2 : T, T}; - groups_[T] = G; + if (!batch_rec_) groups_[T] = G; + const int64_t nQall = g.n_qsa_layers(); + // a batch window: row t is slot t, whose state lives in its own session + auto slot_ss = [&](int t) -> SessionState& { return batch_rec_ ? *slots_[(size_t) brow_[t]] : ss; }; + const int hrow0 = batch_rec_ ? row_base_ : 0; // a slot group's own hand-off rows // ---- the window's inputs, from mapped staging copy_i32_from_mapped(tok_, m_tok_, T, cs); @@ -416,17 +578,15 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { // per-ROW positions of the K rows [t][NKV] and the indexer query rows [t][IQ] (for batched RoPE) const int32_t* pos_k = pos_ + MT * NH; const int32_t* pos_i = pos_ + MT * (NH + NKV); - if (ple_on) copy_from_mapped(ple_, m_ple_, (int64_t) T * N, cs); // ---- the embeddings, broadcast to the hc streams - or, in a later stage of a layer split, the previous stage's // residual, pending write and inject (see set_stage) const int64_t HB = Verifier::handoff_floats(g); if (lb_ > 0) { - for (int t = 0; t < T; ++t) { - copy_from_mapped(Rt(t), hand_in_ + (size_t) t * HB, HC * N, cs); - copy_from_mapped(bo_ + (size_t) t * N, hand_in_ + (size_t) t * HB + HC * N, N, cs); - copy_from_mapped(inj2_ + (size_t) t * HC, hand_in_ + (size_t) t * HB + HC * N + N, HC, cs); - } + const float* hin = hand_in_ + (size_t) hrow0 * HB; // the group's rows: [R][bo][inj] contiguous + copy_from_mapped(R_, hin, (int64_t) T * HC * N, cs); + copy_from_mapped(bo_, hin + (size_t) T * HC * N, (int64_t) T * N, cs); + copy_from_mapped(inj2_, hin + (size_t) T * (HC + 1) * N, (int64_t) T * HC, cs); } else if (const NativeEmbed* ne = native_embed()) { // plan v0.3 P6: the GGUF-form table ne->gather_dev(tok_, T, emb_, cs); broadcast_streams(emb_, R_, N, (int) HC, T, cs); @@ -474,20 +634,48 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { // already applied it) bool pending = l > 0 && !cvec().covers(l - 1); if (l == 1 && ple_on) { + if (grp == 0) { + if (all_resident_) wait_flag_ge(m_flag_, 1, cs); + copy_from_mapped(ple_, m_ple_, (int64_t) T * N, cs); + } float* normalized = (float*) ((uint8_t*) ss.ple.scratch + ple_block_scratch_bytes()); + static const bool ple_batch_env = [] { + const char* e = std::getenv("STRATA_PLE_BATCH"); + return !e || e[0] != '0'; + }(); + const bool ple_batch_kv = ple_batch_env && dec_batch && n > 1 && ss.ple.w.key_bf16 != nullptr && + ss.ple.w.value_bf16 != nullptr && ple_native_bf16_enabled() && + ple_native_postops_enabled(); + if (ple_batch_kv) { + try { + bf16_gemv_fp32_mmvf_multi(ple_ + (size_t) tb * N, N, ss.ple.w.key_bf16, + xn_ + (size_t) tb * HC * N, HC * N, N, HC * N, n, cs); + bf16_gemv_fp32_mmvf_multi(ple_ + (size_t) tb * N, N, ss.ple.w.value_bf16, + z_ + (size_t) tb * N, N, N, N, n, cs); + } catch (const std::exception& e) { + err = std::string("verify PLE multi: ") + e.what(); + return false; + } + } for (int t = tb; t < te; ++t) { gr_write(Rt(t), bo_ + t * N, inj2_ + t * HC, gs, Rt(t), cs); PleOut po; po.normalized = normalized; po.result = Rt(t); + float* hist = batch_rec_ ? slot_ss(t).ple_hist : ss.ple.hist; // the row's own history try { - ple_block(ple_ + t * N, Rt(t), ss.ple.hist, ss.ple.w, po, ss.ple.scratch, cs); - ple_history_advance(ss.ple.hist, normalized, cs); + if (ple_batch_kv) { + ple_block_projected(xn_ + (size_t) t * HC * N, z_ + (size_t) t * N, Rt(t), hist, + ss.ple.w, po, ss.ple.scratch, cs); + } else { + ple_block(ple_ + t * N, Rt(t), hist, ss.ple.w, po, ss.ple.scratch, cs); + } + ple_history_advance(hist, normalized, cs); } catch (const std::exception& e) { err = std::string("verify PLE: ") + e.what(); return false; } - copy_from_mapped(hist_snap_ + (size_t) t * HS, ss.ple.hist, HS, cs); + copy_from_mapped(hist_snap_ + (size_t) t * HS, hist, HS, cs); } pending = false; } @@ -530,6 +718,15 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { native_quantize_q8_1(xm, xq_, (int) N, n, cs); native_mmvq(wqkv->native_type, wqkv->native_data, xq_, qkv + (size_t) tb * C, (int) N, (int) C, n, cs); stamp(l, 2, grp); + if (batch_rec_) { // each row from its own slot's conv history, one row each + for (int t = tb; t < te; ++t) { + SessionState& sx = slot_ss(t); + const float* cx = sx.gdn_state + (size_t) (gi - sx.gdn_ord0) * gdn_floats + + (uint64_t) g.ssm_state_size * g.ssm_v_heads * g.ssm_state_size; + gdn_conv_l2_multi(cx, qkv + (size_t) t * C, (const float*) wc->data, hb + (size_t) t * C, (int) C, + (int) (2 * HK), EPS, 1, cs, 0); + } + } else gdn_conv_l2_multi(conv, qkv, (const float*) wc->data, hb, (int) C, (int) (2 * HK), EPS, n, cs, tb); stamp(l, 3, grp); gdn_ab_multi(xm, (const uint16_t*) wa->data, (const uint16_t*) wb->data, (const float*) wdt->data, @@ -539,6 +736,15 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { native_mmvq(wg->native_type, wg->native_data, xq_, z_ + (size_t) tb * ZV, (int) N, (int) ZV, n, cs); stamp(l, 5, grp); // the recurrence from the untouched state over tokens [0, te); outputs only for this group's + if (batch_rec_) { // each row's recurrence from its own slot's state, one token + for (int t = tb; t < te; ++t) { + SessionState& sx = slot_ss(t); + float* stx = sx.gdn_state + (size_t) (gi - sx.gdn_ord0) * gdn_floats; + gdn_step_norm_multi(stx, hb + (size_t) t * C, (int) C, gate + (size_t) t * HV, beta + (size_t) t * HV, + z_ + (size_t) t * ZV, (const float*) wnm->data, EPS, y_ + (size_t) t * ZV, + (int) HK, (int) HV, 1, nullptr, cs, 0); + } + } else gdn_step_norm_multi(state, hb, (int) C, gate, beta, z_, (const float*) wnm->data, EPS, y_, (int) HK, (int) HV, te, nullptr, cs, tb); stamp(l, 6, grp); @@ -583,8 +789,14 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { fwht256_inplace_cuda(vcur_ + tb * NKV * HD, (int64_t) n * NKV, cs); } stamp(l, 8, grp); + if (batch_rec_) { // every slot's indexer tail, restored by its commit + for (int t = tb; t < te; ++t) + copy_from_mapped(tail_snap_b_ + ((size_t) brow_[t] * nQall + qi) * TS, slot_ss(t).qsa_states[qi].idx_tail, + TS, cs); + } else if (grp == 0) copy_from_mapped(tail_snap_ + (size_t) qi * TS, st.idx_tail, TS, cs); for (int t = tb; t < te; ++t) { + const QsaState& st = slot_ss(t).qsa_states[qi]; // the row's own K/V (ss's outside a batch) const int32_t* step_t = step_ + t * kStepCount; if (st.kv_hybrid) { // K8V4: the unused half's lanes folded onto the used pool (layer.cpp) kv_append_q8_step(st.k_q, st.k_q, st.k_scale, st.k_scale, st.page_table, step_t, @@ -601,11 +813,13 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { kv_append_step(st.k_pool, st.v_pool, st.page_table, step_t, kcur_ + t * NKV * HD, vcur_ + t * NKV * HD, s, cs, &st.host); } - const QsaIndexerBuffers ib{st.idx_tail, st.idx_dead, st.idx_pooled, st.idx_block_pos}; - for (int t = tb; t < te; ++t) + for (int t = tb; t < te; ++t) { + const QsaState& sx = slot_ss(t).qsa_states[qi]; + const QsaIndexerBuffers ib{sx.idx_tail, sx.idx_dead, sx.idx_pooled, sx.idx_block_pos}; native_qsa_indexer_append(idx_raw + t * ID, step_ + t * kStepCount + kStepPos, 0, - (const float*) wikn->data, EPS, ib, s, st.max_cells, + (const float*) wikn->data, EPS, ib, s, sx.max_cells, rope_scaling(), cs); + } stamp(l, 9, grp); native_mmvq(wq->native_type, wq->native_data, xq_, qfull_ + tb * NH * 2 * HD, (int) N, (int) (NH * 2 * HD), n, cs); @@ -638,6 +852,20 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { } } stamp(l, 10, grp); + if (batch_rec_) { // each row selects and attends over its own slot's K/V + for (int t = tb; t < te; ++t) { + const QsaState& sx = slot_ss(t).qsa_states[qi]; + qsa_block_scores(sx.idx_pooled, sx.idx_dead, qidx_ + t * IQ * ID, step_ + t * kStepCount, 1, + max_blocks_, s, scores_ + (size_t) t * max_blocks_, cs); + qsa_block_topk(scores_ + (size_t) t * max_blocks_, step_ + t * kStepCount, 1, max_blocks_, cap_, s, + sel_ + (size_t) t * cap_, cs); + qsa_kv_resolve(sx, *g_, sel_ + (size_t) t * cap_, step_ + t * kStepCount, 1, cap_, cs); + const QsaAttnPools px = qsa_attn_pools(sx); + qsa_decode_attn_batch(qcur_ + t * NH * HD, px, sel_ + (size_t) t * cap_, step_ + t * kStepCount, + cap_, s, attn_scratch_ + (size_t) t * attn_scratch_floats_, + attn_ + t * NH * HD, 1, cs); + } + } else { qsa_block_scores(st.idx_pooled, st.idx_dead, qidx_ + tb * IQ * ID, step_ + tb * kStepCount, n, max_blocks_, s, scores_ + (size_t) tb * max_blocks_, cs); qsa_block_topk(scores_ + (size_t) tb * max_blocks_, step_ + tb * kStepCount, n, max_blocks_, cap_, s, @@ -649,6 +877,7 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { const QsaAttnPools pools = qsa_attn_pools(st); 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_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, @@ -671,6 +900,16 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { } stamp(l, 16, grp); gr_read_group(1, true, inj_, inj2_); + static const bool sh_stream_env = [] { + const char* e = std::getenv("STRATA_SH_STREAM"); + return !e || e[0] != '0'; + }(); + const bool sh_fork = sh_stream_env && !prof_on_ && sh_cs_ != nullptr && ev_fork_ != nullptr && ev_join_ != nullptr; + cudaStream_t sh_stream = sh_fork ? sh_cs_ : cs; + if (sh_fork) { + cudaEventRecord(ev_fork_, cs); + cudaStreamWaitEvent(sh_cs_, ev_fork_, 0); + } // the window's rows routed in 2 launches (one router GEMV reading the weight once, one // top-10) instead of 2 per token; every row's arithmetic is the single-token call's (STRATA_DEC_BATCH=0: old) const WeightRef* w_router = v.get("ffn_gate_inp.weight"); @@ -680,19 +919,46 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { NE, n, cs); native_router_top10_multi(logits_ + tb * NE, ids_ + tb * K, w_ + tb * K, n, cs); } catch (const std::exception& e) { err = "verify router: " + std::string(e.what()); return false; } +#if defined(STRATA_HIP_GFX906) + } else if (dec_batch) { + // gfx906: the Coder's 256-expert router (the native router is 512 x 10 only) for the whole window in one + // multi-column BF16 projection and one top-k, bitwise the per-token calls (route_window_parity); ~3 ms + // of a ~55 ms verify window on 2x MI50. STRATA_ROUTE_PER_TOKEN=1: the per-token calls. + if (!moe_route_window(wt, g, l, K, ss.moe, mixed_ + tb * N, logits_ + tb * NE, ids_ + tb * K, w_ + tb * K, n, + cs, err)) + return false; +#endif } else for (int t = tb; t < te; ++t) { MoEBuffers mb = ss.moe; mb.logits = logits_ + t * NE; mb.ids = ids_ + t * K; mb.weights = w_ + t * K; if (!moe_route(wt, g, l, K, mb, mixed_ + t * N, cs, err, nullptr)) return false; } - if (device_plan_) // E-6: every routed expert resident: this group's plan without the host + if (all_resident_) { resident_plan(ids_ + tb * K, n * (int) K, (int) K, hits_.d_res + l * g.n_expert, (int) g.n_expert, hits_.cache_base, slot_off_d_, (long long) hits_.blob, - plan_ + (size_t) grp * (size_t) (plan_i32_ + 16), (long long) max_t_ * K, skip_ + grp, - (uint32_t) ((l - lb_) * G + grp + 1), cs); - doorbell_publish(xm, ids_ + tb * K, w_ + tb * K, (int64_t) n * N, (int64_t) n * K, m_x_ + tb * N, - m_ids_ + tb * K, m_w_ + tb * K, m_seq_, cs); + plan_ + (size_t) grp * (size_t) (plan_i32_ + 16), (long long) max_t_ * K, nullptr, 0, cs); + } else { + if (device_plan_) // E-6: every routed expert resident: this group's plan without the host + resident_plan(ids_ + tb * K, n * (int) K, (int) K, hits_.d_res + l * g.n_expert, (int) g.n_expert, + hits_.cache_base, slot_off_d_, (long long) hits_.blob, + plan_ + (size_t) grp * (size_t) (plan_i32_ + 16), (long long) max_t_ * K, skip_ + grp, + (uint32_t) ((l - lb_) * G + grp + 1), cs); +#if defined(STRATA_USE_HIP) + if (g_doorbell_store) // #649 A/B: the step's ring stored, not incremented over PCIe + doorbell_publish_value(xm, ids_ + tb * K, w_ + tb * K, (int64_t) n * N, (int64_t) n * K, m_x_ + tb * N, + m_ids_ + tb * K, m_w_ + tb * K, m_seq_, (uint32_t) ((l - lb_) * G + grp + 1), cs); + else +#endif + if (remote_opt_) // #578: the helper GPUs reduce with the routing weights - publish them + doorbell_publish(xm, ids_ + tb * K, w_ + tb * K, (int64_t) n * N, (int64_t) n * K, m_x_ + tb * N, + m_ids_ + tb * K, m_w_ + tb * K, m_seq_, cs); + else { + const int32_t* layer_res = hits_.d_res != nullptr ? (hits_.d_res + l * g.n_expert) : nullptr; + doorbell_publish_res(xm, ids_ + tb * K, layer_res, (int) g.n_expert, (int64_t) n * N, (int64_t) n * K, + m_x_ + tb * N, m_ids_ + tb * K, m_seq_, cs); + } + } stamp(l, 17, grp); { const WeightRef *wgi = need(v, "ffn_gate_inp_shexp.weight", err), *wsg = need(v, "ffn_gate_shexp.weight", err), @@ -705,16 +971,19 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { nsw.gate_type = wsg->native_type; nsw.gate_data = wsg->native_data; nsw.up_type = wsu->native_type; nsw.up_data = wsu->native_data; nsw.down_type = wsd->native_type; nsw.down_data = wsd->native_data; - nsw.q8_1 = xq_; - if (dec_batch) f32_to_bf16_bulk(mixed_ + tb * N, sh_bf16_ + tb * N, (int64_t) n * N, cs); // contiguous rows - else for (int t = tb; t < te; ++t) f32_to_bf16_bulk(mixed_ + t * N, sh_bf16_ + t * N, N, cs); + nsw.q8_1 = sh_fork ? sh_xq_ : xq_; + if (!shared_expert_native_bf16_enabled()) { + if (dec_batch) f32_to_bf16_bulk(mixed_ + tb * N, sh_bf16_ + tb * N, (int64_t) n * N, sh_stream); // contiguous rows + else for (int t = tb; t < te; ++t) f32_to_bf16_bulk(mixed_ + t * N, sh_bf16_ + t * N, N, sh_stream); + } try { shared_expert_multi(n, xm, sh_bf16_ + tb * N, nsw, (const uint16_t*) wgi->data, sh_gate_ + (size_t) tb * g.n_ff, - sh_up_ + (size_t) tb * g.n_ff, sh_g_ + tb, shared_ + tb * N, N, g.n_ff, cs); + sh_up_ + (size_t) tb * g.n_ff, sh_g_ + tb, shared_ + tb * N, N, g.n_ff, sh_stream); } catch (const std::exception& e) { err = std::string("verify shared expert: ") + e.what(); return false; } + if (sh_fork) cudaEventRecord(ev_join_, sh_cs_); } if (strata::kernels::cpu::expert_layout().native) quantize_q8_1_rows(xm, n, N, nat_xq_ + (size_t) tb * (N / 32) * 36, cs); @@ -725,19 +994,17 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { }; // ---------------------------------------------------------------- post(l, group): experts, combine + static const bool fuse_head_gr_env = [] { + const char* e = std::getenv("STRATA_FUSE_HEAD_GR"); + return e && e[0] == '1'; + }(); + const bool fuse_head_gr = fuse_head_gr_env && (le_ == g.n_layers) && (head_ != nullptr && head_->loaded()) && + !cvec().covers(g.n_layers - 1); auto post = [&](int64_t l, int grp) -> bool { const int tb = tb_[grp], te = te_[grp], n = te - tb; const uint32_t ring = (uint32_t) ((l - lb_) * G + grp + 1); const int64_t cap = (int64_t) n * K, capx = (int64_t) max_t_ * K; int32_t* pl = plan_ + (size_t) grp * (size_t) (plan_i32_ + 16); - if (device_plan_) { // E-6: skipped when the device planned this group (all its experts resident) - wait_flag_ge_or(m_flagA_, ring, skip_ + grp, cs); - copy_i32_from_mapped_unless(pl, m_plan_ + (size_t) grp * (size_t) plan_i32_, plan_i32_, skip_ + grp, ring, cs); - } else { - wait_flag_ge(m_flagA_, ring, cs); // the pool published this group's GPU plan - copy_i32_from_mapped(pl, m_plan_ + (size_t) grp * (size_t) plan_i32_, plan_i32_, cs); - } - stamp(l, 19, grp); const int32_t* p_counts = pl; const int32_t* p_start = pl + 4; const int32_t* p_dst = p_start + capx + 1; @@ -747,50 +1014,70 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { const unsigned long long* p_ptr2 = p_ptr + capx; const int32_t* p_start2 = pl + ptr_off + 4 * capx; float* hit_out = hit_out_ + (size_t) tb * K * N; + float* parts_out = parts_ + (size_t) tb * K * N; const auto& lay = strata::kernels::cpu::expert_layout(); // plan v0.3 P6: the VRAM groups now; the PCIe groups once the copy engine has landed them in staging. // `gy`: the native launch's groups side by side (0: cap, one block row per possible group). - auto grouped = [&](const unsigned long long* gp, const int32_t* gs, const int32_t* gn, int64_t gy) { + auto grouped = [&](const unsigned long long* gp, const int32_t* gs, const int32_t* gn, int64_t gy, float* dst_buf) { if (lay.native) { // the layer's GGUF formats (i-quant gate/up, Q2_0 / IQ4_NL down) const auto& f = lay.fmt[(size_t) l]; const NativeExpertLayout L = native_expert_layout(f.gu_type, f.d_type, f.n_embd, f.n_ff); native_expert_grouped(L, gp, gs, gn, p_dst, p_tok, cap, cap, - nat_xq_ + (size_t) tb * (N / 32) * 36, hit_scratch_, hit_out, cs, gy); + nat_xq_ + (size_t) tb * (N / 32) * 36, hit_scratch_, dst_buf, cs, gy); } else { moe_grouped_s2(gp, gs, gn, p_dst, p_tok, cap, cap, hit_xq_ + (size_t) tb * (N / 32) * 34, - hit_xs_ + (size_t) tb * (N / 32), hit_scratch_, hit_out, cs); + hit_xs_ + (size_t) tb * (N / 32), hit_scratch_, dst_buf, cs); } }; - grouped(p_ptr, p_start, p_counts, 0); - stamp(l, 20, grp); - if (device_plan_) wait_flag_ge_or(m_flagB_, ring, skip_ + grp, cs); - else wait_flag_ge(m_flagB_, ring, cs); // the PCIe share is in staging (DMA) or mapped - if (sink_.pcie_mode == 2) { // stage it with a copy kernel, then point at staging - const int64_t per = G == 2 ? kStagingBlobs / 2 : kStagingBlobs; - uint8_t* stage = staging_ + (size_t) (grp * per) * lay.max_blob; - fetch_blobs(p_ptr2, p_counts + 2, stage, (int64_t) lay.blob_bytes(l), (int) per, cs); - rebase_ptrs((unsigned long long*) p_ptr2, p_counts + 2, stage, (int64_t) lay.blob_bytes(l), cs); - } - stamp(l, 21, grp); - // the PCIe share is pcie_frac of the misses: a few groups when the cache is cold, usually none (always none at - // pcie_frac 0), so its launch is kPcieGroupRows block rows striding over the groups, not cap of them - grouped(p_ptr2, p_start2, p_counts + 2, kPcieGroupRows); - stamp(l, 22, grp); - if (device_plan_) { // no CPU share when the device planned the group: its rows are zeros - wait_flag_ge_or(m_flag_, ring, skip_ + grp, cs); - copy_or_zero_from_mapped(parts_ + (size_t) tb * K * N, m_ymiss_ + (size_t) tb * K * N, (long long) n * K * N, - skip_ + grp, ring, cs); + if (all_resident_) { + stamp(l, 19, grp); + grouped(p_ptr, p_start, p_counts, 0, parts_out); + stamp(l, 20, grp); } else { - wait_flag_ge(m_flag_, ring, cs); // the CPU's share is in the mapped rows - stamp(l, 23, grp); - if (dec_batch) // only the CPU rows cross PCIe (p_dst[0, counts[1]) = the GPU's own rows) - copy_rows_from_mapped(parts_ + (size_t) tb * K * N, m_ymiss_ + (size_t) tb * K * N, (int64_t) n * K, N, - p_dst, p_counts + 1, cs); - else - copy_from_mapped(parts_ + (size_t) tb * K * N, m_ymiss_ + (size_t) tb * K * N, (int64_t) n * K * N, cs); + if (device_plan_) { // E-6: skipped when the device planned this group (all its experts resident) + wait_flag_ge_or(m_flagA_, ring, skip_ + grp, cs); + copy_i32_from_mapped_unless(pl, m_plan_ + (size_t) grp * (size_t) plan_i32_, plan_i32_, skip_ + grp, ring, cs); + } else { + wait_flag_ge(m_flagA_, ring, cs); // the pool published this group's GPU plan + copy_i32_from_mapped(pl, m_plan_ + (size_t) grp * (size_t) plan_i32_, plan_i32_, cs); + } + stamp(l, 19, grp); + grouped(p_ptr, p_start, p_counts, 0, hit_out); + stamp(l, 20, grp); + if (device_plan_) wait_flag_ge_or(m_flagB_, ring, skip_ + grp, cs); + else wait_flag_ge(m_flagB_, ring, cs); // the PCIe share is in staging (DMA) or mapped + if (sink_.pcie_mode == 2) { // stage it with a copy kernel, then point at staging + const int64_t per = G == 2 ? kStagingBlobs / 2 : kStagingBlobs; + uint8_t* stage = staging_ + (size_t) (grp * per) * lay.max_blob; + fetch_blobs(p_ptr2, p_counts + 2, stage, (int64_t) lay.blob_bytes(l), (int) per, cs); + rebase_ptrs((unsigned long long*) p_ptr2, p_counts + 2, stage, (int64_t) lay.blob_bytes(l), cs); + } + stamp(l, 21, grp); + // the PCIe share is pcie_frac of the misses: a few groups when the cache is cold, usually none (always none at + // pcie_frac 0), so its launch is kPcieGroupRows block rows striding over the groups, not cap of them + grouped(p_ptr2, p_start2, p_counts + 2, kPcieGroupRows, hit_out); + stamp(l, 22, grp); + if (device_plan_) { // no CPU share when the device planned the group: its rows are zeros + wait_flag_ge_or(m_flag_, ring, skip_ + grp, cs); + copy_or_zero_from_mapped(parts_out, m_ymiss_ + (size_t) tb * K * N, (long long) n * K * N, + skip_ + grp, ring, cs); + } else { + wait_flag_ge(m_flag_, ring, cs); // the CPU's share is in the mapped rows + stamp(l, 23, grp); + if (remote_opt_) // #578: the helper's rows come back reduced; skip them as well + remote_opt_->copy_rows(parts_out, m_ymiss_ + (size_t) tb * K * N, tb, n, p_dst, p_counts + 1, cs); + else if (dec_batch) // only the CPU rows cross PCIe (p_dst[0, counts[1]) = the GPU's own rows) + copy_rows_from_mapped(parts_out, m_ymiss_ + (size_t) tb * K * N, (int64_t) n * K, N, + p_dst, p_counts + 1, cs); + else + copy_from_mapped(parts_out, m_ymiss_ + (size_t) tb * K * N, (int64_t) n * K * N, cs); + } + moe_hit_add(parts_out, hit_out, p_dst, p_counts + 1, cap, N, cs); + } + if (!prof_on_ && sh_cs_ != nullptr && ev_fork_ != nullptr && ev_join_ != nullptr) { + cudaStreamWaitEvent(cs, ev_join_, 0); } - moe_hit_add(parts_ + (size_t) tb * K * N, hit_out, p_dst, p_counts + 1, cap, N, cs); if (dec_batch && n > 1 && native_moe_combine_enabled()) { // one launch for the window's rows try { native_moe_combine_multi(parts_ + (size_t) tb * K * N, w_ + tb * K, shared_ + tb * N, bo_ + tb * N, N, K, n, cs); @@ -801,10 +1088,14 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { mb.weights = w_ + t * K; mb.shared = shared_ + t * N; if (!moe_combine_parts(g, l, K, mb, parts_ + (size_t) t * K * N, bo_ + t * N, cs, err)) return false; } + if (remote_opt_) remote_opt_->combine(bo_ + tb * N, y_dummy_ + tb * N, tb, n, + device_plan_ ? skip_ + grp : nullptr, ring, cs); stamp(l, 24, grp); if (l == g.n_layers - 1) { - for (int t = tb; t < te; ++t) gr_write(Rt(t), bo_ + t * N, inj2_ + t * HC, gs, Rt(t), cs); - if (cvec().covers(l)) cvec_apply(Rt(tb), l, n, HC * N, nullptr, 0, nullptr, 0, false, cs); + if (!fuse_head_gr) { + for (int t = tb; t < te; ++t) gr_write(Rt(t), bo_ + t * N, inj2_ + t * HC, gs, Rt(t), cs); + if (cvec().covers(l)) cvec_apply(Rt(tb), l, n, HC * N, nullptr, 0, nullptr, 0, false, cs); + } } else if (cvec().covers(l)) { cvec_apply(Rt(tb), l, n, HC * N, bo_ + tb * N, N, inj2_ + tb * HC, HC, true, cs); } @@ -819,11 +1110,10 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { if (l + 1 < le_ && !pre(l + 1, grp)) return false; } if (le_ < g.n_layers) { // a layer split's earlier stage: hand the residual on, no head - for (int t = 0; t < T; ++t) { - copy_from_mapped(hand_out_ + (size_t) t * HB, Rt(t), HC * N, cs); - copy_from_mapped(hand_out_ + (size_t) t * HB + HC * N, bo_ + (size_t) t * N, N, cs); - copy_from_mapped(hand_out_ + (size_t) t * HB + HC * N + N, inj2_ + (size_t) t * HC, HC, cs); - } + float* hout = hand_out_ + (size_t) hrow0 * HB; + copy_from_mapped(hout, R_, (int64_t) T * HC * N, cs); + copy_from_mapped(hout + (size_t) T * HC * N, bo_, (int64_t) T * N, cs); + copy_from_mapped(hout + (size_t) T * (HC + 1) * N, inj2_, (int64_t) T * HC, cs); return true; } @@ -833,14 +1123,33 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) { const WeightRef *hn = wt.find("output_hc_norm.weight"), *hd = wt.find("output_hc_down.weight"), *hu = wt.find("output_hc_up.weight"); if (!hn || !hd || !hu) { err = "verify: an output_hc_* weight is missing"; return false; } - for (int t = 0; t < T; ++t) { - BlockBuffers bb = ss.block; - bb.R = Rt(t); - bb.mixed = head_mixed_ + t * N; - if (head_ != nullptr && head_->loaded()) { - if (!lm_head_mix(wt, g, bb, cs, err)) return false; - } else if (!lm_head(wt, g, bb, head_logits_ + (size_t) t * n_vocab_, cs, err)) { - return false; + if (fuse_head_gr) { + FusedGrArgs fa[kFusedGrMaxT]; + for (int t = 0; t < T; ++t) { + FusedGrArgs& a = fa[t]; + a.R = Rt(t); a.R_out = Rt(t); a.apply = true; + a.bo_prev = bo_ + t * N; a.inj_prev = inj2_ + t * HC; + a.w_norm = (const float*) hn->data; + a.w_down = (const uint16_t*) hd->data; + a.w_up = (const uint16_t*) hu->data; + a.w_inject = nullptr; + a.eps = EPS; + a.lo = lo_ + t * g.hc_lr; + a.rs = rs_ + t * HC; + a.inject_out = head_inj_; + a.mixed = head_mixed_ + t * N; + } + fused_gr_read_multi(fa, T, xn_, cs); + } else { + for (int t = 0; t < T; ++t) { + BlockBuffers bb = ss.block; + bb.R = Rt(t); + bb.mixed = head_mixed_ + t * N; + if (head_ != nullptr && head_->loaded()) { + if (!lm_head_mix(wt, g, bb, cs, err)) return false; + } else if (!lm_head(wt, g, bb, head_logits_ + (size_t) t * n_vocab_, cs, err)) { + return false; + } } } if (head_ != nullptr && head_->loaded()) { @@ -984,7 +1293,7 @@ bool Verifier::capture_commit(std::string& err) { gdn_conv_commit(conv, qkv, (int) C, commit_, cs_); gdn_step_norm_multi(state, h_L_ + (size_t) gdn_index * MT * C, (int) C, gate_L_ + (size_t) gdn_index * MT * HV, beta_L_ + (size_t) gdn_index * MT * HV, z_, (const float*) wnm->data, EPS, y_dummy_, - (int) g.ssm_k_heads, (int) HV, (int) MT, commit_, cs_); + (int) g.ssm_k_heads, (int) HV, (int) MT, commit_, cs_, (int) MT); ++gdn_index; } else { const QsaState& st = ss.qsa_states[qsa_index]; @@ -1019,10 +1328,70 @@ bool Verifier::capture_commit(std::string& err) { return true; } +void Verifier::stage_inputs(int T, const int32_t* tokens, int64_t pos0) { + using namespace strata::kernels; + const ModelGeometry& g = *g_; + const QsaShapes s = shapes_of(g); + int32_t* const pk = h_pos_ + (size_t) max_t_ * g.n_head; + int32_t* const pi = pk + (size_t) max_t_ * g.n_head_kv; + for (int t = 0; t < T; ++t) { + h_tok_[t] = tokens[t]; + qsa_step_fill(h_step_ + t * kStepCount, pos0 + t, s); + const int32_t pos_t = (int32_t) (pos0 + t); + for (int64_t h = 0; h < g.n_head; ++h) h_pos_[t * g.n_head + h] = pos_t; + for (int64_t h = 0; h < g.n_head_kv; ++h) pk[t * g.n_head_kv + h] = pos_t; + for (int64_t h = 0; h < g.idx_q_heads; ++h) pi[t * g.idx_q_heads + h] = pos_t; + } + *(volatile uint32_t*) h_seq_ = 0; + *(volatile uint32_t*) h_flag_ = 0; + *(volatile uint32_t*) h_flagA_ = 0; + *(volatile uint32_t*) h_flagB_ = 0; + std::atomic_thread_fence(std::memory_order_seq_cst); + last_t_ = T; + last_pos0_ = pos0; + for (int t = 0; t < T; ++t) last_tokens_[t] = tokens[t]; + staged_ = true; +} + +void Verifier::collect_profile() { + const ModelGeometry& g = *g_; + 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); + for (int i = 1; i <= 24; ++i) { + const unsigned long long x = at(l, i); + if (x == 0 || x < prev) continue; + prof_sum_[kind][i] += (double) (x - prev); + prev = x; + } + 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] += gap(at(L, 1), at(L, 0)); + ++prof_windows_; +} + bool Verifier::run(int T, const int32_t* tokens, int64_t pos0, PoolMultiFn pool, void* user, int32_t* out, std::string& err) { using namespace strata::kernels; const OnDevice on_device(device_); + last_batch_ = false; if (T < 1 || T > max_t_) { err = "verify: window size out of range"; return false; } if (released_.load()) { err = "verify: an earlier window never finished on the GPU (#267); restart the engine"; return false; } const ModelGeometry& g = *g_; @@ -1031,37 +1400,26 @@ bool Verifier::run(int T, const int32_t* tokens, int64_t pos0, PoolMultiFn pool, if (!capture(T, err) || !capture_commit(err)) return false; VDBG("captured; staging\n"); const Clock::time_point t0 = Clock::now(); - const QsaShapes s = shapes_of(g); - for (int t = 0; t < T; ++t) { - h_tok_[t] = tokens[t]; - qsa_step_fill(h_step_ + t * kStepCount, pos0 + t, s); - for (int64_t h = 0; h < g.n_head; ++h) h_pos_[t * g.n_head + h] = (int32_t) (pos0 + t); - int32_t* pk = h_pos_ + (size_t) max_t_ * g.n_head; - int32_t* pi = pk + (size_t) max_t_ * g.n_head_kv; - for (int64_t h = 0; h < g.n_head_kv; ++h) pk[t * g.n_head_kv + h] = (int32_t) (pos0 + t); - for (int64_t h = 0; h < g.idx_q_heads; ++h) pi[t * g.idx_q_heads + h] = (int32_t) (pos0 + t); - } - if (ss.ple.ready() && ple_stage()) { - uint32_t rows[kVerifyMaxT * PLE_N_HEADS]; + if (!staged_) stage_inputs(T, tokens, pos0); + staged_ = false; + const bool do_ple = ss.ple.ready() && ple_stage(); + uint32_t ple_rows[kVerifyMaxT * PLE_N_HEADS]; + if (do_ple) { int32_t prev[2] = {ss.ple_prev[0], ss.ple_prev[1]}; for (int t = 0; t < T; ++t) { - ngram_rows(&tokens[t], prev, 1, ss.ple.consts, rows + t * PLE_N_HEADS); + ngram_rows(&tokens[t], prev, 1, ss.ple.consts, ple_rows + t * PLE_N_HEADS); prev[0] = prev[1]; prev[1] = tokens[t]; + ss.ple.table->prefetch_rows(ple_rows + t * PLE_N_HEADS); } - if (!ss.ple.table->gather_batch(rows, (size_t) T, h_ple_, err)) return false; } - *(volatile uint32_t*) h_seq_ = 0; - *(volatile uint32_t*) h_flag_ = 0; - *(volatile uint32_t*) h_flagA_ = 0; - *(volatile uint32_t*) h_flagB_ = 0; + if (trace_h_ != nullptr) std::memset(trace_h_, 0, trace_n_ * 8); // #649: this window's breadcrumbs only std::atomic_thread_fence(std::memory_order_seq_cst); - last_t_ = T; - last_pos0_ = pos0; - for (int t = 0; t < T; ++t) last_tokens_[t] = tokens[t]; + trace_ev("WINDOW", -1, -1, pos0 * 16 + T); ms_host += ms_since(t0); VDBG("staged; launching\n"); const cudaError_t le = cudaGraphLaunch(exec_[T], cs_); + trace_ev("LAUNCHED", -1, -1, (int64_t) le); if (le != cudaSuccess) { err = std::string("verify: launch: ") + cudaGetErrorString(le); return false; } (void) cudaStreamQuery(cs_); VDBG("launched\n"); @@ -1071,6 +1429,18 @@ bool Verifier::run(int T, const int32_t* tokens, int64_t pos0, PoolMultiFn pool, const int gtb[2] = {0, (T + 1) / 2}, gte[2] = {G == 2 ? (T + 1) / 2 : T, T}; const int64_t steps = (le_ - lb_) * G; const bool test_stall = g_test_stall > 0 && windows + 1 == g_test_stall; // #267 test hook (off: false) + if (all_resident_ && !test_stall) { + if (do_ple) { + const Clock::time_point tp = Clock::now(); + if (!ss.ple.table->gather_batch(ple_rows, (size_t) T, h_ple_, err)) return false; + std::atomic_thread_fence(std::memory_order_seq_cst); + _mm_sfence(); + *flag = 1; + ms_host += ms_since(tp); + } else { + *flag = 1; + } + } else for (int64_t k = 0; k < steps; ++k) { const int64_t l = lb_ + k / G; const int grp = (int) (k % G); @@ -1087,6 +1457,8 @@ bool Verifier::run(int T, const int32_t* tokens, int64_t pos0, PoolMultiFn pool, last_flush = now; const cudaError_t q = cudaStreamQuery(cs_); if (q != cudaErrorNotReady && *seq < want) { + trace_ev("NEVER-RANG", k, l, (int64_t) q); + trace_dump(stderr); err = "verify: layer " + std::to_string(l) + " never rang (" + (q == cudaSuccess ? std::string("graph finished") : std::string(cudaGetErrorString(q))) + ")"; return false; @@ -1094,20 +1466,33 @@ bool Verifier::run(int T, const int32_t* tokens, int64_t pos0, PoolMultiFn pool, } if (now - a > std::chrono::seconds(20)) { // #267: the caller ends the engine; no spin kernel may outlive it + trace_ev("TIMEOUT", k, l, (int64_t) cudaStreamQuery(cs_)); + if (g_trace) { + diag(stderr); // the words and the breadcrumbs before the release ... + const bool drained = release_gpu_waits(5000); + trace_dump(stderr); // ... and after it: did the GPU move once its waits were raised? + err = "verify: timed out at layer " + std::to_string(l) + released_note(drained); + return false; + } err = "verify: timed out at layer " + std::to_string(l) + released_note(release_gpu_waits(5000)); return false; } } const Clock::time_point b = Clock::now(); + if (g_trace) trace_ev("RANG", k, l, (int64_t) std::chrono::duration_cast(b - a).count()); VDBG("layer %lld rang\n", (long long) l); cur_layer_ = want - 1; set_plan_slot(grp); const int tb = gtb[grp], n = gte[grp] - gtb[grp]; progress_at("verify window: the CPU experts of layer", l); + if (remote_opt_) remote_opt_->begin(h_w_ + (size_t) tb * ss.k, tb, n); if (pool != nullptr) pool(user, h_x_ + (size_t) tb * g.n_embd, h_ids_ + (size_t) tb * ss.k, n, ss.k, h_ymiss_ + (size_t) tb * ss.k * g.n_embd, l); + if (remote_opt_) remote_opt_->end(); VDBG("layer %lld served\n", (long long) l); + if (g_trace) trace_ev(*(volatile uint32_t*) h_flagA_ == want ? "SERVED" : "SERVED-NO-PLAN-YET", k, l, + (int64_t) ms_since(b)); // aux: ms the CPU experts took progress_tick(); std::atomic_thread_fence(std::memory_order_seq_cst); _mm_sfence(); @@ -1121,52 +1506,39 @@ bool Verifier::run(int T, const int32_t* tokens, int64_t pos0, PoolMultiFn pool, *(volatile uint32_t*) h_flagA_ = want; raise_flag(h_flagB_, want); } + // Layer 1's pre(1, 0) copies h_ple_ -> ple_ after Layer 0's wait_flag_ge(m_flag_, 1). + // By collecting PLE here at k == 0 (after publishing flagA/flagB for Layer 0 so the GPU can run + // Layer 0's VRAM experts, and before raising *flag = 1), the NVMe PLE reads overlap with both + // MtpDrafter::draft and Layer 0's attention + router + expert execution! + if (k == 0 && do_ple) { + const Clock::time_point tp = Clock::now(); + if (!ss.ple.table->gather_batch(ple_rows, (size_t) T, h_ple_, err)) return false; + std::atomic_thread_fence(std::memory_order_seq_cst); + _mm_sfence(); + ms_host += ms_since(tp); + } if (!(test_stall && k + 1 == steps)) *flag = want; ms_wait += std::chrono::duration(b - a).count(); ms_pool += ms_since(b); } + // (#646 staged the next stage's inputs here; 0.1.39b keeps the layer split's order: each stage stages its own) progress_at("verify window: waiting for the GPU to finish the window (flags A/B/M raised)", (int64_t) T); // #267: a window the GPU never finishes (a spin kernel that never sees its flag) holds the host here; the stall // watchdog then releases every verifier's GPU waits (release_live_verifiers) before it ends the engine, so no // spin kernel outlives the process - the case that left Windows GPUs "lost" until a power cycle. The wait // itself stays a blocking sync: a cudaStreamQuery poll here cost IQ3_S ~3% decode (a core calling the driver // beside the expert workers). + trace_ev("SYNC", -1, -1, 0); const cudaError_t se = cudaStreamSynchronize(cs_); + trace_ev("SYNCED", -1, -1, (int64_t) se); if (se != cudaSuccess) { err = std::string("verify: ") + cudaGetErrorString(se); return false; } + commit_pending_ = false; progress_at("verify window: waiting for the expert copies", (int64_t) T); - cudaStreamSynchronize(copy_); // no host function of this window may raise flag B in the next one - if (prof_on_ && G == 1) { // the window's GPU stage stamps - 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); - for (int i = 1; i <= 24; ++i) { - const unsigned long long x = at(l, i); - if (x == 0 || x < prev) continue; - prof_sum_[kind][i] += (double) (x - prev); - prev = x; - } - 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] += gap(at(L, 1), at(L, 0)); - ++prof_windows_; + if (copy_used_) { + cudaStreamSynchronize(copy_); // no host function of this window may raise flag B in the next one + copy_used_ = false; } + if (prof_on_ && G == 1) collect_profile(); // the window's GPU stage stamps // ---- a sampled or penalized request: the head's sampling again, host-side so its parameters are this call's // own (a captured kernel would replay the same draws forever). Row t's draw is Philox(seed, pos0 + t): tied to // the POSITION it samples, not to how the text was cut into windows, so a seed replays the same text whatever @@ -1221,7 +1593,7 @@ void Verifier::set_plan_slot(int grp) { sink_.ptr = (unsigned long long*) (base + ptr_off); sink_.ptr2 = sink_.ptr + cap; sink_.start2 = base + ptr_off + 4 * cap; - const int G = groups_[last_t_] > 0 ? groups_[last_t_] : 1; + const int G = last_batch_ ? 1 : (groups_[last_t_] > 0 ? groups_[last_t_] : 1); const int64_t per = G == 2 ? kStagingBlobs / 2 : kStagingBlobs; sink_.staging = (unsigned long long) (staging_ + (size_t) (grp * per) * strata::kernels::cpu::expert_layout().max_blob); sink_.staging_cap = per; @@ -1250,6 +1622,7 @@ void Verifier::fetch_dma(void* ctx, const uint8_t* const* src, int n, size_t byt Verifier* v = (Verifier*) ctx; const uint32_t want = v->cur_layer_ + 1; if (n <= 0) { raise_flag(v->h_flagB_, want); return; } + v->copy_used_ = true; uint8_t* stage = (uint8_t*) v->sink_.staging; // this group's half in a split window for (int i = 0; i < n; ++i) cudaMemcpyAsync(stage + (size_t) i * bytes, src[i], bytes, cudaMemcpyHostToDevice, v->copy_); FlagSet& fs = v->flag_sets_[v->cur_layer_ % (sizeof v->flag_sets_ / sizeof v->flag_sets_[0])]; @@ -1342,6 +1715,7 @@ bool Verifier::commit(int n_keep, std::string& err) { } else { const cudaError_t re = cudaEventRecord(commit_done_, cs_); if (re != cudaSuccess) { err = std::string("verify: commit event: ") + cudaGetErrorString(re); return false; } + (void) cudaStreamQuery(cs_); commit_pending_ = true; } if (ple_stage()) // stages that share one session must advance it once @@ -1363,6 +1737,417 @@ bool Verifier::wait_commit(std::string& err) { return next_ == nullptr || next_->wait_commit(err); } + +// ================================ BATCH WINDOWS (see init_slots) ================================ + +bool Verifier::init_slots(const std::vector& slots, std::string& err) { + const OnDevice on_device(device_); + if (g_ == nullptr || ss_ == nullptr) { err = "verify: init_slots before init"; return false; } + if (slots.empty() || (int) slots.size() > max_t_) { + err = "verify: init_slots needs 1.." + std::to_string(max_t_) + " sessions"; + return false; + } + for (SessionState* x : slots) { + if (x == nullptr || x->layer_lo != ss_->layer_lo || x->layer_hi != ss_->layer_hi || + x->max_cells != ss_->max_cells || x->gdn_ord0 != ss_->gdn_ord0 || x->qsa_ord0 != ss_->qsa_ord0) { + err = "verify: a slot's session is not carved like the stage's own (layer range, context)"; + return false; + } + } + const strata::kernels::QsaShapes s = shapes_of(*g_); + const int64_t S = (int64_t) slots.size(), CB = 2 + max_t_; + const int64_t TS = (s.idx_block - 1) * g_->idx_key_dim, nQ = g_->n_qsa_layers(); + void* d = nullptr; + if (!mapped((size_t) (S * CB * 4 + 16), (void**) &h_commitb_, (void**) &m_commitb_)) { + err = "verify: the batch commit staging failed"; + return false; + } + const uint64_t a = ((uint64_t) S * CB * 4 + 255) & ~255ull; + if (cudaMalloc(&d, a + (uint64_t) S * std::max(nQ, 1) * TS * 4) != cudaSuccess) { + err = "verify: the batch buffers do not fit"; + return false; + } + arena_b_ = d; + commitb_ = (int32_t*) d; + tail_snap_b_ = (float*) ((uint8_t*) d + a); + slots_ = slots; + slot_sp_.assign(slots.size(), sampling_); // greedy until set_slot_sampling + std::fprintf(stderr, "strata verify: batch windows of up to %lld sequences (layers [%lld, %lld))\n", (long long) S, + (long long) lb_, (long long) le_); + return true; +} + +bool Verifier::capture_batch(const int* rows, int S, int hbase, std::string& err) { + cudaGraphExec_t& ex = exec_bm_[batch_key(rows, S, hbase)]; + if (ex != nullptr) return true; + if (cudaStreamBeginCapture(cs_, cudaStreamCaptureModeThreadLocal) != cudaSuccess) { + err = "verify: begin batch capture failed"; + return false; + } + batch_rec_ = true; + row_base_ = hbase; + for (int t = 0; t < S; ++t) brow_[t] = rows[t]; + std::string rerr; + const bool ok = record_window(S, cs_, rerr); + batch_rec_ = false; + row_base_ = 0; + cudaGraph_t graph = nullptr; + const cudaError_t ce = cudaStreamEndCapture(cs_, &graph); + if (!ok || ce != cudaSuccess) { + if (graph) cudaGraphDestroy(graph); + err = !ok ? rerr : std::string("verify: end batch capture: ") + cudaGetErrorString(ce); + return false; + } + const cudaError_t ie = cudaGraphInstantiate(&ex, graph, 0); + cudaGraphDestroy(graph); + if (ie != cudaSuccess) { err = std::string("verify: batch instantiate: ") + cudaGetErrorString(ie); return false; } + cudaGraphUpload(ex, cs_); + cudaStreamSynchronize(cs_); + std::string list; + for (int t = 0; t < S; ++t) list += (t ? "," : "") + std::to_string(rows[t]); + std::fprintf(stderr, "strata verify: captured the batch window over slots %s\n", list.c_str()); + return true; +} + +bool Verifier::capture_commit_batch(const int* rows, int S, int hbase, std::string& err) { + cudaGraphExec_t& cex = commit_bm_[batch_key(rows, S, hbase)]; + if (cex != nullptr) return true; + using namespace strata::kernels; + const ModelGeometry& g = *g_; + const QsaShapes s = shapes_of(g); + const int64_t C = g.ssm_conv_channels, HV = g.ssm_v_heads, ZV = g.ssm_value_dim, ID = g.idx_key_dim, MT = max_t_; + const int64_t CB = 2 + MT, nQ = g.n_qsa_layers(); + const uint64_t gdn_floats = (uint64_t) g.ssm_state_size * g.ssm_v_heads * g.ssm_state_size + + (uint64_t) g.ssm_conv_channels * (g.ssm_d_conv - 1); + const int64_t TS = (s.idx_block - 1) * ID; + const int64_t HS = (int64_t) NG_HIST * NG_HC_DIM; + if (cudaStreamBeginCapture(cs_, cudaStreamCaptureModeThreadLocal) != cudaSuccess) { + err = "verify: begin batch commit capture failed"; + return false; + } + bool ok = true; + try { + for (int t = 0; t < S; ++t) + copy_i32_from_mapped(commitb_ + (size_t) rows[t] * CB, m_commitb_ + (size_t) rows[t] * CB, CB, cs_); + int64_t qsa_index = 0, gdn_index = 0; + for (int64_t l = 0; l < lb_; ++l) (is_qsa_layer(g, l) ? qsa_index : gdn_index) += 1; + for (int64_t l = lb_; l < le_ && ok; ++l) { + const LayerView v(*wt_, l); + if (!is_qsa_layer(g, l)) { + const WeightRef* wnm = need(v, "ssm_norm.weight", err); + if (!wnm) { ok = false; break; } + for (int t = 0; t < S; ++t) { + SessionState& sx = *slots_[(size_t) rows[t]]; + const int32_t* keep = commitb_ + (size_t) rows[t] * CB; + float* state = sx.gdn_state + (size_t) (gdn_index - sx.gdn_ord0) * gdn_floats; + float* conv = state + (uint64_t) g.ssm_state_size * g.ssm_v_heads * g.ssm_state_size; + gdn_conv_commit(conv, qkv_L_ + (size_t) gdn_index * MT * C + (size_t) t * C, (int) C, keep, cs_); + gdn_step_norm_multi(state, h_L_ + (size_t) gdn_index * MT * C + (size_t) t * C, (int) C, + gate_L_ + (size_t) gdn_index * MT * HV + (size_t) t * HV, + beta_L_ + (size_t) gdn_index * MT * HV + (size_t) t * HV, z_ + (size_t) t * ZV, + (const float*) wnm->data, EPS, y_dummy_ + (size_t) t * ZV, (int) g.ssm_k_heads, + (int) HV, 1, keep, cs_); + } + ++gdn_index; + } else { + const WeightRef* wikn = need(v, "indexer.k_norm.weight", err); + if (!wikn) { ok = false; break; } + for (int t = 0; t < S; ++t) { + const QsaState& st = slots_[(size_t) rows[t]]->qsa_states[qsa_index]; + copy_from_mapped(st.idx_tail, tail_snap_b_ + ((size_t) rows[t] * nQ + qsa_index) * TS, TS, cs_); + const QsaIndexerBuffers ib{st.idx_tail, st.idx_dead, st.idx_pooled, st.idx_block_pos}; + native_qsa_indexer_append(idx_raw_L_ + (size_t) (qsa_index * MT + t) * ID, commitb_ + (size_t) rows[t] * CB + 2, + 0, (const float*) wikn->data, EPS, ib, s, st.max_cells, rope_scaling(), cs_); + } + ++qsa_index; + } + } + if (ok && ss_->ple.ready() && ple_stage()) + for (int t = 0; t < S; ++t) + copy_indexed(slots_[(size_t) rows[t]]->ple_hist, hist_snap_ + (size_t) t * HS, HS, + commitb_ + (size_t) rows[t] * CB + 1, HS, cs_); + } catch (const std::exception& e) { + err = std::string("verify batch commit: ") + e.what(); + ok = false; + } + cudaGraph_t graph = nullptr; + const cudaError_t ce = cudaStreamEndCapture(cs_, &graph); + if (!ok) { + if (graph) cudaGraphDestroy(graph); + return false; + } + if (ce != cudaSuccess || cudaGraphInstantiate(&cex, graph, 0) != cudaSuccess) { + if (graph) cudaGraphDestroy(graph); + err = std::string("verify: batch commit capture: ") + cudaGetErrorString(ce); + return false; + } + cudaGraphDestroy(graph); + return true; +} + +bool Verifier::stage_batch(const int* rows, int S, int hbase, const int32_t* tokens, const int64_t* pos, + std::string& err) { + using namespace strata::kernels; + if (S < 1 || S > max_t_ || hbase < 0 || hbase + S > (int) slots_.size()) { + err = "verify: batch rows out of range (init_slots)"; + return false; + } + for (int t = 0; t < S; ++t) { + bool dup = false; + for (int u = 0; u < t; ++u) dup = dup || rows[u] == rows[t]; + if (rows[t] < 0 || rows[t] >= (int) slots_.size() || dup) { err = "verify: a batch row's slot is out of range or twice"; return false; } + } + if (released_.load()) { err = "verify: an earlier window never finished on the GPU (#267); restart the engine"; return false; } + const ModelGeometry& g = *g_; + for (int t = 0; t < S; ++t) + if (pos[t] < 0 || pos[t] + 1 > slots_[(size_t) rows[t]]->max_cells) { + err = "verify: slot " + std::to_string(rows[t]) + " runs past its context"; + return false; + } + if (!capture_batch(rows, S, hbase, err) || !capture_commit_batch(rows, S, hbase, err)) return false; + const Clock::time_point t0 = Clock::now(); + const QsaShapes s = shapes_of(g); + for (int t = 0; t < S; ++t) { + h_tok_[t] = tokens[t]; + qsa_step_fill(h_step_ + t * kStepCount, pos[t], s); + for (int64_t h = 0; h < g.n_head; ++h) h_pos_[t * g.n_head + h] = (int32_t) pos[t]; + int32_t* pk = h_pos_ + (size_t) max_t_ * g.n_head; + int32_t* pi = pk + (size_t) max_t_ * g.n_head_kv; + for (int64_t h = 0; h < g.n_head_kv; ++h) pk[t * g.n_head_kv + h] = (int32_t) pos[t]; + for (int64_t h = 0; h < g.idx_q_heads; ++h) pi[t * g.idx_q_heads + h] = (int32_t) pos[t]; + } + if (ss_->ple.ready() && ple_stage()) { + uint32_t ple_rows[kVerifyMaxT * PLE_N_HEADS]; // (not `rows`: that is the slots of the window's rows) + for (int t = 0; t < S; ++t) { + const SessionState& sx = *slots_[(size_t) rows[t]]; + int32_t prev[2] = {sx.ple_prev[0], sx.ple_prev[1]}; + ngram_rows(&tokens[t], prev, 1, ss_->ple.consts, ple_rows + t * PLE_N_HEADS); + } + if (!ss_->ple.table->gather_batch(ple_rows, (size_t) S, h_ple_, err)) return false; + } + // the commit's rows (a batch window keeps every row): [1, 0, position, -1 ..] per slot + const int64_t CB = 2 + max_t_; + for (int t = 0; t < S; ++t) { + int32_t* c = h_commitb_ + (size_t) rows[t] * CB; + c[0] = 1; + c[1] = 0; + c[2] = (int32_t) pos[t]; + for (int64_t j = 3; j < CB; ++j) c[j] = -1; + } + *(volatile uint32_t*) h_seq_ = 0; + *(volatile uint32_t*) h_flag_ = 0; + *(volatile uint32_t*) h_flagA_ = 0; + *(volatile uint32_t*) h_flagB_ = 0; + std::atomic_thread_fence(std::memory_order_seq_cst); + last_t_ = S; + for (int t = 0; t < S; ++t) last_rows_[t] = rows[t]; + row_base_ = hbase; // the graphs' key (and nothing else) reads it until the next stage_batch + last_batch_ = true; + for (int t = 0; t < S; ++t) { last_tokens_[t] = tokens[t]; last_pos_b_[t] = pos[t]; } + ms_host += ms_since(t0); + return true; +} + +bool Verifier::run_slots(int S, const int32_t* tokens, const int64_t* pos, PoolMultiFn pool, void* user, int32_t* out, + std::string& err) { + int rows[8] = {}; + for (int t = 0; t < S && t < 8; ++t) rows[t] = t; + return run_slot_rows(rows, S, tokens, pos, pool, user, out, err); +} + +bool Verifier::run_slot_rows(const int* rows, int S, const int32_t* tokens, const int64_t* pos, PoolMultiFn pool, + void* user, int32_t* out, std::string& err) { + using namespace strata::kernels; + const OnDevice on_device(device_); + const ModelGeometry& g = *g_; + if (!stage_batch(rows, S, 0, tokens, pos, err)) return false; + const cudaError_t le = cudaGraphLaunch(exec_bm_[batch_key(rows, S, 0)], cs_); + if (le != cudaSuccess) { err = std::string("verify: batch launch: ") + cudaGetErrorString(le); return false; } + (void) cudaStreamQuery(cs_); + volatile uint32_t* const seq = h_seq_; + volatile uint32_t* const flag = h_flag_; + const int64_t steps = le_ - lb_; + for (int64_t k = 0; k < steps; ++k) { + const int64_t l = lb_ + k; + const uint32_t want = (uint32_t) (k + 1); + const Clock::time_point a = Clock::now(); + auto last_flush = a; + uint32_t spins = 0; + progress_at("verify batch: waiting for the GPU to reach layer", l); + while (*seq < want) { + _mm_pause(); + if ((++spins & 1023u) != 0) continue; + const auto now = Clock::now(); + if (now - last_flush > std::chrono::microseconds(2000)) { + last_flush = now; + const cudaError_t q = cudaStreamQuery(cs_); + if (q != cudaErrorNotReady && *seq < want) { + err = "verify batch: layer " + std::to_string(l) + " never rang (" + + (q == cudaSuccess ? std::string("graph finished") : std::string(cudaGetErrorString(q))) + ")"; + return false; + } + } + if (now - a > std::chrono::seconds(20)) { + err = "verify batch: timed out at layer " + std::to_string(l) + released_note(release_gpu_waits(5000)); + return false; + } + } + const Clock::time_point b = Clock::now(); + cur_layer_ = want - 1; + set_plan_slot(0); + progress_at("verify batch: the CPU experts of layer", l); + if (pool != nullptr) pool(user, h_x_, h_ids_, S, ss_->k, h_ymiss_, l); + progress_tick(); + std::atomic_thread_fence(std::memory_order_seq_cst); + _mm_sfence(); + if (*(volatile uint32_t*) h_flagA_ != want) { // the pool did not publish a plan: an empty one + sink_.counts[0] = 0; + sink_.counts[1] = 0; + sink_.counts[2] = 0; + sink_.start[0] = 0; + sink_.start2[0] = 0; + std::atomic_thread_fence(std::memory_order_seq_cst); + *(volatile uint32_t*) h_flagA_ = want; + raise_flag(h_flagB_, want); + } + *flag = want; + ms_wait += std::chrono::duration(b - a).count(); + ms_pool += ms_since(b); + } + const cudaError_t se = cudaStreamSynchronize(cs_); + if (se != cudaSuccess) { err = std::string("verify batch: ") + cudaGetErrorString(se); return false; } + cudaStreamSynchronize(copy_); + if (prof_on_) collect_profile(); + ++windows; + if (le_ < g.n_layers) return next_ == nullptr || next_->run_slot_rows(rows, S, tokens, pos, pool, next_user_, out, err); + if (!sample_rows(S, err)) return false; + for (int t = 0; t < S; ++t) out[t] = ((volatile int32_t*) h_out_)[t]; + progress_at("decode"); + progress_beat(); + return true; +} + +bool Verifier::commit_slots(std::string& err) { + const OnDevice on_device(device_); + if (!last_batch_ || last_t_ < 1) { err = "verify: commit_slots without a batch window"; return false; } + const int S = last_t_; + const Clock::time_point t0 = Clock::now(); + std::atomic_thread_fence(std::memory_order_seq_cst); + const cudaError_t le = cudaGraphLaunch(commit_bm_[batch_key(last_rows_, S, row_base_)], cs_); + if (le != cudaSuccess) { err = std::string("verify: batch commit launch: ") + cudaGetErrorString(le); return false; } + const cudaError_t se = cudaStreamSynchronize(cs_); + if (se != cudaSuccess) { err = std::string("verify: batch commit: ") + cudaGetErrorString(se); return false; } + if (ple_stage()) + for (int t = 0; t < S; ++t) { + SessionState& sx = *slots_[(size_t) last_rows_[t]]; + sx.ple_prev[0] = sx.ple_prev[1]; + sx.ple_prev[1] = last_tokens_[t]; + } + ms_commit += ms_since(t0); + return next_ == nullptr || next_->commit_slots(err); +} + +bool Verifier::sample_rows(int S, std::string& err) { + bool any = false; + for (int t = 0; t < S; ++t) { + strata::kernels::SamplerParams sp = slot_sp_[(size_t) last_rows_[t]]; + if (sp.greedy || sp.temperature <= 0.0f) continue; + sp.counter = (uint64_t) last_pos_b_[t]; // Philox(seed, position): the solo window's draw for this position + sp.penalty_last_n = 0; + strata::kernels::sample_tokens(head_logits_ + (size_t) t * (size_t) n_vocab_, 1, (int) n_vocab_, nullptr, 0, sp, + m_out_ + t, cs_); + any = true; + } + if (any && cudaStreamSynchronize(cs_) != cudaSuccess) { err = "verify batch: the row sampling failed"; return false; } + return true; +} + +bool Verifier::batch_launch(int base, int S, const int32_t* tokens, const int64_t* pos, std::string& err) { + const OnDevice on_device(device_); + if (b_running_) { err = "verify: batch_launch while this stage is busy"; return false; } + int rows[8] = {}; + for (int t = 0; t < S && t < 8; ++t) rows[t] = base + t; + if (!stage_batch(rows, S, base, tokens, pos, err)) return false; + cudaError_t le = cudaGraphLaunch(exec_bm_[batch_key(rows, S, base)], cs_); + if (le == cudaSuccess) le = cudaGraphLaunch(commit_bm_[batch_key(rows, S, base)], cs_); // right behind it: every row is kept + if (le != cudaSuccess) { err = std::string("verify: batch launch: ") + cudaGetErrorString(le); return false; } + (void) cudaStreamQuery(cs_); + if (ple_stage()) // the host's side of the commit (the hash's last two tokens) + for (int t = 0; t < S; ++t) { + SessionState& sx = *slots_[(size_t) rows[t]]; + sx.ple_prev[0] = sx.ple_prev[1]; + sx.ple_prev[1] = tokens[t]; + } + b_running_ = true; + b_k_ = 0; + b_steps_ = le_ - lb_; + b_last_ = Clock::now(); + return true; +} + +int Verifier::batch_poll(PoolMultiFn pool, void* user, std::string& err) { + if (!b_running_) return 1; + const OnDevice on_device(device_); + volatile uint32_t* const seq = h_seq_; + const int S = last_t_; + while (b_k_ < b_steps_) { + const uint32_t want = (uint32_t) (b_k_ + 1); + if (*seq < want) { + const auto now = Clock::now(); + if (now - b_last_ > std::chrono::milliseconds(2)) { + const cudaError_t q = cudaStreamQuery(cs_); + if (q != cudaErrorNotReady && *seq < want) { + err = "verify batch: layer " + std::to_string(lb_ + b_k_) + " never rang (" + + (q == cudaSuccess ? std::string("graph finished") : std::string(cudaGetErrorString(q))) + ")"; + b_running_ = false; + return -1; + } + if (now - b_last_ > std::chrono::seconds(20)) { + err = "verify batch: timed out at layer " + std::to_string(lb_ + b_k_) + released_note(release_gpu_waits(5000)); + b_running_ = false; + return -1; + } + } + return 0; + } + const Clock::time_point b = Clock::now(); + cur_layer_ = want - 1; + set_plan_slot(0); + if (pool != nullptr) pool(user, h_x_, h_ids_, S, ss_->k, h_ymiss_, lb_ + b_k_); + progress_tick(); + std::atomic_thread_fence(std::memory_order_seq_cst); + _mm_sfence(); + if (*(volatile uint32_t*) h_flagA_ != want) { // the pool did not publish a plan: an empty one + sink_.counts[0] = 0; + sink_.counts[1] = 0; + sink_.counts[2] = 0; + sink_.start[0] = 0; + sink_.start2[0] = 0; + std::atomic_thread_fence(std::memory_order_seq_cst); + *(volatile uint32_t*) h_flagA_ = want; + raise_flag(h_flagB_, want); + } + *(volatile uint32_t*) h_flag_ = want; + ms_pool += ms_since(b); + b_last_ = Clock::now(); + ++b_k_; + } + const cudaError_t q = cudaStreamQuery(cs_); + if (q == cudaErrorNotReady) return 0; + if (q != cudaSuccess) { err = std::string("verify batch: ") + cudaGetErrorString(q); b_running_ = false; return -1; } + const cudaError_t qc = cudaStreamQuery(copy_); // no host function of this window may raise flag B in the next + if (qc == cudaErrorNotReady) return 0; + if (prof_on_) collect_profile(); + if (last_stage()) { + if (!sample_rows(S, err)) { b_running_ = false; return -1; } + for (int t = 0; t < S; ++t) b_out_[t] = ((volatile int32_t*) h_out_)[t]; + } + ++windows; + b_running_ = false; + progress_beat(); + return 1; +} + 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; diff --git a/src/kernels/cpu/expert_layout.cpp b/src/kernels/cpu/expert_layout.cpp index 4583b1077c..8b6e3bc2c5 100644 --- a/src/kernels/cpu/expert_layout.cpp +++ b/src/kernels/cpu/expert_layout.cpp @@ -21,9 +21,26 @@ ExpertLayout g_layout; const ExpertLayout& expert_layout() { return g_layout; } +int cpu_isa_cap() { + // STRATA_FORCE_ISA (tests, the experimental older-CPU builds): the engine's own dispatch acts as if this CPU + // stopped at that level. ggml-cpu is not affected: it runs what the build compiled it for. + static const int cap = [] { + const char* f = std::getenv("STRATA_FORCE_ISA"); + if (f == nullptr || f[0] == '\0') return 3; + const std::string v(f); + if (v == "avx2") return 2; + if (v == "avx") return 1; + if (v == "sse" || v == "sse4.2" || v == "none") return 0; + std::fprintf(stderr, "strata: STRATA_FORCE_ISA=%s is not avx2, avx or sse; ignored\n", f); + return 3; + }(); + return cap; +} + bool cpu_avx512_ok() { static const bool ok = [] { if (const char* f = std::getenv("STRATA_FORCE_AVX2"); f != nullptr && f[0] == '1') return false; + if (cpu_isa_cap() < 3) return false; unsigned r[4] = {0, 0, 0, 0}; auto cpuid = [&](unsigned leaf, unsigned sub) { #if defined(_MSC_VER) @@ -53,8 +70,41 @@ bool cpu_avx512_ok() { return ok; } +bool cpu_avx512bw_ok() { + static const bool ok = [] { + if (const char* f = std::getenv("STRATA_FORCE_AVX2"); f != nullptr && f[0] == '1') return false; + unsigned r[4] = {0, 0, 0, 0}; + auto cpuid = [&](unsigned leaf, unsigned sub) { +#if defined(_MSC_VER) + int x[4]; + __cpuidex(x, (int) leaf, (int) sub); + for (int i = 0; i < 4; ++i) r[i] = (unsigned) x[i]; +#else + __cpuid_count(leaf, sub, r[0], r[1], r[2], r[3]); +#endif + }; + cpuid(0, 0); + if (r[0] < 7) return false; + cpuid(1, 0); + if (!((r[2] >> 27) & 1u)) return false; // OSXSAVE +#if defined(_MSC_VER) + const unsigned long long xcr0 = _xgetbv(0); +#else + unsigned lo = 0, hi = 0; + __asm__ volatile("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0)); + const unsigned long long xcr0 = ((unsigned long long) hi << 32) | lo; +#endif + if ((xcr0 & 0xE6) != 0xE6) return false; // the OS saves the AVX-512 state + cpuid(7, 0); + const unsigned ebx = r[1]; + return ((ebx >> 16) & 1u) && ((ebx >> 17) & 1u) && ((ebx >> 30) & 1u) && ((ebx >> 31) & 1u); + }(); + return ok; +} + bool cpu_avx2_ok() { static const bool ok = [] { + if (cpu_isa_cap() < 2) return false; unsigned r[4] = {0, 0, 0, 0}; auto cpuid = [&](unsigned leaf, unsigned sub) { #if defined(_MSC_VER) @@ -85,6 +135,57 @@ bool cpu_avx2_ok() { return ok; } +bool cpu_avx1_ok() { + // AVX (Sandy Bridge, 2011): AVX + OSXSAVE with the OS saving the YMM state; FMA, F16C and AVX2 not needed. + // From the Strata_Dirigo fork (rwkeyes). + static const bool ok = [] { + if (cpu_isa_cap() < 1) return false; + unsigned r[4] = {0, 0, 0, 0}; +#if defined(_MSC_VER) + int x[4]; + __cpuidex(x, 1, 0); + for (int i = 0; i < 4; ++i) r[i] = (unsigned) x[i]; +#else + __cpuid_count(1, 0, r[0], r[1], r[2], r[3]); +#endif + if (!((r[2] >> 27) & 1u) || !((r[2] >> 28) & 1u)) return false; // OSXSAVE, AVX +#if defined(_MSC_VER) + const unsigned long long xcr0 = _xgetbv(0); +#else + unsigned lo = 0, hi = 0; + __asm__ volatile("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0)); + const unsigned long long xcr0 = ((unsigned long long) hi << 32) | lo; +#endif + return (xcr0 & 0x6) == 0x6; + }(); + return ok; +} + +bool cpu_sse42_ok() { + static const bool ok = [] { + unsigned r[4] = {0, 0, 0, 0}; +#if defined(_MSC_VER) + int x[4]; + __cpuidex(x, 1, 0); + for (int i = 0; i < 4; ++i) r[i] = (unsigned) x[i]; +#else + __cpuid_count(1, 0, r[0], r[1], r[2], r[3]); +#endif + return ((r[2] >> 20) & 1u) && ((r[2] >> 23) & 1u); // SSE4.2, POPCNT + }(); + return ok; +} + +const char* isa_floor_build() { +#if defined(STRATA_ISA_FLOOR_AVX) + return "avx"; +#elif defined(STRATA_ISA_FLOOR_NONE) + return "sse4.2"; +#else + return ""; +#endif +} + std::string cpu_name() { unsigned r[12] = {}; #if defined(_MSC_VER) diff --git a/src/kernels/cpu/iq_avx2.cpp b/src/kernels/cpu/iq_avx2.cpp index 09d178db6b..bb8849bb51 100644 --- a/src/kernels/cpu/iq_avx2.cpp +++ b/src/kernels/cpu/iq_avx2.cpp @@ -56,19 +56,30 @@ inline __m256i sc32(int s) { return _mm256_set1_epi16(s); } // keven_signs_q2xs (ggml keeps it static in arch/x86/quants.c): one u64 per 7-bit sign index, byte k // = 0xFF when bit k of ksigns_iq2xs[i] is set, 0x01 otherwise. With this a whole 32-value sign vector // is four scalar loads and a set_epi64x - no ksigns byte packing chain and no bit_selector expansion. -// Built once at load time from the shared ggml-common table so it can never drift from it. +// +// Computed at COMPILE time (constexpr), so this file has no static constructor at all. This TU is compiled +// for AVX2, and a runtime constructor here runs before main() on every CPU: MSVC turned its variable shift +// into BMI2 `shlx` (#391, demetree: strata.exe exited 0xC000001D before printing anything on a Sandy Bridge +// Xeon) and GCC vectorised the loop into AVX2 (`vpbroadcastb`, found on an AVX-only Xeon E5 by the +// Strata_Dirigo fork). ksigns_iq2xs[i] is i's 7 bits plus an even-parity bit 7 (ggml-common.h); a const +// array is not usable in a constant expression, so the byte is derived the same way here, and +// native_expert_parity checks these kernels against ggml-cpu's. struct EvenSigns { uint64_t v[128]; - EvenSigns() { + constexpr EvenSigns() : v{} { for (int i = 0; i < 128; ++i) { + int par = 0; + for (int k = 0; k < 7; ++k) par ^= (i >> k) & 1; + const int s = i | (par << 7); // == ksigns_iq2xs[i] uint64_t r = 0; - for (int k = 0; k < 8; ++k) - r |= (uint64_t) (((ksigns_iq2xs[i] >> k) & 1) ? 0xFF : 0x01) << (8 * k); + for (int k = 0; k < 8; ++k) r |= (uint64_t) (((s >> k) & 1) ? 0xFF : 0x01) << (8 * k); v[i] = r; } } }; -static const EvenSigns even_signs; +static constexpr EvenSigns even_signs{}; +static_assert(even_signs.v[0] == 0x0101010101010101ull && even_signs.v[1] == 0xFF010101010101FFull, + "keven_signs_q2xs: byte k = 0xFF when bit k of ksigns_iq2xs[i] is set"); inline float hsum8(__m256 v) { const __m128 lo = _mm256_castps256_ps128(v), hi = _mm256_extractf128_ps(v, 1); @@ -95,6 +106,11 @@ inline void rows_ahead(const uint8_t* p) { _mm_prefetch((const char*) p + 64, _MM_HINT_T0); } +// E-2 on the AVX-2 path (the AVX-512 kernels' STRATA_IQ_GATHER): the IQ3 grids by one AVX2 gather instead of eight +// scalar loads assembled with set_epi32. AVX2 gather is a different instruction with worse throughput on some cores +// (opt-in, as on AVX-512); on the i7-12850HX it measures ~1.3x on the two 32-bit-grid formats, bit-exact. +static const bool gather = [] { const char* v = std::getenv("STRATA_IQ256_GATHER"); return v != nullptr && std::atoi(v) != 0; }(); + // ---- per format: one 32-value half (values 64*j + 32*half .. +31) -> grid magnitudes, sign vector, scales template struct Fmt32; @@ -152,8 +168,13 @@ template <> struct Fmt32<18> { // IQ3_XXS: d, qs[64] grid bytes, 8 x u32 (4 x static constexpr float K = 0.25f; static inline void decode(const uint8_t* b, int j, int half, __m256i& g, __m256i& sgn, __m256i& sc) { const uint8_t* q = b + 2 + 16 * j + 8 * half; - g = _mm256_set_epi32((int) iq3xxs_grid[q[7]], (int) iq3xxs_grid[q[6]], (int) iq3xxs_grid[q[5]], (int) iq3xxs_grid[q[4]], - (int) iq3xxs_grid[q[3]], (int) iq3xxs_grid[q[2]], (int) iq3xxs_grid[q[1]], (int) iq3xxs_grid[q[0]]); + if (gather) { + const __m256i idx = _mm256_cvtepu8_epi32(_mm_loadl_epi64((const __m128i*) q)); + g = _mm256_i32gather_epi32((const int*) iq3xxs_grid, idx, 4); + } else { + g = _mm256_set_epi32((int) iq3xxs_grid[q[7]], (int) iq3xxs_grid[q[6]], (int) iq3xxs_grid[q[5]], (int) iq3xxs_grid[q[4]], + (int) iq3xxs_grid[q[3]], (int) iq3xxs_grid[q[2]], (int) iq3xxs_grid[q[1]], (int) iq3xxs_grid[q[0]]); + } const uint32_t w = u32(b + 2 + 64 + 8 * j + 4 * half); sgn = _mm256_set_epi64x((long long) even_signs.v[(w >> 21) & 127], (long long) even_signs.v[(w >> 14) & 127], (long long) even_signs.v[(w >> 7) & 127], (long long) even_signs.v[w & 127]); @@ -167,9 +188,17 @@ template <> struct Fmt32<21> { // IQ3_S: d, qs[64], qh[8], signs[32], scales[4 static inline void decode(const uint8_t* b, int j, int half, __m256i& g, __m256i& sgn, __m256i& sc) { const uint8_t* q = b + 2 + 16 * j + 8 * half; const uint32_t h = b[66 + 2 * j + half]; + if (gather) { + const __m256i bits = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7); + __m256i idx = _mm256_cvtepu8_epi32(_mm_loadl_epi64((const __m128i*) q)); + idx = _mm256_add_epi32(idx, _mm256_slli_epi32( + _mm256_and_si256(_mm256_srlv_epi32(_mm256_set1_epi32((int) h), bits), _mm256_set1_epi32(1)), 8)); + g = _mm256_i32gather_epi32((const int*) iq3s_grid, idx, 4); + } else { #define G3(k) (int) iq3s_grid[q[k] | (((h >> k) & 1u) << 8)] - g = _mm256_set_epi32(G3(7), G3(6), G3(5), G3(4), G3(3), G3(2), G3(1), G3(0)); + g = _mm256_set_epi32(G3(7), G3(6), G3(5), G3(4), G3(3), G3(2), G3(1), G3(0)); #undef G3 + } const uint64_t m = u64(b + 74 + 8 * j); sgn = sgn_vec(half ? (uint32_t) (m >> 32) : (uint32_t) m); const uint8_t s = b[106 + j]; diff --git a/src/kernels/cpu/kq_avx1.cpp b/src/kernels/cpu/kq_avx1.cpp new file mode 100644 index 0000000000..fbdf73f40f --- /dev/null +++ b/src/kernels/cpu/kq_avx1.cpp @@ -0,0 +1,71 @@ +// src/kernels/cpu/kq_avx1.cpp - the AVX1 router dot for older CPUs (see kq_avx1.hpp). From rwkeyes' Strata_Dirigo +// fork (MIT), its AVX1 floor. +// +// Measured on the hardware this exists for (Xeon E5-2665, 2x8 cores, AVX only) for one layer's router +// [512 experts x 5120 embeddings]: the scalar std::fma fallback it replaces took 362 ms/layer, a scalar +// mul+add 4.9 ms, and this kernel 2.6 ms - 137x, with no extra memory (the weights stay bf16). +// +// Compiled for AVX (/arch:AVX, -mavx; CMakeLists.txt sets that per source); it is only ever called behind +// cpu_avx1_ok(), so no CPU that lacks AVX can reach it. +#include "strata/kernels/cpu/kq_avx1.hpp" + +#include + +#include + +#if !defined(__AVX__) +#error "kq_avx1.cpp must be compiled with AVX enabled (see the per-source flags in CMakeLists.txt)" +#endif + +namespace strata::kernels::cpu { +namespace { + +// 8 floats -> one float, the same reduction order the AVX2 kernel's hsum_float_8 uses. +inline float hsum_float_8_avx1(__m256 v) { + __m128 lo = _mm256_castps256_ps128(v); + __m128 hi = _mm256_extractf128_ps(v, 1); + lo = _mm_add_ps(lo, hi); + lo = _mm_add_ps(lo, _mm_movehl_ps(lo, lo)); + lo = _mm_add_ss(lo, _mm_shuffle_ps(lo, lo, 0x55)); + return _mm_cvtss_f32(lo); +} + +// 8 bf16 -> 8 f32: a bf16 is the top half of its f32, so widen in 128-bit halves (AVX1 has no +// 256-bit integer ops) and shift each 32-bit lane into the high half. +inline __m256 bf16x8_to_f32x8_avx1(const uint16_t* p) { + const __m128i v = _mm_loadu_si128((const __m128i*) p); + const __m128i z = _mm_setzero_si128(); + const __m128 lo = _mm_castsi128_ps(_mm_slli_epi32(_mm_unpacklo_epi16(v, z), 16)); + const __m128 hi = _mm_castsi128_ps(_mm_slli_epi32(_mm_unpackhi_epi16(v, z), 16)); + return _mm256_insertf128_ps(_mm256_castps128_ps256(lo), hi, 1); +} + +} // namespace + +void bf16_rows_dot_multi_avx1(const uint16_t* w, int rows, int cols, const float* x, int nt, float* out) { + // each row is read once for all `nt` (<= 8) tokens, exactly as the AVX2 kernel does + for (int r = 0; r < rows; ++r) { + const uint16_t* wr = w + (size_t) r * (size_t) cols; + __m256 acc[8]; + for (int t = 0; t < nt; ++t) acc[t] = _mm256_setzero_ps(); + int c = 0; + for (; c + 8 <= cols; c += 8) { + const __m256 wf = bf16x8_to_f32x8_avx1(wr + c); + for (int t = 0; t < nt; ++t) { + const __m256 xv = _mm256_loadu_ps(x + (size_t) t * (size_t) cols + c); + acc[t] = _mm256_add_ps(acc[t], _mm256_mul_ps(wf, xv)); // no FMA on AVX1: mul, then add + } + } + for (int t = 0; t < nt; ++t) { + float s = hsum_float_8_avx1(acc[t]); + for (; c < cols; ++c) { // cols % 8 != 0 tail (cols % 8 == 0 today) + const uint32_t bits = (uint32_t) wr[c] << 16; + float wf; std::memcpy(&wf, &bits, sizeof wf); + s += wf * x[(size_t) t * (size_t) cols + c]; + } + out[(size_t) t * (size_t) rows + r] = s; + } + } +} + +} // namespace strata::kernels::cpu diff --git a/src/kernels/cpu/native_expert.cpp b/src/kernels/cpu/native_expert.cpp index 0903761e61..0dc5d3aa84 100644 --- a/src/kernels/cpu/native_expert.cpp +++ b/src/kernels/cpu/native_expert.cpp @@ -84,7 +84,14 @@ void native_gu_rows(const NativeFmt& f, const uint8_t* blob, const void* const* // AVX-2 kernel, STRATA_NO_IQ256 drops the AVX-2 kernel; ggml-cpu's single-token vec_dot is reached only with // both set (and on a CPU without AVX-512, STRATA_NO_IQ512 changes nothing). static const bool avx512 = cpu_avx512_ok() && std::getenv("STRATA_NO_IQ512") == nullptr; - static const bool avx2 = std::getenv("STRATA_NO_IQ256") == nullptr; + // STRATA_NO_IQ256 drops the AVX-2 kernel. cpu_avx2_ok() is ALSO required: iq_avx2.cpp and kq_avx2.cpp + // are compiled /arch:AVX2 and use AVX2 and FMA3, so calling them on a CPU without either is an + // illegal instruction, not a slow path. The gate sat next to `avx512` above, which does test + // cpu_avx512_ok(), and that asymmetry is what let a pre-Haswell CPU reach iq256_gu_rows. + // Without it this falls through to ggml-cpu's vec_dot below, which is compiled for whatever + // baseline the build selected (AVX1 here) and covers the same types - IQ2_XXS and IQ2_S among + // them. That path loops over tokens itself, so it is correct for any `nt`, not just one. + static const bool avx2 = cpu_avx2_ok() && std::getenv("STRATA_NO_IQ256") == nullptr; // #152: from how many tokens the multi-token kernels run (ggml's vec_dot below that). The default 2 is the // measured-fastest rule, but a token's expert rows then round differently alone than in a group, so greedy output // can depend on how many drafts a verify window held. STRATA_IQ_MT_MIN=1 (opt-in, 0.1.30) uses the multi-token @@ -93,7 +100,7 @@ void native_gu_rows(const NativeFmt& f, const uint8_t* blob, const void* const* // Unsloth UD-Q4_K_XL's Q4_K gate/up: the multi-token kernel is bit-exact against ggml's per-token dot (any group // size, no #152 rule). Opt-in, STRATA_KQ256=1: measured no faster in the engine (a window's expert groups hold // ~1.4 tokens and the weights stay in L1 across ggml's per-token calls; 1.01-1.13x in native_expert_parity). - static const bool kq = [] { const char* v = std::getenv("STRATA_KQ256"); return v != nullptr && std::atoi(v) != 0; }(); + static const bool kq = [] { const char* v = std::getenv("STRATA_KQ256"); return cpu_avx2_ok() && v != nullptr && std::atoi(v) != 0; }(); if (kq && f.gu_type == 12 && nt >= 2) { // one token: ggml's own dot below (the same bits, less overhead) kq256_gu_rows(f.gu_type, blob, f.gu_row, f.up_off, (int) f.n_embd, act, nt, ff, r0, r1); return; @@ -133,11 +140,14 @@ void native_down_rows(const NativeFmt& f, const uint8_t* blob, const void* const static const bool iq4nl_mt = std::getenv("STRATA_NO_IQ4NL") == nullptr; static const int mt_min = [] { const char* e = std::getenv("STRATA_IQ_MT_MIN"); return e ? std::atoi(e) : 2; }(); static const bool kq = [] { const char* v = std::getenv("STRATA_KQ256"); return v != nullptr && std::atoi(v) != 0; }(); - if (kq && nt >= 2 && (f.d_type == 7 || f.d_type == 8)) { // Q5_1 / Q8_0 down: bit-exact, any group size + // Both multi-token kernels below are /arch:AVX2 translation units (kq_avx2.cpp and iq_avx2.cpp), + // so a CPU without AVX2 has to reach ggml-cpu's vec_dot instead - same reasoning as the gate/up + // rows above, where `avx512` tested cpu_avx512_ok() and `avx2` did not. + if (cpu_avx2_ok() && kq && nt >= 2 && (f.d_type == 7 || f.d_type == 8)) { // Q5_1 / Q8_0 down: bit-exact, any group size kq256_rows(f.d_type, blob + f.down_off, f.d_row, (int) f.n_ff, hq, nt, out, r0, r1); return; } - if (nt >= mt_min && f.d_type == 20 && iq4nl_mt) { // #152: the same rule as the gate/up rows + if (cpu_avx2_ok() && nt >= mt_min && f.d_type == 20 && iq4nl_mt) { // #152: the same rule as the gate/up rows iq4nl256_down_rows(blob + f.down_off, f.d_row, (int) f.n_ff, hq, nt, out, r0, r1); return; } diff --git a/src/kernels/cpu/pool.cpp b/src/kernels/cpu/pool.cpp index 45e4347037..c64462936b 100644 --- a/src/kernels/cpu/pool.cpp +++ b/src/kernels/cpu/pool.cpp @@ -14,9 +14,11 @@ #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #include +#include "pool_affinity_win.hpp" #else #include #include +#include "pool_affinity_linux.hpp" #endif namespace strata::kernels::cpu { @@ -36,7 +38,13 @@ CpuTopology detect_cpu_topology(bool skip_first, PoolAffinity affinity) { DWORD len = 0; GetLogicalProcessorInformationEx(RelationProcessorCore, nullptr, &len); if (len == 0) { - for (unsigned i = 0; i < std::thread::hardware_concurrency(); ++i) topo.worker_cores.push_back((int) i); + // Preserve group identity even when detailed core topology is unavailable. + const WORD groups = GetActiveProcessorGroupCount(); + for (WORD group = 0; group < groups; ++group) { + const DWORD count = GetActiveProcessorCount(group); + if (count == 0 || count == (DWORD) -1 || count > 64) continue; + for (DWORD i = 0; i < count; ++i) topo.worker_cores.push_back((int) group * 64 + (int) i); + } if (skip_first && !topo.worker_cores.empty()) { topo.host_core = topo.worker_cores.front(); topo.worker_cores.erase(topo.worker_cores.begin()); @@ -60,10 +68,12 @@ CpuTopology detect_cpu_topology(bool skip_first, PoolAffinity affinity) { 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)); + for (WORD group = 0; group < e->Processor.GroupCount; ++group) { + const GROUP_AFFINITY& g = e->Processor.GroupMask[group]; + for (int bit = 0; bit < 64; ++bit) { + if (g.Mask & (KAFFINITY(1) << bit)) { + cd.lps.push_back((int) (g.Group * 64 + bit)); + } } } if (!cd.lps.empty()) { @@ -95,6 +105,12 @@ CpuTopology detect_cpu_topology(bool skip_first, PoolAffinity affinity) { } if (affinity == PoolAffinity::All || !topo.is_hybrid) { + // #642 (from Hardin22's fork): on a hybrid CPU the P-cores first (the host takes the first of them), so a + // pool smaller than the core count (setup's --pool-workers for a hybrid CPU) runs on the P-cores and the + // first E-cores rather than on whatever the OS numbered first. All cores alike: the order is unchanged. + if (topo.is_hybrid) + std::stable_sort(descs.begin(), descs.end(), + [](const CoreDesc& x, const CoreDesc& y) { return x.efficiency > y.efficiency; }); 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(); @@ -162,12 +178,8 @@ CpuTopology detect_cpu_topology(bool skip_first, PoolAffinity affinity) { }; std::vector allowed; - cpu_set_t set; - CPU_ZERO(&set); - if (sched_getaffinity(0, sizeof set, &set) == 0) { - for (int i = 0; i < CPU_SETSIZE; ++i) - if (CPU_ISSET(i, &set)) allowed.push_back(i); - } else { + std::vector allowed_mask; + if (detail::get_thread_affinity(allowed_mask, &allowed) != 0) { for (unsigned i = 0; i < std::thread::hardware_concurrency(); ++i) allowed.push_back((int) i); } @@ -219,6 +231,9 @@ CpuTopology detect_cpu_topology(bool skip_first, PoolAffinity affinity) { } if (affinity == PoolAffinity::All || !topo.is_hybrid) { + if (topo.is_hybrid) // #642: the P-cores first (see the Windows branch) + std::stable_sort(all_cpus.begin(), all_cpus.end(), + [](const CoreLinux& x, const CoreLinux& y) { return x.cap > y.cap; }); for (const auto& cl : all_cpus) { if (!cl.is_sibling) topo.worker_cores.push_back(cl.cpu); } @@ -264,49 +279,60 @@ std::vector physical_cores(bool skip_first, PoolAffinity affinity) { namespace { -void pin_this_thread(int core) { - if (core < 0) return; +bool pin_this_thread(int core, [[maybe_unused]] int worker = -1) { + if (core < 0) return false; #if defined(_WIN32) - SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR) 1 << (core & 63)); + return detail::set_thread_group_affinity(core, worker); #else - cpu_set_t set; - CPU_ZERO(&set); - CPU_SET(core, &set); - pthread_setaffinity_np(pthread_self(), sizeof set, &set); + const int error = detail::pin_thread_to_cpu(core); + if (error != 0) + std::fprintf(stderr, "strata cpu pool: affinity for worker %d (CPU %d) failed: %d; previous affinity kept\n", + worker, core, error); + return error == 0; #endif } } // namespace -long long pin_current_thread(int core) { - if (core < 0) return -1; +ThreadAffinity pin_current_thread(int core) { + if (core < 0) return {}; #if defined(_WIN32) - // `SetThreadAffinityMask` RETURNS the previous mask, or 0 on failure - so 0 doubles as the error, which is - // why the caller must not treat it as a restorable value. - const DWORD_PTR prev = SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR) 1 << (core & 63)); - return prev == 0 ? -1 : (long long) prev; + ThreadAffinity previous; + ULONG target = 0; + if (!detail::get_thread_cpu_sets(previous.cpu_sets) || !detail::cpu_set_for_core(core, target) || + !SetThreadSelectedCpuSets(GetCurrentThread(), &target, 1)) { + std::fprintf(stderr, "strata cpu pool: host CPU Set selection for processor %d failed: %lu; previous placement kept\n", + core, (unsigned long) GetLastError()); + return {}; + } + previous.valid = true; + return previous; #else - cpu_set_t prev; - CPU_ZERO(&prev); - if (pthread_getaffinity_np(pthread_self(), sizeof prev, &prev) != 0) return -1; - unsigned long mask = 0; - for (int i = 0; i < CPU_SETSIZE && i < 64; ++i) - if (CPU_ISSET(i, &prev)) mask |= 1ul << i; - pin_this_thread(core); - return (long long) mask; + ThreadAffinity previous; + int error = detail::get_thread_affinity(previous.mask); + if (error == 0) error = detail::pin_thread_to_cpu(core); + if (error != 0) { + std::fprintf(stderr, "strata cpu pool: host affinity for CPU %d failed: %d; previous affinity kept\n", core, error); + return {}; + } + previous.valid = true; + return previous; #endif } -void restore_thread_affinity(long long previous) { - if (previous <= 0) return; +void restore_thread_affinity(const ThreadAffinity& previous) { + if (!previous.valid) return; #if defined(_WIN32) - SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR) previous); + // Clearing an originally empty selection restores process-default/all-group eligibility without + // turning the caller's implicit Windows 11 affinity into an explicit single-group hard mask. + if (!SetThreadSelectedCpuSets(GetCurrentThread(), previous.cpu_sets.empty() ? nullptr : previous.cpu_sets.data(), + (ULONG) previous.cpu_sets.size())) + std::fprintf(stderr, "strata cpu pool: host CPU Set restoration failed: %lu\n", + (unsigned long) GetLastError()); #else - cpu_set_t set; - CPU_ZERO(&set); - for (int i = 0; i < 64; ++i) - if ((previous >> i) & 1) CPU_SET(i, &set); - pthread_setaffinity_np(pthread_self(), sizeof set, &set); + const int error = detail::set_thread_affinity(previous.mask); + if (error != 0) + std::fprintf(stderr, "strata cpu pool: host affinity restoration failed: %d\n", error); #endif } @@ -366,7 +392,7 @@ ExpertPool::ExpertPool(int n_workers, bool pin, bool host_works, PoolAffinity af for (int i = 0; i < n_; ++i) { 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); + pin_this_thread(core, i); worker(i); }); } @@ -540,7 +566,7 @@ void ExpertPool::drain(int id, ExpertScratch& scratch, uint32_t epoch) { const int e = (int) (r / per), r0 = (int) (r % per); const int r1 = (int) std::min(per, r0 + (g1 - r)); SplitBufMulti& sb = split_multi_[(size_t) e]; - if (mode_ == 5 && nfmt_->gu_type == 42) { + if (mode_ == 5 && q2_native_kernels(nfmt_->gu_type)) { // a native Q2_0 pack: gate and up rows on the Q2_0 kernels, then SwiGLU thread_local float gbuf[MAXT][FF], ubuf[MAXT][FF]; float* gp[MAXT]; @@ -556,7 +582,7 @@ void ExpertPool::drain(int id, ExpertScratch& scratch, uint32_t epoch) { float* ff[MAXT]; for (int t = 0; t < mjobs_[e].nt; ++t) ff[t] = sb.ff[t]; native_gu_rows(*nfmt_, mjobs_[e].blob, mjobs_[e].nact, mjobs_[e].nt, ff, r0, r1); - } else if (nfmt_->d_type == 42) { + } else if (q2_native_kernels(nfmt_->d_type)) { // Q2_0 down (most IQ layers): the AVX-512 kernel, ggml-cpu has only a scalar one on x86 const ActQ* a2[MAXT]; for (int t = 0; t < mjobs_[e].nt; ++t) a2[t] = &sb.a2[t]; @@ -676,7 +702,7 @@ void ExpertPool::run_split_multi_native(const NativeFmt& f, ExpertJobMulti* jobs const auto b = std::chrono::steady_clock::now(); for (int e = 0; e < nb; ++e) for (int t = 0; t < mjobs_[e].nt; ++t) - if (f.d_type == 42) act_quant_any(split_multi_[(size_t) e].ff[t], FF, split_multi_[(size_t) e].a2[t]); + if (q2_native_kernels(f.d_type)) act_quant_any(split_multi_[(size_t) e].ff[t], FF, split_multi_[(size_t) e].a2[t]); else native_quant_h(f, split_multi_[(size_t) e].ff[t], split_multi_[(size_t) e].hq[t]); const auto c = std::chrono::steady_clock::now(); mrows_ = (int64_t) nb * H; diff --git a/src/kernels/cpu/pool_affinity_linux.hpp b/src/kernels/cpu/pool_affinity_linux.hpp new file mode 100644 index 0000000000..0791ac5c7a --- /dev/null +++ b/src/kernels/cpu/pool_affinity_linux.hpp @@ -0,0 +1,69 @@ +#pragma once + +#if defined(__linux__) +#include +#include + +#include +#include +#include +#include + +namespace strata::kernels::cpu::detail { + +// CPU_ALLOC and the sized macros also handle kernels whose CPU mask exceeds CPU_SETSIZE. +// Keep the native mask opaque: copy its bytes for storage and use CPU_*_S to inspect it. +class LinuxCpuSet { +public: + explicit LinuxCpuSet(int cpus) : bytes(CPU_ALLOC_SIZE(cpus)), set(CPU_ALLOC(cpus)) { + if (set) CPU_ZERO_S(bytes, set); + } + ~LinuxCpuSet() { CPU_FREE(set); } + LinuxCpuSet(const LinuxCpuSet&) = delete; + LinuxCpuSet& operator=(const LinuxCpuSet&) = delete; + + const size_t bytes; + cpu_set_t* const set; +}; + +// pthread affinity functions return an error number directly; they do not set errno. +// A too-small query buffer returns EINVAL, even when the number of online CPUs is small. +inline int get_thread_affinity(std::vector& mask, std::vector* cpus = nullptr) { + for (int count = CPU_SETSIZE;;) { + LinuxCpuSet current(count); + if (!current.set) return ENOMEM; + const int error = pthread_getaffinity_np(pthread_self(), current.bytes, current.set); + if (error == 0) { + mask.resize(current.bytes / sizeof(unsigned long)); + std::memcpy(mask.data(), current.set, current.bytes); + if (cpus) { + cpus->clear(); + for (int cpu = 0; cpu < count; ++cpu) + if (CPU_ISSET_S(cpu, current.bytes, current.set)) cpus->push_back(cpu); + } + return 0; + } + if (error != EINVAL || count > INT_MAX / 2) return error; + count *= 2; + } +} + +inline int set_thread_affinity(const std::vector& mask) { + constexpr size_t word_bits = sizeof(unsigned long) * CHAR_BIT; + if (mask.empty() || mask.size() > INT_MAX / word_bits) return EINVAL; + LinuxCpuSet target(static_cast(mask.size() * word_bits)); + if (!target.set) return ENOMEM; + std::memcpy(target.set, mask.data(), target.bytes); + return pthread_setaffinity_np(pthread_self(), target.bytes, target.set); +} + +inline int pin_thread_to_cpu(int core) { + if (core < 0 || core == INT_MAX) return EINVAL; + LinuxCpuSet target(core < CPU_SETSIZE ? CPU_SETSIZE : core + 1); + if (!target.set) return ENOMEM; + CPU_SET_S(core, target.bytes, target.set); + return pthread_setaffinity_np(pthread_self(), target.bytes, target.set); +} + +} // namespace strata::kernels::cpu::detail +#endif diff --git a/src/kernels/cpu/pool_affinity_linux_test.cpp b/src/kernels/cpu/pool_affinity_linux_test.cpp new file mode 100644 index 0000000000..6b5752af1f --- /dev/null +++ b/src/kernels/cpu/pool_affinity_linux_test.cpp @@ -0,0 +1,290 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "strata/kernels/cpu/pool.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include "pool_affinity_linux.hpp" + +#include +#include +#include + +namespace cpu = strata::kernels::cpu; + +class DynamicCpuSet { +public: + explicit DynamicCpuSet(size_t count) + : count_(count), bytes_(CPU_ALLOC_SIZE(count)), set_(CPU_ALLOC(count)) { + if (set_) CPU_ZERO_S(bytes_, set_); + } + + ~DynamicCpuSet() { + if (set_) CPU_FREE(set_); + } + + DynamicCpuSet(const DynamicCpuSet&) = delete; + DynamicCpuSet& operator=(const DynamicCpuSet&) = delete; + + cpu_set_t* get() { return set_; } + const cpu_set_t* get() const { return set_; } + size_t bytes() const { return bytes_; } + size_t count() const { return count_; } + explicit operator bool() const { return set_ != nullptr; } + +private: + size_t count_; + size_t bytes_; + cpu_set_t* set_; +}; + +static size_t initial_cpu_count() { + const long configured = sysconf(_SC_NPROCESSORS_CONF); + return (std::max)(size_t(128), configured > 0 ? (size_t) configured : size_t(0)); +} + +// Query with CPU_ALLOC rather than cpu_set_t: this must observe CPUs beyond CPU_SETSIZE too. +static bool get_affinity(std::vector& ids) { + size_t count = initial_cpu_count(); + for (int attempt = 0; attempt < 8; ++attempt) { + DynamicCpuSet set(count); + if (!set) return false; + const int error = pthread_getaffinity_np(pthread_self(), set.bytes(), set.get()); + if (error == 0) { + ids.clear(); + for (size_t i = 0; i < set.count(); ++i) + if (CPU_ISSET_S(i, set.bytes(), set.get())) ids.push_back((int) i); + return !ids.empty(); + } + if (error != EINVAL || count > (size_t(1) << 20)) { + std::fprintf(stderr, "pthread_getaffinity_np failed: %s\n", std::strerror(error)); + return false; + } + count *= 2; + } + std::fprintf(stderr, "pthread_getaffinity_np still needs a larger CPU set\n"); + return false; +} + +static bool set_affinity(const std::vector& ids) { + if (ids.empty()) return false; + const int highest = *std::max_element(ids.begin(), ids.end()); + if (highest < 0) return false; + const size_t count = (std::max)(initial_cpu_count(), (size_t) highest + 1); + DynamicCpuSet set(count); + if (!set) return false; + for (int id : ids) { + if (id < 0 || (size_t) id >= count) return false; + CPU_SET_S((size_t) id, set.bytes(), set.get()); + } + const int error = pthread_setaffinity_np(pthread_self(), set.bytes(), set.get()); + if (error != 0) { + std::fprintf(stderr, "pthread_setaffinity_np failed: %s\n", std::strerror(error)); + return false; + } + return true; +} + +class OriginalAffinity { +public: + bool capture() { return get_affinity(ids_); } + const std::vector& ids() const { return ids_; } + bool restore() { return ids_.empty() || set_affinity(ids_); } + ~OriginalAffinity() { if (!ids_.empty()) set_affinity(ids_); } + +private: + std::vector ids_; +}; + +static bool expect_affinity(const std::vector& expected, const char* label) { + std::vector actual; + if (!get_affinity(actual)) { + std::fprintf(stderr, "Linux affinity test: %s: cannot query current affinity\n", label); + return false; + } + if (actual != expected) { + std::fprintf(stderr, "Linux affinity test: %s: expected %zu CPUs, observed %zu\n", + label, expected.size(), actual.size()); + return false; + } + return true; +} + +static bool round_trip_full_mask(const std::vector& allowed) { + const int target = allowed.front(); + const auto previous = cpu::pin_current_thread(target); + const bool pinned = previous.valid && expect_affinity({target}, "pin from full allowed mask"); + cpu::restore_thread_affinity(previous); + return pinned && expect_affinity(allowed, "restore full allowed mask"); +} + +static bool topology_respects_current_mask() { + std::vector allowed; + if (!get_affinity(allowed)) return false; + const auto topology = cpu::detect_cpu_topology(false); + if (topology.worker_cores.empty()) return false; + for (int core : topology.worker_cores) { + if (!std::binary_search(allowed.begin(), allowed.end(), core)) { + std::fprintf(stderr, "Linux affinity test: topology selected disallowed CPU %d\n", core); + return false; + } + } + return true; +} + +static bool round_trip_sparse_nested(const std::vector& allowed) { + if (allowed.size() < 2) { + std::puts("SKIP sparse/nested affinity: fewer than two allowed CPUs"); + return true; + } + const std::vector sparse{allowed.front(), allowed.back()}; + if (!set_affinity(sparse) || !expect_affinity(sparse, "establish sparse mask") || + !topology_respects_current_mask()) return false; + + const auto outer = cpu::pin_current_thread(sparse.front()); + const bool outer_pinned = outer.valid && expect_affinity({sparse.front()}, "outer pin"); + if (!outer_pinned) { + cpu::restore_thread_affinity(outer); + return false; + } + + const auto inner = cpu::pin_current_thread(sparse.back()); + const bool inner_pinned = inner.valid && expect_affinity({sparse.back()}, "inner pin"); + cpu::restore_thread_affinity(inner); + const bool outer_restored = expect_affinity({sparse.front()}, "restore inner pin to outer pin"); + cpu::restore_thread_affinity(outer); + const bool sparse_restored = expect_affinity(sparse, "restore outer pin to sparse mask"); + const bool full_restored = set_affinity(allowed) && expect_affinity(allowed, "restore full mask after sparse test"); + return inner_pinned && outer_restored && sparse_restored && full_restored; +} + +static bool round_trip_high_cpu(const std::vector& allowed, bool require_high) { + const auto it = std::find_if(allowed.begin(), allowed.end(), [](int id) { return id >= 64; }); + if (it == allowed.end()) { + if (require_high) { + std::fprintf(stderr, "Linux affinity test: --require-high-cpu requested, but no allowed CPU ID is >= 64\n"); + return false; + } + std::puts("SKIP high-CPU affinity path: no allowed CPU ID is >= 64 (use --require-high-cpu to require it)"); + return true; + } + const int target = allowed.front() == *it ? (allowed.size() > 1 ? allowed.back() : *it) : allowed.front(); + if (target == *it) { + if (require_high) { + std::fprintf(stderr, "Linux affinity test: --require-high-cpu needs at least two allowed CPUs to test restoration\n"); + return false; + } + std::puts("SKIP high-CPU affinity round trip: only one CPU is allowed, so no meaningful pin transition is possible"); + return true; + } + + // Restrict the caller to one high-numbered CPU, then pin to a different originally allowed CPU. + // Restoring the high-only mask catches truncation even when the original process mask is sparse. + const std::vector high_only{*it}; + if (!set_affinity(high_only) || !expect_affinity(high_only, "establish high-only mask") || + !topology_respects_current_mask()) return false; + const auto previous = cpu::pin_current_thread(target); + const bool pinned = previous.valid && expect_affinity({target}, "pin away from high-only mask"); + cpu::restore_thread_affinity(previous); + const bool restored = pinned && expect_affinity(high_only, "restore high-only mask"); + if (restored) std::printf("High-CPU affinity round trip: saved CPU %d, pin target %d\n", *it, target); + return restored; +} + +static bool negative_cpu_is_noop() { + std::vector before; + if (!get_affinity(before)) return false; + const auto invalid = cpu::pin_current_thread(-1); + std::vector after; + const bool unchanged = get_affinity(after) && before == after; + cpu::restore_thread_affinity(invalid); + return !invalid.valid && unchanged && expect_affinity(before, "negative CPU no-op"); +} + +static bool worker_thread_can_pin(int target) { + bool ok = false; + std::thread worker([&] { + std::vector before; + if (!get_affinity(before)) return; + std::vector saved; + if (cpu::detail::get_thread_affinity(saved) != 0) return; + const int pin_error = cpu::detail::pin_thread_to_cpu(target); + const bool pinned = pin_error == 0 && expect_affinity({target}, "worker thread helper pin") && + sched_getcpu() == target; + const int restore_error = cpu::detail::set_thread_affinity(saved); + ok = pinned && restore_error == 0 && expect_affinity(before, "worker thread helper restore"); + }); + worker.join(); + return ok; +} + +static bool failed_pin_preserves_affinity() { + std::vector before; + if (!get_affinity(before)) return false; + // An ID outside the caller's current mask may still be online and legal, so it is not a + // reliable failure case. INT_MAX is rejected before allocating or changing any CPU set. + const int error = cpu::detail::pin_thread_to_cpu(INT_MAX); + std::vector after; + const bool helper_unchanged = get_affinity(after) && before == after; + if (error == 0) { + std::fprintf(stderr, "Linux affinity test: pin_thread_to_cpu(INT_MAX) unexpectedly succeeded\n"); + return false; + } + const auto invalid = cpu::pin_current_thread(INT_MAX); + cpu::restore_thread_affinity(invalid); + std::vector after_public; + const bool public_unchanged = get_affinity(after_public) && before == after_public; + if (!helper_unchanged || invalid.valid || !public_unchanged) + std::fprintf(stderr, "Linux affinity test: failed pin changed the observed affinity or returned a valid snapshot\n"); + return helper_unchanged && !invalid.valid && public_unchanged; +} + +int main(int argc, char** argv) { + bool require_high = false; + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "--require-high-cpu") == 0) require_high = true; + else { + std::fprintf(stderr, "Unknown argument: %s\n", argv[i]); + return 2; + } + } + + OriginalAffinity original; + if (!original.capture()) { + std::fprintf(stderr, "Linux affinity test: cannot capture original thread affinity\n"); + return 1; + } + const auto allowed = original.ids(); + + bool ok = round_trip_full_mask(allowed) && worker_thread_can_pin(allowed.back()); + if (ok) ok = round_trip_sparse_nested(allowed); + if (ok) ok = round_trip_high_cpu(allowed, require_high); + if (ok) ok = negative_cpu_is_noop(); + if (ok) ok = failed_pin_preserves_affinity(); + const bool restored = original.restore() && expect_affinity(allowed, "final original affinity cleanup"); + if (!ok || !restored) return 1; + + std::printf("Linux affinity test: full, sparse/nested, worker helper, invalid-target, failed-pin, and cleanup checks passed (%zu allowed CPUs)\n", + allowed.size()); + return 0; +} + +#else + +int main() { + std::puts("SKIP Linux affinity test: Linux is required"); + return 0; +} + +#endif diff --git a/src/kernels/cpu/pool_affinity_test.cpp b/src/kernels/cpu/pool_affinity_test.cpp new file mode 100644 index 0000000000..052ae59b23 --- /dev/null +++ b/src/kernels/cpu/pool_affinity_test.cpp @@ -0,0 +1,148 @@ +#include "strata/kernels/cpu/pool.hpp" +#include "pool_affinity_win.hpp" + +#include +#include +#include +#include + +namespace cpu = strata::kernels::cpu; + +static bool check(bool ok, const char* message) { + if (!ok) std::fprintf(stderr, "Windows affinity test: %s (error %lu)\n", message, (unsigned long) GetLastError()); + return ok; +} + +static bool select(const std::vector& ids) { + return SetThreadSelectedCpuSets(GetCurrentThread(), ids.empty() ? nullptr : ids.data(), (ULONG) ids.size()) != 0; +} + +static bool selected_is(std::vector expected) { + std::vector actual; + if (!cpu::detail::get_thread_cpu_sets(actual)) return false; + std::sort(actual.begin(), actual.end()); + std::sort(expected.begin(), expected.end()); + return actual == expected; +} + +// Establish reachability before the round trip: CPU Sets are soft affinity and cannot override an +// existing job/hard-affinity restriction. Afterwards, require the same observable reachability. +static bool observe(int core) { + for (int i = 0; i < 200; ++i) { + Sleep(1); + PROCESSOR_NUMBER current{}; + GetCurrentProcessorNumberEx(¤t); + if ((int) current.Group * 64 + current.Number == core) return true; + } + return false; +} + +static bool host_round_trips(const std::vector& cores, const std::vector& ids) { + if (!check(select({}), "clear initial thread CPU Sets")) return false; + std::vector reachable; + for (size_t i = 0; i < cores.size(); ++i) { + if (!check(select({ids[i]}), "select baseline CPU Set")) return false; + reachable.push_back(observe(cores[i])); + } + if (!check(select({}), "clear baseline CPU Sets")) return false; + + for (size_t i = 0; i < cores.size(); ++i) { + const auto previous = cpu::pin_current_thread(cores[i]); + const bool pinned = previous.valid && previous.cpu_sets.empty() && selected_is({ids[i]}); + cpu::restore_thread_affinity(previous); + if (!check(pinned && selected_is({}), "restore empty thread assignment")) return false; + } + + // GetThreadGroupAffinity alone misses a lost implicit all-group state. Selecting each CPU Set + // again must still permit execution there, without ever setting a hard group affinity. + for (size_t i = 0; i < cores.size(); ++i) { + if (!check(select({ids[i]}), "select CPU Set after restore")) return false; + if (reachable[i]) { + if (!check(observe(cores[i]), "lost CPU/group eligibility after host restore")) return false; + std::printf("Host restore: still executes on group %d processor %d\n", cores[i] / 64, cores[i] & 63); + } else { + std::printf("SKIP scheduling observation for group %d: not reachable before test\n", cores[i] / 64); + } + } + + // A nonempty selection may span groups. Nested host selections must restore the entire set. + if (!check(select(ids), "set existing multi-CPU selection")) return false; + const auto outer = cpu::pin_current_thread(cores.front()); + const auto inner = cpu::pin_current_thread(cores.back()); + const bool inner_ok = inner.valid && selected_is({ids.back()}); + cpu::restore_thread_affinity(inner); + const bool outer_ok = outer.valid && selected_is({ids.front()}); + cpu::restore_thread_affinity(outer); + if (!check(inner_ok && outer_ok && selected_is(ids), "restore nested / existing CPU Set selections")) return false; + const auto invalid = cpu::pin_current_thread(-1); + cpu::restore_thread_affinity(invalid); + if (!check(!invalid.valid && selected_is(ids), "invalid target must not mutate selection")) return false; + return check(select({}), "clear final host selection"); +} + +static bool inherited_default(int core, ULONG id, int temporary_core, ULONG temporary_id) { + ULONG count = 0; + if (!GetProcessDefaultCpuSets(GetCurrentProcess(), nullptr, 0, &count) && + GetLastError() != ERROR_INSUFFICIENT_BUFFER) return false; + std::vector saved(count); + if (count && !GetProcessDefaultCpuSets(GetCurrentProcess(), saved.data(), count, &count)) return false; + saved.resize(count); + if (!check(SetProcessDefaultCpuSets(GetCurrentProcess(), &id, 1) != 0, "set process default")) return false; + bool ok = false; + std::thread host([&] { + if (!select({})) return; + const bool reachable = observe(core); + const auto previous = cpu::pin_current_thread(temporary_core); + const bool overridden = previous.valid && selected_is({temporary_id}); + cpu::restore_thread_affinity(previous); + ULONG inherited = 0, required = 0; + ok = overridden && previous.cpu_sets.empty() && selected_is({}) && + GetProcessDefaultCpuSets(GetCurrentProcess(), &inherited, 1, &required) && + required == 1 && inherited == id && (!reachable || observe(core)); + }); + host.join(); + const bool restored = SetProcessDefaultCpuSets(GetCurrentProcess(), saved.empty() ? nullptr : saved.data(), + (ULONG) saved.size()) != 0; + return check(ok && restored, "restore inherited process default"); +} + +int main() { + const auto all = cpu::physical_cores(false); + std::vector cores; + std::vector ids; + for (int core : all) { + if (std::any_of(cores.begin(), cores.end(), [core](int other) { return other / 64 == core / 64; })) continue; + ULONG id = 0; + if (!cpu::detail::cpu_set_for_core(core, id)) continue; + cores.push_back(core); + ids.push_back(id); + } + if (!check(!cores.empty(), "no usable CPU Sets")) return 1; + if (!check(cores.size() == GetActiveProcessorGroupCount(), "CPU Sets do not cover all active groups")) return 1; + + // Hard pins belong on pool-owned threads; never contaminate the caller under test with one. + for (size_t i = 0; i < cores.size(); ++i) { + bool ok = false; + std::thread worker([&] { + if (!cpu::detail::set_thread_group_affinity(cores[i], (int) i)) return; + GROUP_AFFINITY pinned{}; + if (!GetThreadGroupAffinity(GetCurrentThread(), &pinned)) return; + ok = pinned.Group == cores[i] / 64 && pinned.Mask == (KAFFINITY(1) << (cores[i] & 63)); + // Host placement must also preserve an existing restrictive hard affinity. + const auto previous = cpu::pin_current_thread(cores.back()); + cpu::restore_thread_affinity(previous); + GROUP_AFFINITY after{}; + ok = ok && previous.valid && GetThreadGroupAffinity(GetCurrentThread(), &after) && + after.Group == pinned.Group && after.Mask == pinned.Mask; + }); + worker.join(); + if (!check(ok, "worker hard pin / caller hard-affinity preservation")) return 1; + } + + bool host_ok = false; + std::thread host([&] { host_ok = host_round_trips(cores, ids); }); + host.join(); + if (!host_ok || !inherited_default(cores.back(), ids.back(), cores.front(), ids.front())) return 1; + std::printf("Windows affinity test: worker pins, host CPU Sets and process-default restoration passed (%zu groups)\n", cores.size()); + return 0; +} diff --git a/src/kernels/cpu/pool_affinity_win.hpp b/src/kernels/cpu/pool_affinity_win.hpp new file mode 100644 index 0000000000..6f0e402022 --- /dev/null +++ b/src/kernels/cpu/pool_affinity_win.hpp @@ -0,0 +1,64 @@ +#pragma once + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#include +#include +#include + +namespace strata::kernels::cpu::detail { + +// Hard affinity is only for pool-owned threads, which terminate with the pool. A caller's implicit +// all-group affinity cannot be restored from PreviousGroupAffinity, so the host uses CPU Sets below. +inline bool set_thread_group_affinity(int core, int worker) { + if (core < 0) return false; + GROUP_AFFINITY target{}; + target.Group = (WORD) (core / 64); + target.Mask = KAFFINITY(1) << (core & 63); + if (SetThreadGroupAffinity(GetCurrentThread(), &target, nullptr)) return true; + std::fprintf(stderr, "strata cpu pool: SetThreadGroupAffinity for worker %d (group %u, mask 0x%llx) failed: %lu; previous affinity kept\n", + worker, (unsigned) target.Group, (unsigned long long) target.Mask, (unsigned long) GetLastError()); + return false; +} + +inline bool get_thread_cpu_sets(std::vector& ids) { + ULONG count = 0; + if (!GetThreadSelectedCpuSets(GetCurrentThread(), nullptr, 0, &count) && + GetLastError() != ERROR_INSUFFICIENT_BUFFER) return false; + ids.resize(count); + if (count == 0) return true; + if (!GetThreadSelectedCpuSets(GetCurrentThread(), ids.data(), count, &count)) return false; + ids.resize(count); + return true; +} + +// CPU Set IDs are opaque: find the entry by both group and group-relative processor number. +inline bool cpu_set_for_core(int core, ULONG& id) { + ULONG bytes = 0; + if (!GetSystemCpuSetInformation(nullptr, 0, &bytes, GetCurrentProcess(), 0) && + GetLastError() != ERROR_INSUFFICIENT_BUFFER) return false; + std::vector buffer(bytes); + if (bytes && !GetSystemCpuSetInformation(reinterpret_cast(buffer.data()), + bytes, &bytes, GetCurrentProcess(), 0)) return false; + constexpr size_t header_size = offsetof(SYSTEM_CPU_SET_INFORMATION, CpuSet); + for (size_t offset = 0; offset + header_size <= bytes;) { + const auto* info = reinterpret_cast(buffer.data() + offset); + if (info->Size < header_size || info->Size > bytes - offset) break; + if (info->Type == CpuSetInformation && info->Size >= sizeof(SYSTEM_CPU_SET_INFORMATION) && info->CpuSet.Group == core / 64 && + info->CpuSet.LogicalProcessorIndex == (core & 63) && + (!info->CpuSet.Allocated || info->CpuSet.AllocatedToTargetProcess)) { + id = info->CpuSet.Id; + return true; + } + offset += info->Size; + } + SetLastError(ERROR_NOT_FOUND); + return false; +} + +} // namespace strata::kernels::cpu::detail +#endif diff --git a/src/kernels/cpu/router_dot_parity.cpp b/src/kernels/cpu/router_dot_parity.cpp new file mode 100644 index 0000000000..bd1341b949 --- /dev/null +++ b/src/kernels/cpu/router_dot_parity.cpp @@ -0,0 +1,70 @@ +// src/kernels/cpu/router_dot_parity.cpp - the router lookahead's BF16 dot on each rung this CPU has: the AVX2 kernel +// (kq_avx2.cpp) and the AVX1 one for older CPUs (kq_avx1.cpp), against a double-precision reference, plus the time +// of one layer's router. No GPU, no model: random BF16 rows of the model's router shape [512 x 2560], 1..8 tokens. +// +// router_dot_parity (STRATA_FORCE_ISA=avx leaves out the AVX2 kernel, as on an AVX-only CPU) +#include "strata/kernels/cpu/expert_layout.hpp" +#include "strata/kernels/cpu/kq_avx1.hpp" +#include "strata/kernels/cpu/kq_avx2.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cpu = strata::kernels::cpu; + +int main() { + constexpr int R = 512, C = 2560, MAXT = 8; + std::mt19937 rng(7); + std::normal_distribution nd(0.f, 0.05f); + std::vector w((size_t) R * C); + for (auto& v : w) { + const float f = nd(rng); + uint32_t b; + std::memcpy(&b, &f, 4); + v = (uint16_t) (b >> 16); + } + std::vector x((size_t) MAXT * C); + for (auto& v : x) v = nd(rng) * 20.f; + std::vector ref((size_t) MAXT * R); + for (int t = 0; t < MAXT; ++t) + for (int r = 0; r < R; ++r) { + double s = 0; + for (int c = 0; c < C; ++c) { + const uint32_t b = (uint32_t) w[(size_t) r * C + c] << 16; + float f; + std::memcpy(&f, &b, 4); + s += (double) f * x[(size_t) t * C + c]; + } + ref[(size_t) t * R + r] = s; + } + int failures = 0; + auto run = [&](const char* tag, void (*fn)(const uint16_t*, int, int, const float*, int, float*)) { + std::vector out((size_t) MAXT * R); + for (int nt = 1; nt <= MAXT; ++nt) { + std::fill(out.begin(), out.end(), 0.f); + fn(w.data(), R, C, x.data(), nt, out.data()); + double num = 0, den = 0; + for (int i = 0; i < nt * R; ++i) { num += std::fabs(out[(size_t) i] - ref[(size_t) i]); den += std::fabs(ref[(size_t) i]); } + const double rel = num / (den + 1e-30); + if (nt == 1 || nt == MAXT) std::printf(" %-5s nt=%d rel %.2e vs the double reference\n", tag, nt, rel); + if (!(rel < 1e-5)) { std::printf(" %-5s nt=%d MISMATCH\n", tag, nt); ++failures; } + } + const int it = 20; + const auto t0 = std::chrono::steady_clock::now(); + for (int i = 0; i < it; ++i) fn(w.data(), R, C, x.data(), 4, out.data()); + const double ms = std::chrono::duration(std::chrono::steady_clock::now() - t0).count() / it; + std::printf(" %-5s one layer's router, 4 tokens: %.3f ms\n", tag, ms); + }; + std::printf("router_dot_parity: %s (avx2 %d, avx %d)\n", cpu::cpu_name().c_str(), (int) cpu::cpu_avx2_ok(), + (int) cpu::cpu_avx1_ok()); + if (cpu::cpu_avx2_ok()) run("avx2", cpu::bf16_rows_dot_multi); + if (cpu::cpu_avx1_ok()) run("avx", cpu::bf16_rows_dot_multi_avx1); + std::printf("router_dot_parity: %d failures\n", failures); + return failures == 0 ? 0 : 1; +} diff --git a/src/kernels/cuda/elementwise.cu b/src/kernels/cuda/elementwise.cu index 26cf66faab..4817763dbe 100644 --- a/src/kernels/cuda/elementwise.cu +++ b/src/kernels/cuda/elementwise.cu @@ -294,15 +294,62 @@ __global__ void doorbell_publish_kernel(const float* __restrict__ x, const int32 const float* __restrict__ w, int n, int k, float* x_out, int32_t* ids_out, float* w_out, uint32_t* seq) { for (int i = threadIdx.x; i < n; i += blockDim.x) x_out[i] = x[i]; - if ((int) threadIdx.x < k) { ids_out[threadIdx.x] = ids[threadIdx.x]; w_out[threadIdx.x] = w[threadIdx.x]; } + if ((int) threadIdx.x < k) { + ids_out[threadIdx.x] = ids[threadIdx.x]; + if (w_out != nullptr) w_out[threadIdx.x] = w[threadIdx.x]; + } __threadfence_system(); __syncthreads(); if (threadIdx.x == 0) { + *(volatile uint32_t*) seq = *(volatile uint32_t*) seq + 1u; + } +} + +__global__ void doorbell_publish_res_kernel(const float* __restrict__ x, const int32_t* __restrict__ ids, + const int32_t* __restrict__ d_res, int n_expert, int n, int k, + float* x_out, int32_t* ids_out, uint32_t* seq) { + int any_miss = 0; + if ((int) threadIdx.x < k) { + const int32_t id = ids[threadIdx.x]; + ids_out[threadIdx.x] = id; + if (d_res == nullptr || id < 0 || id >= n_expert || d_res[id] < 0) any_miss = 1; + } + if (__syncthreads_or(any_miss)) { + for (int i = threadIdx.x; i < n; i += blockDim.x) x_out[i] = x[i]; __threadfence_system(); + } else if ((int) threadIdx.x < k) { + __threadfence_system(); + } + __syncthreads(); + if (threadIdx.x == 0) { *(volatile uint32_t*) seq = *(volatile uint32_t*) seq + 1u; } } +// #649 (HIP, STRATA_DOORBELL_STORE=1): the same publish, but the ring is STORED (the step's own number, known at +// capture) instead of read-modify-written over PCIe - one store, no read of host memory from the GPU. +__global__ void doorbell_publish_value_kernel(const float* __restrict__ x, const int32_t* __restrict__ ids, + const float* __restrict__ w, int n, int k, float* x_out, int32_t* ids_out, + float* w_out, uint32_t* seq, uint32_t value) { + for (int i = threadIdx.x; i < n; i += blockDim.x) x_out[i] = x[i]; + if ((int) threadIdx.x < k) { ids_out[threadIdx.x] = ids[threadIdx.x]; w_out[threadIdx.x] = w[threadIdx.x]; } + __threadfence_system(); + __syncthreads(); + if (threadIdx.x == 0) { + __threadfence_system(); + *(volatile uint32_t*) seq = value; + __threadfence_system(); + } +} + +void doorbell_publish_value(const float* x, const int32_t* ids, const float* weights, int64_t n, int64_t k, + float* x_out, int32_t* ids_out, float* weights_out, uint32_t* d_seq, uint32_t value, + void* stream) { + if (k > 1024) { std::fprintf(stderr, "doorbell_publish: k too large\n"); std::exit(1); } + doorbell_publish_value_kernel<<<1, 1024, 0, (cudaStream_t) stream>>>(x, ids, weights, (int) n, (int) k, x_out, + ids_out, weights_out, d_seq, value); +} + void doorbell_publish(const float* x, const int32_t* ids, const float* weights, int64_t n, int64_t k, float* x_out, int32_t* ids_out, float* weights_out, uint32_t* d_seq, void* stream) { if (k > 1024) { std::fprintf(stderr, "doorbell_publish: k too large\n"); std::exit(1); } @@ -311,6 +358,14 @@ void doorbell_publish(const float* x, const int32_t* ids, const float* weights, check_launch("doorbell_publish"); } +void doorbell_publish_res(const float* x, const int32_t* ids, const int32_t* d_res, int n_expert, int64_t n, int64_t k, + float* x_out, int32_t* ids_out, uint32_t* d_seq, void* stream) { + if (k > 1024) { std::fprintf(stderr, "doorbell_publish_res: k too large\n"); std::exit(1); } + doorbell_publish_res_kernel<<<1, 1024, 0, (cudaStream_t) stream>>>(x, ids, d_res, n_expert, (int) n, (int) k, + x_out, ids_out, d_seq); + check_launch("doorbell_publish_res"); +} + __global__ void copy_i32_from_mapped_kernel(int32_t* __restrict__ dst, const volatile int32_t* src, int n) { for (int i = threadIdx.x; i < n; i += blockDim.x) dst[i] = src[i]; } diff --git a/src/kernels/cuda/fused_gr.cu b/src/kernels/cuda/fused_gr.cu index c9d1b80daa..71012ffcda 100644 --- a/src/kernels/cuda/fused_gr.cu +++ b/src/kernels/cuda/fused_gr.cu @@ -320,13 +320,13 @@ __global__ void __launch_bounds__(THREADS) gr_up_multi_kernel(GrMulti m) { constexpr int PR = LR + HC; // partial rows per (token, stream): 320 down + 4 inject constexpr int TQ3 = N / 8 / 32; // uint4 weight chunks per lane in one stream's slice (10) -template +template __global__ void __launch_bounds__(THREADS) gr_down_v3_kernel(GrMulti m, float* __restrict__ part, float* __restrict__ ssg) { extern __shared__ __align__(16) float xs[]; // [T][N / S] constexpr int R2 = 1; // down rows per warp - __shared__ float red[WARPS][kFusedGrMaxT]; + __shared__ float red[WARPS][MAX_T]; const int t = threadIdx.x, lane = t & 31, warp = t >> 5; - const int T = m.T; + const int T = EXACT_T ? MAX_T : m.T; // S = 2: each stream's 2560 columns in two halves (blockIdx.y = stream * S + half): twice the blocks constexpr int SL = N / S, TQS = SL / 8 / 32; const int rg = blockIdx.x, c = blockIdx.y / S, h = blockIdx.y - (blockIdx.y / S) * S; @@ -337,19 +337,11 @@ __global__ void __launch_bounds__(THREADS) gr_down_v3_kernel(GrMulti m, float* _ const int nrows = inject_block ? ((m.a[0].w_inject != nullptr && warp < HC) ? 1 : 0) : R2; const bool active = nrows > 0; const uint16_t* wbase = inject_block ? m.a[0].w_inject : m.a[0].w_down; - uint4 wv[R2][TQS]; -#pragma unroll - for (int r = 0; r < R2; ++r) { - if (r >= nrows) break; - const uint4* w4 = reinterpret_cast(wbase + (size_t) (row0 + r) * D + (size_t) c * N + (size_t) h * SL); + float ssp[MAX_T]; #pragma unroll - for (int q = 0; q < TQS; ++q) wv[r][q] = __ldg(w4 + lane + 32 * q); - } - float ssp[kFusedGrMaxT]; -#pragma unroll - for (int k = 0; k < kFusedGrMaxT; ++k) { + for (int k = 0; k < MAX_T; ++k) { ssp[k] = 0.0f; - if (k >= T) continue; + if (!EXACT_T && k >= T) continue; const FusedGrArgs& a = m.a[k]; const float gw = a.apply ? 2.0f * sigmoidf_(a.inj_prev[c] / (float) HC) : 0.0f; const float4* R4 = reinterpret_cast(a.R + (size_t) c * N + (size_t) h * SL); @@ -369,8 +361,8 @@ __global__ void __launch_bounds__(THREADS) gr_down_v3_kernel(GrMulti m, float* _ } } #pragma unroll - for (int k = 0; k < kFusedGrMaxT; ++k) { - if (k >= T) break; + for (int k = 0; k < MAX_T; ++k) { + if (!EXACT_T && k >= T) break; const float v = warp_sum(ssp[k]); if (lane == 0) red[warp][k] = v; } @@ -384,34 +376,36 @@ __global__ void __launch_bounds__(THREADS) gr_down_v3_kernel(GrMulti m, float* _ #pragma unroll for (int r = 0; r < R2; ++r) { if (r >= nrows) break; - float acc[kFusedGrMaxT]; + const uint4* w4 = reinterpret_cast(wbase + (size_t) (row0 + r) * D + (size_t) c * N + (size_t) h * SL); + float acc[MAX_T]; #pragma unroll - for (int k = 0; k < kFusedGrMaxT; ++k) acc[k] = 0.0f; + for (int k = 0; k < MAX_T; ++k) acc[k] = 0.0f; #pragma unroll for (int q = 0; q < TQS; ++q) { const int j = lane + 32 * q; + const uint4 wvq = __ldg(w4 + j); #pragma unroll - for (int k = 0; k < kFusedGrMaxT; ++k) - if (k < T) acc[k] += dot8(wv[r][q], xs + (size_t) k * SL + j * 8); + for (int k = 0; k < MAX_T; ++k) + if (EXACT_T || k < T) acc[k] += dot8(wvq, xs + (size_t) k * SL + j * 8); } const int prow = inject_block ? LR + warp : row0 + r; #pragma unroll - for (int k = 0; k < kFusedGrMaxT; ++k) { - if (k >= T) break; + for (int k = 0; k < MAX_T; ++k) { + if (!EXACT_T && k >= T) break; const float v = warp_sum(acc[k]); if (lane == 0) part[(((size_t) k * HC + c) * S + h) * PR + prow] = v; } } } -template +template __global__ void __launch_bounds__(THREADS) gr_up_v3_kernel(GrMulti m, const float* __restrict__ part, const float* __restrict__ ssg) { - __shared__ __align__(16) float lo[kFusedGrMaxT][LR]; - __shared__ float rsS[kFusedGrMaxT][HC]; - __shared__ float g[kFusedGrMaxT][HC][UPM_COLS]; + __shared__ __align__(16) float lo[MAX_T][LR]; + __shared__ float rsS[MAX_T][HC]; + __shared__ float g[MAX_T][HC][UPM_COLS]; const int t = threadIdx.x, lane = t & 31, warp = t >> 5; - const int T = m.T; + const int T = EXACT_T ? MAX_T : m.T; const int d0 = blockIdx.x * UPM_COLS; constexpr int RPW = HC * UPM_COLS / WARPS; // 8 rows per warp if (t < T * HC) { @@ -470,8 +464,8 @@ __global__ void __launch_bounds__(THREADS) gr_up_v3_kernel(GrMulti m, const floa } float mine = 0.0f; #pragma unroll - for (int k = 0; k < kFusedGrMaxT; ++k) { - if (k >= T) break; + for (int k = 0; k < MAX_T; ++k) { + if (!EXACT_T && k >= T) break; float acc = dot8(wa, lo[k] + lane * 8); if (lane < LR / 8 - 32) acc += dot8(wb, lo[k] + (32 + lane) * 8); acc = warp_sum(acc); @@ -712,7 +706,11 @@ int down_chunk(bool staged, int* tile_out) { // 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__) +#if defined(STRATA_HIP_GFX906) + const bool small_tile = false; // gfx906: 64 KiB LDS, the 2560 tile as before (8 tokens slice into launches) +#else const bool small_tile = true; // all eight tokens fit gfx1100's 64 KiB LDS at this tile +#endif #else int cc_maj = 0, cc_min = 0; cudaDeviceGetAttribute(&cc_maj, cudaDevAttrComputeCapabilityMajor, dev); @@ -759,10 +757,26 @@ int down_chunk(bool staged, int* tile_out) { // 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). +#if defined(STRATA_HIP_GFX906) +__global__ void __launch_bounds__(THREADS) gr_norm_fast_kernel(GrMulti m); // below, with the AMD fast path +__global__ void __launch_bounds__(THREADS) gr_up_fast_kernel(GrMulti m); +} // namespace +static bool gr_fast(); +namespace { +#endif 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 defined(STRATA_HIP_GFX906) + // gfx906: the latency-hidden norm/up (STRATA_GR_FAST=0: off) - the same sums in the same order as the kernels + // they replace (gr_parity checks the multi-token read against single-token calls bitwise) + const bool fast = gr_fast(); if (variant >= kHcSplit) gr_norm_split_kernel<<>>(m); + else if (fast) gr_norm_fast_kernel<<>>(m); else gr_norm_multi_kernel<<>>(m); +#else + if (variant >= kHcSplit) gr_norm_split_kernel<<>>(m); + else gr_norm_multi_kernel<<>>(m); +#endif if (stamp_buf) gpu_stamp(stamp_buf, stamp_i0, (void*) st); const bool staged = variant >= kHcStaged; int tv = 2560; @@ -796,6 +810,10 @@ void launch_multi(const GrMulti& m, int variant, cudaStream_t st, unsigned long } } if (stamp_buf) gpu_stamp(stamp_buf, stamp_i0 + 1, (void*) st); +#if defined(STRATA_HIP_GFX906) + if (fast) gr_up_fast_kernel<<>>(m); + else +#endif gr_up_multi_kernel<<>>(m); } @@ -811,8 +829,230 @@ int env_variant() { // per device: the variant `fused_gr_check` chose (0 = not checked yet) std::atomic g_variant[64]; + +#if defined(STRATA_HIP_GFX906) +// ---- AMD fast path (STRATA_GR_FAST, default 1; 0 = the old kernels): the same arithmetic per value in the same order, latency +// hidden. gr_norm: one block per token (as before) but a thread's 10 float4 of R / bo / w_norm are loaded +// before any is used, and xn stays in registers until rs is known (was: store, then re-read the 40 KB). +// gr_up: a warp's 8 rows of w_up and their epilogue inputs are loaded before the dots (was: one row's load chain +// after another). +constexpr int NQ = D / (THREADS * 4); // 10 float4 per thread +static_assert(D % (THREADS * 4) == 0, "whole float4 per thread"); +__global__ void __launch_bounds__(THREADS) gr_norm_fast_kernel(GrMulti m) { + __shared__ float part[WARPS][HC]; + __shared__ float s_rs[HC]; + const FusedGrArgs& a = m.a[blockIdx.x]; + float* xn = m.xn + (size_t) blockIdx.x * D; + const int t = threadIdx.x, lane = t & 31, warp = t >> 5; + float gw[HC]; +#pragma unroll + for (int c = 0; c < HC; ++c) gw[c] = a.apply ? 2.0f * sigmoidf_(a.inj_prev[c] / (float) HC) : 0.0f; + float4 r[NQ], g[NQ]; +#pragma unroll + for (int k = 0; k < NQ; ++k) { + const int i = t * 4 + k * THREADS * 4; + r[k] = *reinterpret_cast(a.R + i); + g[k] = *reinterpret_cast(a.w_norm + i); + } + if (a.apply) { +#pragma unroll + for (int k = 0; k < NQ; ++k) { + const int i = t * 4 + k * THREADS * 4, c = i / N, d = i - c * N; + const float4 b = *reinterpret_cast(a.bo_prev + d); + r[k].x = fmaf(b.x, gw[c], r[k].x); r[k].y = fmaf(b.y, gw[c], r[k].y); + r[k].z = fmaf(b.z, gw[c], r[k].z); r[k].w = fmaf(b.w, gw[c], r[k].w); + } + } + float ss[HC] = {0.0f, 0.0f, 0.0f, 0.0f}; +#pragma unroll + for (int k = 0; k < NQ; ++k) { + const int i = t * 4 + k * THREADS * 4, c = i / N; + const float sq = r[k].x * r[k].x + r[k].y * r[k].y + r[k].z * r[k].z + r[k].w * r[k].w; +#pragma unroll + for (int cc = 0; cc < HC; ++cc) if (cc == c) ss[cc] += sq; + r[k] = make_float4(r[k].x * g[k].x, r[k].y * g[k].y, r[k].z * g[k].z, r[k].w * g[k].w); + } +#pragma unroll + for (int c = 0; c < HC; ++c) { + const float v = warp_sum(ss[c]); + if (lane == 0) part[warp][c] = v; + } + __syncthreads(); + if (t < HC) { + float s = 0.0f; + for (int w = 0; w < WARPS; ++w) s += part[w][t]; + s_rs[t] = rsqrtf(s / (float) N + a.eps); + a.rs[t] = s_rs[t]; + } + __syncthreads(); +#pragma unroll + for (int k = 0; k < NQ; ++k) { + const int i = t * 4 + k * THREADS * 4; + const float sr = s_rs[i / N]; + *reinterpret_cast(xn + i) = make_float4(r[k].x * sr, r[k].y * sr, r[k].z * sr, r[k].w * sr); + } +} +// gr_up: 8 lanes per row instead of a 32-lane warp. Lane j holds the old lanes j, j+8, j+16, j+24 (and chunk +// 32+j, the old lane j's second chunk): the xor tree's stages 16 and 8 are its own adds (a + b == b + a), stages +// 4, 2, 1 are shuffles inside the 8 - the same tree, bitwise the same sum, 3 shuffles a token instead of 5 and no +// idle lanes on the tail chunks. A block's 64 rows are 2 passes of 32 groups, their weights loaded up front. +__device__ __forceinline__ float xor8(float v) { +#pragma unroll + for (int o = 4; o > 0; o >>= 1) v += __shfl_xor(v, o, 64); + return v; +} +__global__ void __launch_bounds__(THREADS) gr_up_fast_kernel(GrMulti m) { + __shared__ __align__(16) float lo[kFusedGrMaxT][LR]; + __shared__ float g[kFusedGrMaxT][HC][UPM_COLS]; + const int t = threadIdx.x, j = t & 7, grp = t >> 3; + const int T = m.T; + const int d0 = blockIdx.x * UPM_COLS; + static_assert(LR == 40 * 8 && HC * UPM_COLS == 64 && THREADS == 256, "geometry"); + uint4 w[2][5]; + float rv[2] = {0.0f, 0.0f}, wn[2] = {0.0f, 0.0f}, rsc[2] = {0.0f, 0.0f}, bo[2] = {0.0f, 0.0f}, ip[2] = {0.0f, 0.0f}; + const bool apply = j < T && m.a[j < T ? j : 0].apply; +#pragma unroll + for (int p = 0; p < 2; ++p) { + const int r = grp + 32 * p, c = r / UPM_COLS, dd = r - c * UPM_COLS, i = c * N + d0 + dd; + const uint4* w4 = reinterpret_cast(m.a[0].w_up + (size_t) i * LR); +#pragma unroll + for (int q = 0; q < 4; ++q) w[p][q] = __ldg(w4 + j + 8 * q); + w[p][4] = __ldg(w4 + 32 + j); + if (j < T) { + const FusedGrArgs& a = m.a[j]; + rv[p] = a.R[i]; + wn[p] = a.w_norm[i]; + rsc[p] = a.rs[c]; + if (apply) { bo[p] = a.bo_prev[d0 + dd]; ip[p] = a.inj_prev[c]; } + } + } + for (int i = t; i < T * LR; i += THREADS) lo[i / LR][i % LR] = m.a[i / LR].lo[i % LR]; + __syncthreads(); +#pragma unroll + for (int p = 0; p < 2; ++p) { + const int r = grp + 32 * p, c = r / UPM_COLS, dd = r - c * UPM_COLS, i = c * N + d0 + dd; + float mine = 0.0f; +#pragma unroll + for (int k = 0; k < kFusedGrMaxT; ++k) { + if (k >= T) break; + const float* l = lo[k]; + const float p0 = dot8(w[p][0], l + j * 8) + dot8(w[p][4], l + (32 + j) * 8); // old lane j + const float p1 = dot8(w[p][1], l + (j + 8) * 8); // old lane j + 8 + const float p2 = dot8(w[p][2], l + (j + 16) * 8); // old lane j + 16 + const float p3 = dot8(w[p][3], l + (j + 24) * 8); // old lane j + 24 + const float s = xor8((p0 + p2) + (p1 + p3)); // stage 16: (j, j+16), (j+8, j+24); stage 8; then 4, 2, 1 + if (j == k) mine = s; + } + if (j < T) { + float x0 = rv[p]; + if (apply) { + x0 = fmaf(bo[p], 2.0f * sigmoidf_(ip[p] / (float) HC), x0); + m.a[j].R_out[i] = x0; + } + const float x = x0 * wn[p] * rsc[p]; + g[j][c][dd] = x * sigmoidf_(mine); + } + } + __syncthreads(); + for (int i = t; i < T * UPM_COLS; i += THREADS) { + const int k = i / UPM_COLS, col = i - k * UPM_COLS; + float s = 0.0f; +#pragma unroll + for (int c = 0; c < HC; ++c) s += g[k][c][col]; + m.a[k].mixed[d0 + col] = s / (float) HC; + } +} +#endif + +#if defined(STRATA_HIP_GFX906) +// ---- AMD: `gr_down_multi` split along K. The CUDA kernel is one warp per row of w_down (320 + 4 rows), 41 +// blocks: on a 60-CU gfx906 that left most of the card idle and read the 6.5 MB matrix at ~100 GB/s. Here a block +// is 8 wavefronts = 8 rows of one K-slice of 2048 (the slice of every token's xn staged in LDS once), 41 x 5 +// blocks, and a second kernel adds the 5 slice sums in a fixed order and runs the epilogue. A token's result does +// not depend on how many tokens share the window (each column is reduced on its own). +constexpr int GS_SL = 2048; // K per slice +constexpr int GS_S = D / GS_SL; // 5 slices +constexpr int GS_ROWS = LR + HC; // 324: the down rows, then the inject rows +constexpr int GS_WAVES = 8; +static_assert(D % GS_SL == 0 && GS_SL == 64 * 8 * 4, "a lane takes 4 chunks of 8 per slice"); +// the slice sums: a module-scope device array is per device by construction, and needs no allocation during a +// graph capture +__device__ float g_gr_part[GS_S * kFusedGrMaxT * GS_ROWS]; +__global__ void __launch_bounds__(GS_WAVES * 64) gr_down_split_kernel(GrMulti m) { + float* part = g_gr_part; + extern __shared__ __align__(16) float tile[]; // [T][GS_SL] + const int t = threadIdx.x, lane = t & 63, wave = t >> 6; + const int T = m.T, sl = blockIdx.y; + const bool inject_block = blockIdx.x == DOWN_BLOCKS; + const int row = inject_block ? wave : blockIdx.x * GS_WAVES + wave; + const bool active = !(inject_block && (m.a[0].w_inject == nullptr || wave >= 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(wrow) + sl * (GS_SL / 8); + uint4 wv[4]; + if (active) { +#pragma unroll + for (int q = 0; q < 4; ++q) wv[q] = __ldg(w4 + lane + 64 * q); + } + const float4* src4 = reinterpret_cast(m.xn); + float4* tile4 = reinterpret_cast(tile); + for (int i = t; i < T * (GS_SL / 4); i += GS_WAVES * 64) { + const int k = i / (GS_SL / 4), off = i - k * (GS_SL / 4); + tile4[i] = src4[((size_t) k * D + (size_t) sl * GS_SL) / 4 + off]; + } + __syncthreads(); + if (!active) return; + float acc[kFusedGrMaxT]; +#pragma unroll + for (int k = 0; k < kFusedGrMaxT; ++k) acc[k] = 0.0f; +#pragma unroll + for (int q = 0; q < 4; ++q) { + const int j = lane + 64 * q; +#pragma unroll + for (int k = 0; k < kFusedGrMaxT; ++k) + if (k < T) acc[k] += dot8(wv[q], tile + k * GS_SL + j * 8); + } +#pragma unroll + for (int k = 0; k < kFusedGrMaxT; ++k) { + if (k >= T) break; + float v = acc[k]; +#pragma unroll + for (int off = 32; off > 0; off >>= 1) v += __shfl_xor(v, off, 64); + if (lane == 0) part[((size_t) sl * kFusedGrMaxT + k) * GS_ROWS + (inject_block ? LR + row : row)] = v; + } +} +__global__ void gr_down_finish_kernel(GrMulti m) { + const float* part = g_gr_part; + const int i = blockIdx.x * blockDim.x + threadIdx.x; + const int k = i / GS_ROWS, r = i - k * GS_ROWS; + if (k >= m.T) return; + const bool inject = r >= LR; + if (inject && m.a[0].w_inject == nullptr) return; + float s = 0.0f; +#pragma unroll + for (int sl = 0; sl < GS_S; ++sl) s += part[((size_t) sl * kFusedGrMaxT + k) * GS_ROWS + r]; + if (inject) { + m.a[k].inject_out[r - LR] = s; + } else { + const float x = s / (float) HC; + m.a[k].lo[r] = x / (1.0f + __expf(-x)); + } +} + +#endif + } // namespace +#if defined(STRATA_HIP_GFX906) +int g_gr_fast = -1; // fused_gr_set_fast (the bench); -1 = STRATA_GR_FAST +static bool gr_fast() { + static const bool env = [] { const char* v = std::getenv("STRATA_GR_FAST"); return v ? std::atoi(v) != 0 : true; }(); + return g_gr_fast >= 0 ? g_gr_fast != 0 : env; +} +#else +int g_gr_fast = -1; +#endif +void fused_gr_set_fast(int on) { g_gr_fast = on; } + void fused_gr_read_multi(const FusedGrArgs* a, int n_tok, float* xn_scratch, void* stream, unsigned long long* stamp_buf, int stamp_i0) { if (n_tok < 1 || n_tok > kFusedGrMaxT || xn_scratch == nullptr) { @@ -843,12 +1083,16 @@ void fused_gr_read_multi(const FusedGrArgs* a, int n_tok, float* xn_scratch, voi int optin = 0; cudaDeviceGetAttribute(&optin, cudaDevAttrMaxSharedMemoryPerBlockOptin, dev3); const int limit = optin > 0 ? optin : 48 * 1024; - const int need1 = (int) (kFusedGrMaxT * N * sizeof(float)), need2 = need1 / 2; + const int need1 = (int) (kFusedGrMaxT * N * sizeof(float)), need6 = (int) (6 * N * sizeof(float)), need2 = need1 / 2; int split = -1; if (need1 <= limit && - cudaFuncSetAttribute(gr_down_v3_kernel<1>, cudaFuncAttributeMaxDynamicSharedMemorySize, need1) == cudaSuccess) + cudaFuncSetAttribute(gr_down_v3_kernel<1, kFusedGrMaxT, false>, cudaFuncAttributeMaxDynamicSharedMemorySize, need1) == cudaSuccess && + cudaFuncSetAttribute(gr_down_v3_kernel<1, 6, true>, cudaFuncAttributeMaxDynamicSharedMemorySize, need6) == cudaSuccess) { + cudaFuncSetAttribute(gr_down_v3_kernel<1, 4, true>, cudaFuncAttributePreferredSharedMemoryCarveout, 100); + cudaFuncSetAttribute(gr_down_v3_kernel<1, 4, false>, cudaFuncAttributePreferredSharedMemoryCarveout, 100); + cudaFuncSetAttribute(gr_down_v3_kernel<1, 2, true>, cudaFuncAttributePreferredSharedMemoryCarveout, 100); split = 1; - else if (need2 <= limit) + } 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 " @@ -864,10 +1108,18 @@ void fused_gr_read_multi(const FusedGrArgs* a, int n_tok, float* xn_scratch, voi const size_t sm = (size_t) n_tok * (N / split) * sizeof(float); if (stamp_buf) gpu_stamp(stamp_buf, stamp_i0, stream); if (split == 2) gr_down_v3_kernel<2><<>>(m, part, ssg); - else gr_down_v3_kernel<1><<>>(m, part, ssg); + else if (n_tok == 2) gr_down_v3_kernel<1, 2, true><<>>(m, part, ssg); + else if (n_tok == 4) gr_down_v3_kernel<1, 4, true><<>>(m, part, ssg); + else if (n_tok == 6) gr_down_v3_kernel<1, 6, true><<>>(m, part, ssg); + else if (n_tok < 4) gr_down_v3_kernel<1, 4, false><<>>(m, part, ssg); + else gr_down_v3_kernel<1, kFusedGrMaxT, false><<>>(m, part, ssg); if (stamp_buf) gpu_stamp(stamp_buf, stamp_i0 + 1, stream); if (split == 2) gr_up_v3_kernel<2><<>>(m, part, ssg); - else gr_up_v3_kernel<1><<>>(m, part, ssg); + else if (n_tok == 2) gr_up_v3_kernel<1, 2, true><<>>(m, part, ssg); + else if (n_tok == 4) gr_up_v3_kernel<1, 4, true><<>>(m, part, ssg); + else if (n_tok == 6) gr_up_v3_kernel<1, 6, true><<>>(m, part, ssg); + else if (n_tok < 4) gr_up_v3_kernel<1, 4, false><<>>(m, part, ssg); + else gr_up_v3_kernel<1, kFusedGrMaxT, false><<>>(m, part, ssg); const cudaError_t e3 = cudaGetLastError(); if (e3 != cudaSuccess) { std::fprintf(stderr, "fused_gr_read_multi v3: %s\n", cudaGetErrorString(e3)); @@ -875,6 +1127,31 @@ void fused_gr_read_multi(const FusedGrArgs* a, int n_tok, float* xn_scratch, voi } return; } +#if defined(STRATA_HIP_GFX906) + // gfx906, opt-in STRATA_GR_SPLIT=1: the latency-hidden norm/up (STRATA_GR_FAST) with the K-split down (~3% faster + // per verify window). Its fixed-order finish sums the K slices in another order than the single-token + // fused_gr_read, so the multi-token read is then no longer bitwise equal to T single-token calls (gr_parity's + // contract); off by default for that reason. + static const bool split_on = std::getenv("STRATA_GR_SPLIT") && std::string(std::getenv("STRATA_GR_SPLIT")) == "1"; + if (split_on) { + const bool fast = gr_fast(); + if (fast) gr_norm_fast_kernel<<>>(m); + else gr_norm_multi_kernel<<>>(m); + if (stamp_buf) gpu_stamp(stamp_buf, stamp_i0, stream); + gr_down_split_kernel<<>>( + m); + gr_down_finish_kernel<<<(unsigned) ((n_tok * GS_ROWS + 255) / 256), 256, 0, st>>>(m); + if (stamp_buf) gpu_stamp(stamp_buf, stamp_i0 + 1, stream); + if (fast) gr_up_fast_kernel<<>>(m); + else gr_up_multi_kernel<<>>(m); + const cudaError_t e = cudaGetLastError(); + if (e != cudaSuccess) { + std::fprintf(stderr, "fused_gr_read_multi: %s\n", cudaGetErrorString(e)); + std::exit(1); + } + return; + } +#endif // 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(); diff --git a/src/kernels/cuda/iq_kernels.cu b/src/kernels/cuda/iq_kernels.cu index 522d8670b1..5c552fa76d 100644 --- a/src/kernels/cuda/iq_kernels.cu +++ b/src/kernels/cuda/iq_kernels.cu @@ -6,6 +6,7 @@ // included unchanged. #include "strata/kernels/iq_kernels.hpp" #include "strata/kernels/dp4a.hpp" +#include "strata/kernels/q8_1_finite.hpp" #include #include @@ -16,6 +17,7 @@ #include #include +#include namespace strata::kernels { namespace { @@ -39,6 +41,18 @@ __device__ __forceinline__ uint32_t unpack_ksigns(const uint8_t v) { return s * 0x01010101; } __device__ __forceinline__ int2 get_int_from_table_16(const int& q4, const int8_t* table) { +#if defined(STRATA_HIP_GFX906) + // AMD: llama.cpp's HIP lookup (vecdotq.cuh) - v_perm_b32 takes 3-bit byte indices, so the low and high halves + // of the table are looked up and the index MSB picks between them: 4 perms per 8 values. + const uint32_t* v32 = reinterpret_cast(table); + const uint32_t q_even = (uint32_t) q4, q_odd = (uint32_t) q4 >> 4; + const uint32_t el = __builtin_amdgcn_perm(v32[1], v32[0], q_even & 0x07070707u); + const uint32_t ol = __builtin_amdgcn_perm(v32[1], v32[0], q_odd & 0x07070707u); + const uint32_t eh = __builtin_amdgcn_perm(v32[3], v32[2], q_even & 0x07070707u); + const uint32_t oh = __builtin_amdgcn_perm(v32[3], v32[2], q_odd & 0x07070707u); + return make_int2((int) __builtin_amdgcn_perm(eh, el, 0x03020100u | ((q_even & 0x08080808u) >> 1)), + (int) __builtin_amdgcn_perm(oh, ol, 0x03020100u | ((q_odd & 0x08080808u) >> 1))); +#else const uint32_t* table32 = (const uint32_t*) table; uint32_t tmp[2]; const uint32_t low_high_selection_indices = (0x32103210 | ((q4 & 0x88888888) >> 1)); @@ -50,6 +64,7 @@ __device__ __forceinline__ int2 get_int_from_table_16(const int& q4, const int8_ tmp[i] = __byte_perm(low, high, low_high_selection_indices >> shift); } return make_int2(__byte_perm(tmp[0], tmp[1], 0x6420), __byte_perm(tmp[0], tmp[1], 0x7531)); +#endif } #define ggml_cuda_dp4a(a, b, c) STRATA_DP4A((a), (b), (c)) @@ -557,18 +572,24 @@ template struct Split; // per-entry ones, which call Fmt::dot per column exactly as before #242 (the launchers test kSplit at compile // time, so the multi kernels are never instantiated for a type without a Split). template inline constexpr bool kSplit = false; +template inline constexpr bool kStageIqGrid = (TY == 16 || TY == 17 || TY == 22 || TY == 29); + template<> inline constexpr bool kSplit<16> = true; template<> struct Split<16> { // IQ2_XXS struct W { int g[8]; int ls; float dw; }; - __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs) { + template + __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs, const uint32_t* __restrict__ s_grid = nullptr) { const block_iq2_xxs* bq2 = (const block_iq2_xxs*) vbq + kbx; const int q2 = get_int_b2(bq2->qs, iqs); const uint8_t* aux8 = (const uint8_t*) &q2; const uint32_t aux32 = get_int_b2(bq2->qs, iqs + 1); + const uint2* grid_lut; + if constexpr (STAGE_GRID) grid_lut = (const uint2*) s_grid; + else grid_lut = (const uint2*) iq2xxs_grid; W r; #pragma unroll for (int k0 = 0; k0 < 8; k0 += 2) { - const uint2 grid_pos = ((const uint2*) iq2xxs_grid)[aux8[k0 / 2]]; + const uint2 grid_pos = grid_lut[aux8[k0 / 2]]; const uint32_t signs = unpack_ksigns(aux32 >> (7 * k0 / 2)); const int signs0 = __vcmpne4(signs & 0x08040201, 0); r.g[k0 + 0] = __vsub4(grid_pos.x ^ signs0, signs0); @@ -604,16 +625,20 @@ struct SplitLs2 { }; template<> inline constexpr bool kSplit<17> = true; template<> struct Split<17> : SplitLs2 { // IQ2_XS - __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs) { + template + __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs, const uint32_t* __restrict__ s_grid = nullptr) { const block_iq2_xs* bq2 = (const block_iq2_xs*) vbq + kbx; const int2 q2_packed = make_int2(get_int_b2(bq2->qs, iqs + 0), get_int_b2(bq2->qs, iqs + 1)); const uint16_t* q2 = (const uint16_t*) &q2_packed; + const uint2* grid_lut; + if constexpr (STAGE_GRID) grid_lut = (const uint2*) s_grid; + else grid_lut = (const uint2*) iq2xs_grid; W r; r.ls0 = bq2->scales[iqs / 2] & 0x0F; r.ls1 = bq2->scales[iqs / 2] >> 4; #pragma unroll for (int l0 = 0; l0 < 8; l0 += 2) { - const uint2 grid_pos = ((const uint2*) iq2xs_grid)[q2[l0 / 2] & 0x1FF]; + const uint2 grid_pos = grid_lut[q2[l0 / 2] & 0x1FF]; const uint32_t signs = unpack_ksigns(q2[l0 / 2] >> 9); const int signs0 = __vcmpne4(signs & 0x08040201, 0); r.g[l0 + 0] = __vsub4(grid_pos.x ^ signs0, signs0); @@ -626,19 +651,23 @@ template<> struct Split<17> : SplitLs2 { // IQ2_XS }; template<> inline constexpr bool kSplit<22> = true; template<> struct Split<22> : SplitLs2 { // IQ2_S - __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs) { + template + __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs, const uint32_t* __restrict__ s_grid = nullptr) { const block_iq2_s* bq2 = (const block_iq2_s*) vbq + kbx; const int qs_packed = get_int_b2(bq2->qs, iqs / 2); const uint8_t* qs = (const uint8_t*) &qs_packed; const int qh = bq2->qh[iqs / 2]; const int signs_packed_32 = get_int_b2(bq2->qs, QK_K / 32 + iqs / 2); const uint8_t* signs_packed_8 = (const uint8_t*) &signs_packed_32; + const uint64_t* grid_lut; + if constexpr (STAGE_GRID) grid_lut = (const uint64_t*) s_grid; + else grid_lut = iq2s_grid; W r; r.ls0 = bq2->scales[iqs / 2] & 0x0F; r.ls1 = bq2->scales[iqs / 2] >> 4; #pragma unroll for (int l0 = 0; l0 < 8; l0 += 2) { - const int* grid_pos = (const int*) (iq2s_grid + (qs[l0 / 2] | ((qh << (8 - l0)) & 0x300))); + const int* grid_pos = (const int*) (grid_lut + (qs[l0 / 2] | ((qh << (8 - l0)) & 0x300))); const int signs0 = __vcmpne4(((signs_packed_8[l0 / 2] & 0x03) << 7) | ((signs_packed_8[l0 / 2] & 0x0C) << 21), 0x00000000); const int signs1 = __vcmpne4(((signs_packed_8[l0 / 2] & 0x30) << 3) | ((signs_packed_8[l0 / 2] & 0xC0) << 17), 0x00000000); r.g[l0 + 0] = __vsub4(grid_pos[0] ^ signs0, signs0); @@ -651,7 +680,8 @@ template<> struct Split<22> : SplitLs2 { // IQ2_S template<> inline constexpr bool kSplit<18> = true; template<> struct Split<18> { // IQ3_XXS struct W { int g[8]; int ls; float dw; }; - __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs) { + template + __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs, const uint32_t* __restrict__ = nullptr) { const block_iq3_xxs* bq3 = (const block_iq3_xxs*) vbq + kbx; const int2 q3_packed = make_int2(get_int_b2(bq3->qs, iqs), get_int_b2(bq3->qs, iqs + 1)); const uint8_t* q3 = (const uint8_t*) &q3_packed; @@ -682,7 +712,8 @@ template<> struct Split<18> { // IQ3_XXS template<> inline constexpr bool kSplit<21> = true; template<> struct Split<21> { // IQ3_S struct W { int g[8]; int ls; float dw; }; - __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs) { + template + __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs, const uint32_t* __restrict__ = nullptr) { const block_iq3_s* bq3 = (const block_iq3_s*) vbq + kbx; const int2 qs_packed = make_int2(get_int_b2(bq3->qs, iqs + 0), get_int_b2(bq3->qs, iqs + 1)); const uint8_t* qs = (const uint8_t*) &qs_packed; @@ -715,15 +746,19 @@ template<> struct Split<21> { // IQ3_S template<> inline constexpr bool kSplit<29> = true; template<> struct Split<29> { // IQ1_M struct W { int g[8]; float delta[4]; int sc0, sc1; float dw; }; - __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs) { + template + __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs, const uint32_t* __restrict__ s_grid = nullptr) { const block_iq1_m* bq1 = (const block_iq1_m*) vbq + kbx; const int qs_packed = get_int_b4(bq1->qs, iqs); const uint8_t* qs = (const uint8_t*) &qs_packed; + const uint32_t* grid_lut; + if constexpr (STAGE_GRID) grid_lut = s_grid; + else grid_lut = iq1s_grid_gpu; W r; #pragma unroll for (int l0 = 0; l0 < 8; l0 += 2) { const int qhl = bq1->qh[2 * iqs + l0 / 4] >> (4 * ((l0 / 2) % 2)); - const int grid = iq1s_grid_gpu[qs[l0 / 2] | ((qhl & 0x07) << 8)]; + const int grid = grid_lut[qs[l0 / 2] | ((qhl & 0x07) << 8)]; r.g[l0 + 0] = (grid >> 0) & 0x0F0F0F0F; r.g[l0 + 1] = (grid >> 4) & 0x0F0F0F0F; r.delta[l0 / 2] = -1.0f + IQ1M_DELTA - (qhl & 0x08) * (2.0f * IQ1M_DELTA / 0x08); @@ -758,7 +793,8 @@ template<> struct Split<29> { // IQ1_M template<> inline constexpr bool kSplit<20> = true; template<> struct Split<20> { // IQ4_NL struct W { int2 v[2]; float dw; }; - __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs) { + template + __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs, const uint32_t* __restrict__ = nullptr) { const block_iq4_nl* bq4 = (const block_iq4_nl*) vbq + kbx; W r; #pragma unroll @@ -781,7 +817,8 @@ template<> struct Split<20> { // IQ4_NL template<> inline constexpr bool kSplit<23> = true; template<> struct Split<23> { // IQ4_XS struct W { int2 v[4]; int ls; float dw; }; - __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs) { + template + __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs, const uint32_t* __restrict__ = nullptr) { const block_iq4_xs* bq4 = (const block_iq4_xs*) vbq + kbx; W r; #pragma unroll @@ -807,7 +844,8 @@ template<> struct Split<23> { // IQ4_XS template<> inline constexpr bool kSplit<42> = true; template<> struct Split<42> { // Q2_0 struct W { int qx[4], qy[4]; float d2; }; - __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs) { + template + __device__ static W load(const void* __restrict__ vbq, int kbx, int iqs, const uint32_t* __restrict__ = nullptr) { const block_q2_0* bq2_0 = (const block_q2_0*) vbq + kbx; W r; r.d2 = bq2_0->d; @@ -840,31 +878,166 @@ template<> struct Split<42> { // Q2_0 // One row against the n <= NC activations x + off[0..n) (n >= 1, warp-uniform; offsets in q8_1 blocks, 32-bit to // spare registers), the whole warp. Per activation this is row_dot: the same calls k, lane-strided the same way, // summed in the same order, then the same warp_sum. Only the weight side moves out of the per-activation loop. -template +template __device__ __forceinline__ void row_dot_multi(const uint8_t* row, const block_q8_1* x, const int (&off)[NC], int n, - int nb, int lane, float (&s)[NC]) { + int nb, int lane, float (&s)[NC], const uint32_t* __restrict__ s_grid = nullptr) { using F = Fmt; using S = Split; #pragma unroll for (int c = 0; c < NC; ++c) s[c] = 0.0f; for (int k = lane; k < nb * F::ipb; k += 32) { const int kbx = k / F::ipb, iqs = F::step * (k % F::ipb); - const typename S::W w = S::load(row, kbx, iqs); + const typename S::W w = S::template load(row, kbx, iqs, s_grid); #pragma unroll for (int c = 0; c < NC; ++c) - if (c < n) s[c] += S::apply(w, x + off[c] + kbx * (F::qk / 32), iqs); + if (EXACT_N || c < n) s[c] += S::apply(w, x + off[c] + kbx * (F::qk / 32), iqs); } #pragma unroll for (int c = 0; c < NC; ++c) - if (c < n) s[c] = warp_sum(s[c]); + if (EXACT_N || c < n) s[c] = warp_sum(s[c]); +} + +// 8-lane sub-warp row dot for K = nb * ipb == 40 (TD == 20, IQ4_NL with n_ff == 640): +// 4 rows per warp (32 rows per 256-thread block), 100% active lanes, bitwise identical addition tree to row_dot_multi. +template +__device__ __forceinline__ void row_dot_40_sub8(const uint8_t* row, const block_q8_1* x, const int (&off)[NC], int n, + int t, float (&s)[NC]) { + using F = Fmt; + using S = Split; +#pragma unroll + for (int c = 0; c < NC; ++c) s[c] = 0.0f; + for (int k = t; k < 40; k += 32) { + const int kbx = k / F::ipb, iqs = F::step * (k % F::ipb); + const typename S::W w = S::load(row, kbx, iqs); +#pragma unroll + for (int c = 0; c < NC; ++c) + if (EXACT_N || c < n) s[c] += S::apply(w, x + off[c] + kbx * (F::qk / 32), iqs); + } + { + const int k = t + 16, kbx = k / F::ipb, iqs = F::step * (k % F::ipb); + const typename S::W w = S::load(row, kbx, iqs); +#pragma unroll + for (int c = 0; c < NC; ++c) { + if (EXACT_N || c < n) { + float s16 = 0.0f; + s16 += S::apply(w, x + off[c] + kbx * (F::qk / 32), iqs); + s[c] = __fadd_rn(s[c], s16); + } + } + } + float s8[NC]; +#pragma unroll + for (int c = 0; c < NC; ++c) s8[c] = 0.0f; + { + const int k = t + 8, kbx = k / F::ipb, iqs = F::step * (k % F::ipb); + const typename S::W w = S::load(row, kbx, iqs); +#pragma unroll + for (int c = 0; c < NC; ++c) + if (EXACT_N || c < n) s8[c] += S::apply(w, x + off[c] + kbx * (F::qk / 32), iqs); + } + { + const int k = t + 24, kbx = k / F::ipb, iqs = F::step * (k % F::ipb); + const typename S::W w = S::load(row, kbx, iqs); +#pragma unroll + for (int c = 0; c < NC; ++c) { + if (EXACT_N || c < n) { + float s24 = 0.0f; + s24 += S::apply(w, x + off[c] + kbx * (F::qk / 32), iqs); + s[c] = __fadd_rn(s[c], __fadd_rn(s8[c], s24)); + } + } + } +#pragma unroll + for (int c = 0; c < NC; ++c) { + if (EXACT_N || c < n) { +#pragma unroll + for (int o = 4; o > 0; o >>= 1) s[c] += __shfl_xor_sync(0xffffffffu, s[c], o); + } + } } +// 16-lane sub-warp row dot for K = nb * ipb == 20 (TD == 42, Q2_0 with n_ff == 640): +// 2 rows per warp (16 rows per 256-thread block), bitwise identical addition tree to row_dot_multi. +template +__device__ __forceinline__ void row_dot_20_sub16(const uint8_t* row, const block_q8_1* x, const int (&off)[NC], int n, + int t, float (&s)[NC]) { + using F = Fmt; + using S = Split; +#pragma unroll + for (int c = 0; c < NC; ++c) s[c] = 0.0f; + { + const int k = t, kbx = k / F::ipb, iqs = F::step * (k % F::ipb); + const typename S::W w = S::load(row, kbx, iqs); +#pragma unroll + for (int c = 0; c < NC; ++c) + if (EXACT_N || c < n) s[c] += S::apply(w, x + off[c] + kbx * (F::qk / 32), iqs); + } + float s16[NC]; +#pragma unroll + for (int c = 0; c < NC; ++c) s16[c] = 0.0f; + if (t < 4) { + const int k = t + 16, kbx = k / F::ipb, iqs = F::step * (k % F::ipb); + const typename S::W w = S::load(row, kbx, iqs); +#pragma unroll + for (int c = 0; c < NC; ++c) + if (EXACT_N || c < n) s16[c] += S::apply(w, x + off[c] + kbx * (F::qk / 32), iqs); + } +#pragma unroll + for (int c = 0; c < NC; ++c) { + if (EXACT_N || c < n) { + s[c] = __fadd_rn(s[c], s16[c]); +#pragma unroll + for (int o = 8; o > 0; o >>= 1) s[c] += __shfl_xor_sync(0xffffffffu, s[c], o); + } + } +} + +template> +__device__ __forceinline__ const uint32_t* stage_iq_grid(uint32_t* s_buf, int tid, int nthreads) { + if constexpr (!STAGE_GRID) { + return nullptr; + } else if constexpr (TY == 16) { + uint2* dst = (uint2*) s_buf; + const uint2* src = (const uint2*) iq2xxs_grid; + for (int i = tid; i < 256; i += nthreads) dst[i] = src[i]; + __syncthreads(); + return s_buf; + } else if constexpr (TY == 17) { + uint2* dst = (uint2*) s_buf; + const uint2* src = (const uint2*) iq2xs_grid; + for (int i = tid; i < 512; i += nthreads) dst[i] = src[i]; + __syncthreads(); + return s_buf; + } else if constexpr (TY == 22) { + uint2* dst = (uint2*) s_buf; + const uint2* src = (const uint2*) iq2s_grid; + for (int i = tid; i < 1024; i += nthreads) dst[i] = src[i]; + __syncthreads(); + return s_buf; + } else if constexpr (TY == 29) { + uint2* dst = (uint2*) s_buf; + const uint2* src = (const uint2*) iq1s_grid_gpu; + for (int i = tid; i < 1024; i += nthreads) dst[i] = src[i]; + __syncthreads(); + return s_buf; + } else { + return nullptr; + } +} + +template> +struct IqGridWords { + static constexpr int value = !STAGE_GRID ? 4 : (TY == 22 || TY == 29) ? 2048 : (TY == 17) ? 1024 : (TY == 16) ? 512 : 4; +}; + // mmvq_kernel with the columns taken NC at a time. After warp_sum every lane holds the same sum, so lane c stores // column c. -template +template> __global__ void __launch_bounds__(128) mmvq_multi_kernel(const uint8_t* __restrict__ w, size_t row_bytes, const block_q8_1* __restrict__ x, float* __restrict__ y, int n_in, int n_out, int ncols) { + __shared__ alignas(16) uint32_t s_grid_buf[IqGridWords::value]; + const uint32_t* s_grid = stage_iq_grid(s_grid_buf, threadIdx.y * 32 + threadIdx.x, 128); const int row = blockIdx.x * 4 + threadIdx.y; if (row >= n_out) return; const int lane = threadIdx.x; @@ -876,7 +1049,7 @@ __global__ void __launch_bounds__(128) mmvq_multi_kernel(const uint8_t* __restri #pragma unroll for (int c = 0; c < NC; ++c) off[c] = (c0 + min(c, n - 1)) * xb; float s[NC]; - row_dot_multi(wr, x, off, n, nb, lane, s); + row_dot_multi(wr, x, off, n, nb, lane, s, s_grid); #pragma unroll for (int c = 0; c < NC; ++c) if (c < n && lane == c) y[(size_t) (c0 + c) * n_out + row] = s[c]; @@ -922,13 +1095,17 @@ __global__ void __launch_bounds__(256) native_gu_kernel(const unsigned long long // has halves of <= 4), so 4 takes such windows in one pass; 8 would take ~64-80 registers against ~48 (ptxas -v). constexpr int GRP_NC = 4; -template +template> __global__ void __launch_bounds__(256) native_gu_multi_kernel(const unsigned long long* __restrict__ grp_ptr, const int32_t* __restrict__ grp_start, const int32_t* __restrict__ n_groups, const int32_t* __restrict__ ent_tok, const block_q8_1* __restrict__ xq, NativeExpertLayout L, float* __restrict__ gate, float* __restrict__ up) { + const int ng = *n_groups; + if (blockIdx.y >= ng) return; + __shared__ alignas(16) uint32_t s_grid_buf[IqGridWords::value]; + const uint32_t* s_grid = stage_iq_grid(s_grid_buf, threadIdx.x, 256); const int warp = threadIdx.x >> 5, lane = threadIdx.x & 31; const int row = blockIdx.x * GU_ROWS + warp; // 0 .. 2*n_ff if (row >= 2 * L.n_ff) return; @@ -937,20 +1114,31 @@ __global__ void __launch_bounds__(256) native_gu_multi_kernel(const unsigned lon const size_t w_off = (is_up ? L.up_off : 0) + (size_t) r * L.gu_row; const int nb = (int) (L.n_embd / Fmt::qk), xb = (int) (L.n_embd / 32); float* dst = is_up ? up : gate; - const int ng = *n_groups; for (int g = blockIdx.y; g < ng; g += gridDim.y) { // the group stride, as native_gu_kernel const uint8_t* wr = (const uint8_t*) grp_ptr[g] + w_off; const int e0 = grp_start[g], e1 = grp_start[g + 1]; for (int e = e0; e < e1; e += GRP_NC) { const int n = min(GRP_NC, e1 - e); - int off[GRP_NC]; + if (n == 1) { + const int off1[1] = { ent_tok[e] * xb }; + float s1[1]; + row_dot_multi(wr, xq, off1, 1, nb, lane, s1, s_grid); + if (lane == 0) dst[(size_t) e * L.n_ff + r] = s1[0]; + } else if (n == 2) { + const int off2[2] = { ent_tok[e] * xb, ent_tok[e + 1] * xb }; + float s2[2]; + row_dot_multi(wr, xq, off2, 2, nb, lane, s2, s_grid); + if (lane < 2) dst[(size_t) (e + lane) * L.n_ff + r] = s2[lane]; + } else { + int off[GRP_NC]; #pragma unroll - for (int c = 0; c < GRP_NC; ++c) off[c] = ent_tok[e + min(c, n - 1)] * xb; - float s[GRP_NC]; - row_dot_multi(wr, xq, off, n, nb, lane, s); + for (int c = 0; c < GRP_NC; ++c) off[c] = ent_tok[e + min(c, n - 1)] * xb; + float s[GRP_NC]; + row_dot_multi(wr, xq, off, n, nb, lane, s, s_grid); #pragma unroll - for (int c = 0; c < GRP_NC; ++c) - if (c < n && lane == c) dst[(size_t) (e + c) * L.n_ff + r] = s[c]; + for (int c = 0; c < GRP_NC; ++c) + if (c < n && lane == c) dst[(size_t) (e + c) * L.n_ff + r] = s[c]; + } } } } @@ -994,25 +1182,148 @@ __global__ void __launch_bounds__(256) native_down_multi_kernel(const unsigned l const int32_t* __restrict__ ent_dst, const block_q8_1* __restrict__ hq, NativeExpertLayout L, float* __restrict__ out) { + const int ng = *n_groups; + if (blockIdx.y >= ng) return; + __shared__ uint32_t s_hq_buf[GRP_NC * 20 * 9]; + const int nb = (int) (L.n_ff / Fmt::qk), hb = (int) (L.n_ff / 32); + if constexpr (TD == 20) { + if (hb == 20) { + const int r = blockIdx.x * 32 + (threadIdx.x >> 3); + const int t = threadIdx.x & 7; + const bool valid_r = (r < L.n_embd); + const size_t w_off = L.down_off + (size_t) (valid_r ? r : 0) * L.d_row; + const block_q8_1* s_hq = reinterpret_cast(s_hq_buf); + for (int g = blockIdx.y; g < ng; g += gridDim.y) { + const uint8_t* wr = (const uint8_t*) grp_ptr[g] + w_off; + const int e0 = grp_start[g], e1 = grp_start[g + 1]; + for (int e = e0; e < e1; e += GRP_NC) { + const int n = min(GRP_NC, e1 - e); + const uint32_t* src = reinterpret_cast(hq + (size_t) e * 20); + const int words = n * (20 * 9); + __syncthreads(); + for (int i = threadIdx.x; i < words; i += 256) s_hq_buf[i] = src[i]; + __syncthreads(); + if (valid_r) { + if (n == 1) { + const int off1[1] = { 0 }; + float s1[1]; + row_dot_40_sub8(wr, s_hq, off1, 1, t, s1); + if (t == 0) out[(size_t) ent_dst[e] * L.n_embd + r] = s1[0]; + } else if (n == 2) { + const int off2[2] = { 0, 20 }; + float s2[2]; + row_dot_40_sub8(wr, s_hq, off2, 2, t, s2); + if (t < 2) out[(size_t) ent_dst[e + t] * L.n_embd + r] = s2[t]; + } else { + int off[GRP_NC]; +#pragma unroll + for (int c = 0; c < GRP_NC; ++c) off[c] = min(c, n - 1) * 20; + float s[GRP_NC]; + row_dot_40_sub8(wr, s_hq, off, n, t, s); +#pragma unroll + for (int c = 0; c < GRP_NC; ++c) + if (c < n && t == c) out[(size_t) ent_dst[e + c] * L.n_embd + r] = s[c]; + } + } + } + } + return; + } + } else if constexpr (TD == 42) { + if (hb == 20) { + const int r = blockIdx.x * 16 + (threadIdx.x >> 4); + const int t = threadIdx.x & 15; + const bool valid_r = (r < L.n_embd); + const size_t w_off = L.down_off + (size_t) (valid_r ? r : 0) * L.d_row; + const block_q8_1* s_hq = reinterpret_cast(s_hq_buf); + for (int g = blockIdx.y; g < ng; g += gridDim.y) { + const uint8_t* wr = (const uint8_t*) grp_ptr[g] + w_off; + const int e0 = grp_start[g], e1 = grp_start[g + 1]; + for (int e = e0; e < e1; e += GRP_NC) { + const int n = min(GRP_NC, e1 - e); + const uint32_t* src = reinterpret_cast(hq + (size_t) e * 20); + const int words = n * (20 * 9); + __syncthreads(); + for (int i = threadIdx.x; i < words; i += 256) s_hq_buf[i] = src[i]; + __syncthreads(); + if (valid_r) { + if (n == 1) { + const int off1[1] = { 0 }; + float s1[1]; + row_dot_20_sub16(wr, s_hq, off1, 1, t, s1); + if (t == 0) out[(size_t) ent_dst[e] * L.n_embd + r] = s1[0]; + } else if (n == 2) { + const int off2[2] = { 0, 20 }; + float s2[2]; + row_dot_20_sub16(wr, s_hq, off2, 2, t, s2); + if (t < 2) out[(size_t) ent_dst[e + t] * L.n_embd + r] = s2[t]; + } else { + int off[GRP_NC]; +#pragma unroll + for (int c = 0; c < GRP_NC; ++c) off[c] = min(c, n - 1) * 20; + float s[GRP_NC]; + row_dot_20_sub16(wr, s_hq, off, n, t, s); +#pragma unroll + for (int c = 0; c < GRP_NC; ++c) + if (c < n && t == c) out[(size_t) ent_dst[e + c] * L.n_embd + r] = s[c]; + } + } + } + } + return; + } + } const int warp = threadIdx.x >> 5, lane = threadIdx.x & 31; const int r = blockIdx.x * 8 + warp; - if (r >= L.n_embd) return; - const size_t w_off = L.down_off + (size_t) r * L.d_row; - const int nb = (int) (L.n_ff / Fmt::qk), hb = (int) (L.n_ff / 32); - const int ng = *n_groups; + const bool valid_r = (r < L.n_embd); + const size_t w_off = L.down_off + (size_t) (valid_r ? r : 0) * L.d_row; for (int g = blockIdx.y; g < ng; g += gridDim.y) { // the group stride, as native_gu_kernel const uint8_t* wr = (const uint8_t*) grp_ptr[g] + w_off; const int e0 = grp_start[g], e1 = grp_start[g + 1]; - for (int e = e0; e < e1; e += GRP_NC) { - const int n = min(GRP_NC, e1 - e); - int off[GRP_NC]; + if (hb == 20) { + const block_q8_1* s_hq = reinterpret_cast(s_hq_buf); + for (int e = e0; e < e1; e += GRP_NC) { + const int n = min(GRP_NC, e1 - e); + const uint32_t* src = reinterpret_cast(hq + (size_t) e * 20); + const int words = n * (20 * 9); + __syncthreads(); + for (int i = threadIdx.x; i < words; i += 256) s_hq_buf[i] = src[i]; + __syncthreads(); + if (valid_r) { + if (n == 1) { + const int off1[1] = { 0 }; + float s1[1]; + row_dot_multi(wr, s_hq, off1, 1, nb, lane, s1); + if (lane == 0) out[(size_t) ent_dst[e] * L.n_embd + r] = s1[0]; + } else if (n == 2) { + const int off2[2] = { 0, 20 }; + float s2[2]; + row_dot_multi(wr, s_hq, off2, 2, nb, lane, s2); + if (lane < 2) out[(size_t) ent_dst[e + lane] * L.n_embd + r] = s2[lane]; + } else { + int off[GRP_NC]; #pragma unroll - for (int c = 0; c < GRP_NC; ++c) off[c] = (e + min(c, n - 1)) * hb; - float s[GRP_NC]; - row_dot_multi(wr, hq, off, n, nb, lane, s); + for (int c = 0; c < GRP_NC; ++c) off[c] = min(c, n - 1) * 20; + float s[GRP_NC]; + row_dot_multi(wr, s_hq, off, n, nb, lane, s); #pragma unroll - for (int c = 0; c < GRP_NC; ++c) - if (c < n && lane == c) out[(size_t) ent_dst[e + c] * L.n_embd + r] = s[c]; + for (int c = 0; c < GRP_NC; ++c) + if (c < n && lane == c) out[(size_t) ent_dst[e + c] * L.n_embd + r] = s[c]; + } + } + } + } else if (valid_r) { + for (int e = e0; e < e1; e += GRP_NC) { + const int n = min(GRP_NC, e1 - e); + int off[GRP_NC]; +#pragma unroll + for (int c = 0; c < GRP_NC; ++c) off[c] = (e + min(c, n - 1)) * hb; + float s[GRP_NC]; + row_dot_multi(wr, hq, off, n, nb, lane, s); +#pragma unroll + for (int c = 0; c < GRP_NC; ++c) + if (c < n && lane == c) out[(size_t) ent_dst[e + c] * L.n_embd + r] = s[c]; + } } } } @@ -1026,11 +1337,11 @@ __device__ __forceinline__ void q8_1_store(const float xi, block_q8_1* __restric amax = fmaxf(amax, __shfl_xor_sync(0xffffffffu, amax, o)); sum += __shfl_xor_sync(0xffffffffu, sum, o); } - const float d = amax / 127.0f; - const int8_t q = amax == 0.0f ? 0 : roundf(xi / d); + const float d = q8_1_finite(amax / 127.0f); // #606: q8_1_finite.hpp - the same bits for every finite block + const int8_t q = q8_1_quant(xi, d, amax); const long long ib = i / 32, iqs = i % 32; y[ib].qs[iqs] = q; - if (iqs == 0) y[ib].ds = make_half2(d, sum); + if (iqs == 0) y[ib].ds = q8_1_ds(d, sum); } __global__ void quantize_q8_1_kernel(const float* __restrict__ x, block_q8_1* __restrict__ y, long long n) { @@ -1211,6 +1522,16 @@ __device__ void dq_q2_0(const void* vx, int64_t ibs, dst_t* yy, int tid) { for (int j = 0; j < 8; ++j) { const int i = part * 8 + j; const int code = (x[b].qs[i / 4] >> ((i % 4) * 2)) & 3; +#if defined(__HIPCC__) && defined(__gfx1012__) && HIP_VERSION_MAJOR < 7 + // HIP 5.7 on RDNA1 folds the half path's negative scale times +0 + // to +0. Preserve the scale's sign, as the FP32/CPU paths do. + if constexpr (std::is_same_v) { + if (code == 1) { + yy[b * 64 + i] = __ushort_as_half(__half_as_ushort(x[b].d) & 0x8000u); + continue; + } + } +#endif yy[b * 64 + i] = cvt(d * (float) (code - 1)); } } @@ -1385,6 +1706,18 @@ bool env_on(const char* name) { } // STRATA_OLD_IQ_MMVQ=1 keeps the per-column kernels (bitwise equal to the new ones; kept for A/B timing) bool g_old_kernels = env_on("STRATA_OLD_IQ_MMVQ"); +// STRATA_IQ_STAGE_GRID=0 disables staging 64-bit i-quant codebook tables into shared memory +bool g_stage_grid = [] { + const char* v = std::getenv("STRATA_IQ_STAGE_GRID"); + return v == nullptr || v[0] == '\0' || v[0] != '0'; +}(); +// The single-matrix mmvq (128-thread blocks, one per 4 rows) pays the 2-8 KB staging per block: on the RTX 5070 its +// IQ2_XS / IQ2_S / IQ1_M calls were 10-20% slower staged at 3-8 columns (iq_multi_parity --bench), while the grouped +// expert kernels gain. So mmvq stages only with STRATA_IQ_STAGE_GRID_MMVQ=1 (bitwise the same either way). +bool g_stage_grid_mmvq = g_stage_grid && [] { + const char* v = std::getenv("STRATA_IQ_STAGE_GRID_MMVQ"); + return v != nullptr && v[0] == '1'; +}(); template void launch_mmvq(const uint8_t* W, size_t rb, const block_q8_1* X, float* y, int n_in, int n_out, int ncols, @@ -1392,10 +1725,24 @@ void launch_mmvq(const uint8_t* W, size_t rb, const block_q8_1* X, float* y, int const dim3 grid((unsigned) ((n_out + 3) / 4)), block(32, 4); if constexpr (!kSplit) mmvq_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); else if (g_old_kernels) mmvq_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); - else if (ncols <= 1) mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); - else if (ncols == 2) mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); - else if (ncols <= 4) mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); - else mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); // 8 at a time past 8 + else if constexpr (kStageIqGrid) { + if (!g_stage_grid_mmvq) { + if (ncols <= 1) mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); + else if (ncols == 2) mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); + else if (ncols <= 4) mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); + else mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); + return; + } + if (ncols <= 1) mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); + else if (ncols == 2) mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); + else if (ncols <= 4) mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); + else mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); + } else { + if (ncols <= 1) mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); + else if (ncols == 2) mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); + else if (ncols <= 4) mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); + else mmvq_multi_kernel<<>>(W, rb, X, y, n_in, n_out, ncols); // 8 at a time past 8 + } } template @@ -1404,7 +1751,13 @@ void launch_gu(dim3 grid, cudaStream_t s, const unsigned long long* grp_ptr, con float* gate, float* up) { if constexpr (!kSplit) native_gu_kernel<<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); else if (g_old_kernels) native_gu_kernel<<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); - else native_gu_multi_kernel<<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); + else if constexpr (kStageIqGrid) { + if (!g_stage_grid) { + native_gu_multi_kernel<<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); + return; + } + native_gu_multi_kernel<<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); + } else native_gu_multi_kernel<<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); } template @@ -1532,6 +1885,560 @@ size_t native_expert_scratch_bytes(int64_t cap, int64_t n_ff) { return 3 * ((f + 255) & ~(size_t) 255) + (((size_t) cap * (size_t) (n_ff / 32) * sizeof(block_q8_1) + 255) & ~(size_t) 255); } + +#if defined(STRATA_HIP_GFX906) +// ---- AMD layouts for the grouped native experts (STRATA_EXP_MODE; 0 = the CUDA one above). +// 1 (W64): a row per 64-lane wavefront - 4x the wavefronts, ~1-2 calls per lane, a 64-lane butterfly. +// 2 (R2): a 32-lane logical warp computes TWO rows in one loop - two independent load chains in flight. +// Either way a (row, entry) sum does not depend on the window size. +template +__device__ __forceinline__ float row_dot64(const uint8_t* row, const block_q8_1* x, int nb, int lane) { + using F = Fmt; + float s = 0.0f; + for (int k = lane; k < nb * F::ipb; k += 64) { + const int kbx = k / F::ipb, iqs = F::step * (k % F::ipb); + s += F::dot(row, x + kbx * (F::qk / 32), kbx, iqs); + } +#pragma unroll + for (int o = 32; o > 0; o >>= 1) s += __shfl_xor(s, o, 64); + return s; +} +template +__device__ __forceinline__ void row_dot2(const uint8_t* r0, const uint8_t* r1, const block_q8_1* x, int nb, int lane, + float& o0, float& o1) { + using F = Fmt; + float s0 = 0.0f, s1 = 0.0f; + for (int k = lane; k < nb * F::ipb; k += 32) { + const int kbx = k / F::ipb, iqs = F::step * (k % F::ipb); + const block_q8_1* xk = x + kbx * (F::qk / 32); + const float a = F::dot(r0, xk, kbx, iqs); + const float b = F::dot(r1, xk, kbx, iqs); + s0 += a; + s1 += b; + } + o0 = warp_sum(s0); + o1 = warp_sum(s1); +} +template +__device__ __forceinline__ void row_dotn(const uint8_t* const* r, const block_q8_1* x, int nb, int lane, float* o) { + using F = Fmt; + float acc[NR]; +#pragma unroll + for (int i = 0; i < NR; ++i) acc[i] = 0.0f; + for (int k = lane; k < nb * F::ipb; k += 32) { + const int kbx = k / F::ipb, iqs = F::step * (k % F::ipb); + const block_q8_1* xk = x + kbx * (F::qk / 32); + float d[NR]; +#pragma unroll + for (int i = 0; i < NR; ++i) d[i] = F::dot(r[i], xk, kbx, iqs); +#pragma unroll + for (int i = 0; i < NR; ++i) acc[i] += d[i]; + } +#pragma unroll + for (int i = 0; i < NR; ++i) o[i] = warp_sum(acc[i]); +} +template +__global__ void __launch_bounds__(256) native_gu_amd_kernel(const unsigned long long* __restrict__ grp_ptr, + const int32_t* __restrict__ grp_start, + const int32_t* __restrict__ n_groups, + const int32_t* __restrict__ ent_tok, + const block_q8_1* __restrict__ xq, NativeExpertLayout L, + float* __restrict__ gate, float* __restrict__ up) { + const int g = blockIdx.y; + if (g >= *n_groups) return; + const uint8_t* blob = (const uint8_t*) grp_ptr[g]; + const int nb = (int) (L.n_embd / Fmt::qk), xb = (int) (L.n_embd / 32); + const int e0 = grp_start[g], e1 = grp_start[g + 1]; + const int nrow = 2 * (int) L.n_ff; + auto wrow = [&](int row) -> const uint8_t* { + const bool is_up = row >= L.n_ff; + return blob + (is_up ? L.up_off : 0) + (size_t) (is_up ? row - (int) L.n_ff : row) * L.gu_row; + }; + auto put = [&](int row, int e, float v) { + const bool is_up = row >= L.n_ff; + (is_up ? up : gate)[(size_t) e * L.n_ff + (is_up ? row - (int) L.n_ff : row)] = v; + }; + if constexpr (MODE == 1) { + const int lane = threadIdx.x & 63, row = blockIdx.x * 4 + (threadIdx.x >> 6); + if (row >= nrow) return; + const uint8_t* wr = wrow(row); + for (int e = e0; e < e1; ++e) { + const float v = row_dot64(wr, xq + (size_t) ent_tok[e] * xb, nb, lane); + if (lane == 0) put(row, e, v); + } + } else if constexpr (MODE == 4) { + const int lane = threadIdx.x & 31, warp = threadIdx.x >> 5; + const int row0 = blockIdx.x * 32 + warp; + if (row0 >= nrow) return; + const uint8_t* w[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) w[i] = wrow(row0 + 8 * i < nrow ? row0 + 8 * i : row0); + for (int e = e0; e < e1; ++e) { + float o[4]; + row_dotn(w, xq + (size_t) ent_tok[e] * xb, nb, lane, o); + if (lane == 0) +#pragma unroll + for (int i = 0; i < 4; ++i) if (row0 + 8 * i < nrow) put(row0 + 8 * i, e, o[i]); + } + } else { + const int lane = threadIdx.x & 31, warp = threadIdx.x >> 5; + const int row0 = blockIdx.x * 16 + warp, row1 = row0 + 8; + if (row0 >= nrow) return; + const bool two = row1 < nrow; + const uint8_t* w0 = wrow(row0); + const uint8_t* w1 = wrow(two ? row1 : row0); + for (int e = e0; e < e1; ++e) { + float a, b; + row_dot2(w0, w1, xq + (size_t) ent_tok[e] * xb, nb, lane, a, b); + if (lane == 0) { put(row0, e, a); if (two) put(row1, e, b); } + } + } +} +template +__global__ void __launch_bounds__(256) native_down_amd_kernel(const unsigned long long* __restrict__ grp_ptr, + const int32_t* __restrict__ grp_start, + const int32_t* __restrict__ n_groups, + const int32_t* __restrict__ ent_dst, + const block_q8_1* __restrict__ hq, NativeExpertLayout L, + float* __restrict__ out) { + const int g = blockIdx.y; + if (g >= *n_groups) return; + const uint8_t* blob = (const uint8_t*) grp_ptr[g]; + const int nb = (int) (L.n_ff / Fmt::qk), hb = (int) (L.n_ff / 32); + const int e0 = grp_start[g], e1 = grp_start[g + 1]; + const int nrow = (int) L.n_embd; + if constexpr (MODE == 1) { + const int lane = threadIdx.x & 63, r = blockIdx.x * 4 + (threadIdx.x >> 6); + if (r >= nrow) return; + const uint8_t* wr = blob + L.down_off + (size_t) r * L.d_row; + for (int e = e0; e < e1; ++e) { + const float v = row_dot64(wr, hq + (size_t) e * hb, nb, lane); + if (lane == 0) out[(size_t) ent_dst[e] * L.n_embd + r] = v; + } + } else if constexpr (MODE == 4) { + const int lane = threadIdx.x & 31, warp = threadIdx.x >> 5; + const int r0 = blockIdx.x * 32 + warp; + if (r0 >= nrow) return; + const uint8_t* w[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) w[i] = blob + L.down_off + (size_t) (r0 + 8 * i < nrow ? r0 + 8 * i : r0) * L.d_row; + for (int e = e0; e < e1; ++e) { + float o[4]; + row_dotn(w, hq + (size_t) e * hb, nb, lane, o); + if (lane == 0) +#pragma unroll + for (int i = 0; i < 4; ++i) if (r0 + 8 * i < nrow) out[(size_t) ent_dst[e] * L.n_embd + r0 + 8 * i] = o[i]; + } + } else { + const int lane = threadIdx.x & 31, warp = threadIdx.x >> 5; + const int r0 = blockIdx.x * 16 + warp, r1 = r0 + 8; + if (r0 >= nrow) return; + const bool two = r1 < nrow; + const uint8_t* w0 = blob + L.down_off + (size_t) r0 * L.d_row; + const uint8_t* w1 = blob + L.down_off + (size_t) (two ? r1 : r0) * L.d_row; + for (int e = e0; e < e1; ++e) { + float a, b; + row_dot2(w0, w1, hq + (size_t) e * hb, nb, lane, a, b); + if (lane == 0) { + out[(size_t) ent_dst[e] * L.n_embd + r0] = a; + if (two) out[(size_t) ent_dst[e] * L.n_embd + r1] = b; + } + } + } +} +// ---- 5 (LDS): gate/up of the grid formats (IQ2_S, IQ3_XXS, IQ3_S) with the codebook grid and the group's q8_1 +// activations in LDS. The bench showed gate/up compute-bound, not bandwidth-bound (T = 2 costs ~1.8x T = 1 on the +// same weights, 110-130 GB/s): each call issues its grid lookups and eight activation loads through the vector +// memory path after the weight load. Same calls, same lane-strided k, same R2 row pairs, same warp_sum: the +// output is bitwise the mode-2 one. +template struct GridOf; +template<> struct GridOf<22> { using T = uint64_t; static constexpr int N = 1024; __device__ static const T* src() { return iq2s_grid; } }; +template<> struct GridOf<18> { using T = uint32_t; static constexpr int N = 256; __device__ static const T* src() { return iq3xxs_grid; } }; +template<> struct GridOf<21> { using T = uint32_t; static constexpr int N = 512; __device__ static const T* src() { return iq3s_grid; } }; +template<> struct GridOf<23> { using T = uint32_t; static constexpr int N = 1; __device__ static const T* src() { return iq3s_grid; } }; // IQ4_XS: no grid + +// SG = 1 (mode 6): the signs without byte-SIMD ops, which gfx906 lacks (strata_vcmpne4 / strata_vsub4 unpack to +// ~12 word ops each). m = the sign nibble as 0x00/0xff bytes; (g ^ m) is g or -g-1 per byte, so +// dp4a(g ^ m, u) - dp4a(m, u) = sum(s g u) exactly - the same integers, the same floats. +__device__ __forceinline__ int nib_mask(uint32_t n) { + const uint32_t b = __umul24(n & 0xFu, 0x204081u) & 0x01010101u; + return (int) ((b << 8) - b); +} +template struct DotG; +template struct DotG<22, SG> { __device__ static __forceinline__ float f(const void* vbq, const block_q8_1* bq8_1, int kbx, int iqs, + const uint64_t* grid) { + const block_iq2_s* bq2 = (const block_iq2_s*) vbq + kbx; + const int qs_packed = get_int_b2(bq2->qs, iqs / 2); + const uint8_t* qs = (const uint8_t*) &qs_packed; + const int qh = bq2->qh[iqs / 2]; + const int signs_packed_32 = get_int_b2(bq2->qs, QK_K / 32 + iqs / 2); + const uint8_t* signs_packed_8 = (const uint8_t*) &signs_packed_32; + const int ls0 = bq2->scales[iqs / 2] & 0x0F; + const int ls1 = bq2->scales[iqs / 2] >> 4; + int sumi0 = 0, sumi1 = 0; +#pragma unroll + for (int l0 = 0; l0 < 8; l0 += 2) { + const int* grid_pos = (const int*) (grid + (qs[l0 / 2] | ((qh << (8 - l0)) & 0x300))); + const int u0 = get_int_b4(bq8_1[iqs / 2].qs, l0 + 0); + const int u1 = get_int_b4(bq8_1[iqs / 2].qs, l0 + 1); + int& acc = l0 < 4 ? sumi0 : sumi1; + if constexpr (SG == 1) { + const int m0 = nib_mask(signs_packed_8[l0 / 2]), m1 = nib_mask(signs_packed_8[l0 / 2] >> 4); + acc = ggml_cuda_dp4a(grid_pos[0] ^ m0, u0, acc); + acc = ggml_cuda_dp4a(grid_pos[1] ^ m1, u1, acc); + acc -= ggml_cuda_dp4a(m1, u1, ggml_cuda_dp4a(m0, u0, 0)); + } else { + const int signs0 = __vcmpne4(((signs_packed_8[l0 / 2] & 0x03) << 7) | ((signs_packed_8[l0 / 2] & 0x0C) << 21), 0x00000000); + const int signs1 = __vcmpne4(((signs_packed_8[l0 / 2] & 0x30) << 3) | ((signs_packed_8[l0 / 2] & 0xC0) << 17), 0x00000000); + const int grid_l = __vsub4(grid_pos[0] ^ signs0, signs0); + const int grid_h = __vsub4(grid_pos[1] ^ signs1, signs1); + acc = ggml_cuda_dp4a(grid_l, u0, acc); + acc = ggml_cuda_dp4a(grid_h, u1, acc); + } + } + const int sumi = (sumi0 * ls0 + sumi1 * ls1 + (sumi0 + sumi1) / 2) / 4; + const float d = __half2float(bq2->d) * __low2float(bq8_1[iqs / 2].ds); + return d * sumi; +} }; +template struct DotG<18, SG> { __device__ static __forceinline__ float f(const void* vbq, const block_q8_1* bq8_1, int kbx, int iqs, + const uint32_t* grid) { + const block_iq3_xxs* bq3 = (const block_iq3_xxs*) vbq + kbx; + const int2 q3_packed = make_int2(get_int_b2(bq3->qs, iqs), get_int_b2(bq3->qs, iqs + 1)); + const uint8_t* q3 = (const uint8_t*) &q3_packed; + const uint32_t aux32 = get_int_b2(bq3->qs, QK_K / 16 + iqs / 2); + int sumi = 0; +#pragma unroll + for (int l0 = 0; l0 < 8; l0 += 2) { + const int2 grid_pos = make_int2(grid[q3[l0 + 0]], grid[q3[l0 + 1]]); + const uint32_t signs = unpack_ksigns(aux32 >> (7 * l0 / 2)); + const int u0 = get_int_b4(bq8_1[iqs / 2].qs, l0 + 0); + const int u1 = get_int_b4(bq8_1[iqs / 2].qs, l0 + 1); + if constexpr (SG == 1) { + const int m0 = nib_mask(signs), m1 = nib_mask(signs >> 4); + sumi = ggml_cuda_dp4a(grid_pos.x ^ m0, u0, sumi); + sumi = ggml_cuda_dp4a(grid_pos.y ^ m1, u1, sumi); + sumi -= ggml_cuda_dp4a(m1, u1, ggml_cuda_dp4a(m0, u0, 0)); + } else { + const int signs0 = __vcmpne4(signs & 0x08040201, 0); + const int grid_l = __vsub4(grid_pos.x ^ signs0, signs0); + const int signs1 = __vcmpne4(signs & 0x80402010, 0); + const int grid_h = __vsub4(grid_pos.y ^ signs1, signs1); + sumi = ggml_cuda_dp4a(grid_l, u0, sumi); + sumi = ggml_cuda_dp4a(grid_h, u1, sumi); + } + } + const int ls = aux32 >> 28; + sumi = (ls * sumi + sumi / 2) / 2; + const float d = __half2float(bq3->d) * __low2float(bq8_1[iqs / 2].ds); + return d * sumi; +} }; +template struct DotG<21, SG> { __device__ static __forceinline__ float f(const void* vbq, const block_q8_1* bq8_1, int kbx, int iqs, + const uint32_t* grid) { + const block_iq3_s* bq3 = (const block_iq3_s*) vbq + kbx; + const int2 qs_packed = make_int2(get_int_b2(bq3->qs, iqs + 0), get_int_b2(bq3->qs, iqs + 1)); + const uint8_t* qs = (const uint8_t*) &qs_packed; + const int qh = bq3->qh[iqs / 2]; + const int signs_packed_32 = get_int_b2(bq3->signs, iqs / 2); + const uint8_t* signs_packed_8 = (const uint8_t*) &signs_packed_32; + int sumi = 0; +#pragma unroll + for (int l0 = 0; l0 < 8; l0 += 2) { + const int2 grid_pos = make_int2(grid[qs[l0 + 0] | ((qh << (8 - l0)) & 0x100)], + grid[qs[l0 + 1] | ((qh << (7 - l0)) & 0x100)]); + const int u0 = get_int_b4(bq8_1[iqs / 2].qs, l0 + 0); + const int u1 = get_int_b4(bq8_1[iqs / 2].qs, l0 + 1); + if constexpr (SG == 1) { + const int m0 = nib_mask(signs_packed_8[l0 / 2]), m1 = nib_mask(signs_packed_8[l0 / 2] >> 4); + sumi = ggml_cuda_dp4a(grid_pos.x ^ m0, u0, sumi); + sumi = ggml_cuda_dp4a(grid_pos.y ^ m1, u1, sumi); + sumi -= ggml_cuda_dp4a(m1, u1, ggml_cuda_dp4a(m0, u0, 0)); + } else { + const int signs0 = __vcmpne4(((signs_packed_8[l0 / 2] & 0x03) << 7) | ((signs_packed_8[l0 / 2] & 0x0C) << 21), 0x00000000); + const int signs1 = __vcmpne4(((signs_packed_8[l0 / 2] & 0x30) << 3) | ((signs_packed_8[l0 / 2] & 0xC0) << 17), 0x00000000); + const int grid_l = __vsub4(grid_pos.x ^ signs0, signs0); + const int grid_h = __vsub4(grid_pos.y ^ signs1, signs1); + sumi = ggml_cuda_dp4a(grid_l, u0, sumi); + sumi = ggml_cuda_dp4a(grid_h, u1, sumi); + } + } + sumi *= 1 + 2 * ((bq3->scales[iqs / 4] >> ((iqs << 1) & 0x04)) & 0x0F); + const float d = __half2float(bq3->d) * __low2float(bq8_1[iqs / 2].ds); + return d * sumi; +} }; + +template struct DotG<23, SG> { __device__ static __forceinline__ float f(const void* vbq, const block_q8_1* bq8_1, int kbx, int iqs, + const uint32_t*) { return vec_dot_iq4_xs_q8_1(vbq, bq8_1, kbx, iqs); } }; + +constexpr int LDS_RB = 64; // gate/up rows per block (4 R2 passes of 16) +constexpr int LDS_NT = 4; // entries whose activations sit in LDS at once + +template +__global__ void __launch_bounds__(256) native_gu_lds_kernel(const unsigned long long* __restrict__ grp_ptr, + const int32_t* __restrict__ grp_start, + const int32_t* __restrict__ n_groups, + const int32_t* __restrict__ ent_tok, + const block_q8_1* __restrict__ xq, NativeExpertLayout L, + float* __restrict__ gate, float* __restrict__ up) { + using GT = typename GridOf::T; + using F = Fmt; + __shared__ GT sgrid[GridOf::N]; + extern __shared__ int sx_raw[]; // LDS_NT tokens x xb blocks of q8_1 (36 bytes = 9 ints each) + const int g = blockIdx.y; + if (g >= *n_groups) return; // uniform over the block + const int tid = threadIdx.x, lane = tid & 31, warp = tid >> 5; + const GT* gsrc = GridOf::src(); + for (int i = tid; i < GridOf::N; i += 256) sgrid[i] = gsrc[i]; + const uint8_t* blob = (const uint8_t*) grp_ptr[g]; + const int nb = (int) (L.n_embd / F::qk), xb = (int) (L.n_embd / 32); + const int e0 = grp_start[g], e1 = grp_start[g + 1]; + const int nrow = 2 * (int) L.n_ff; + const block_q8_1* sx = (const block_q8_1*) sx_raw; + auto wrow = [&](int row) -> const uint8_t* { + const bool is_up = row >= L.n_ff; + return blob + (is_up ? L.up_off : 0) + (size_t) (is_up ? row - (int) L.n_ff : row) * L.gu_row; + }; + auto put = [&](int row, int e, float v) { + const bool is_up = row >= L.n_ff; + (is_up ? up : gate)[(size_t) e * L.n_ff + (is_up ? row - (int) L.n_ff : row)] = v; + }; + for (int c0 = e0; c0 < e1; c0 += LDS_NT) { + const int cn = min(LDS_NT, e1 - c0); + __syncthreads(); + for (int j = 0; j < cn; ++j) { + const int* src = (const int*) (xq + (size_t) ent_tok[c0 + j] * xb); + for (int i = tid; i < xb * 9; i += 256) sx_raw[j * xb * 9 + i] = src[i]; + } + __syncthreads(); + for (int p = 0; p < LDS_RB / 16; ++p) { + const int row0 = blockIdx.x * LDS_RB + p * 16 + warp, row1 = row0 + 8; + if (row0 >= nrow) break; + const bool two = row1 < nrow; + const uint8_t* w0 = wrow(row0); + const uint8_t* w1 = wrow(two ? row1 : row0); + if constexpr (TI) { // mode 7: the tokens inside the k loop - one weight load per k for all of them + auto pass = [&](auto ntc) { + constexpr int NTC = decltype(ntc)::value; + float s0[NTC], s1[NTC]; +#pragma unroll + for (int j = 0; j < NTC; ++j) { s0[j] = 0.0f; s1[j] = 0.0f; } + for (int k = lane; k < nb * F::ipb; k += 32) { + const int kbx = k / F::ipb, iqs = F::step * (k % F::ipb); +#pragma unroll + for (int j = 0; j < NTC; ++j) { + const block_q8_1* xk = sx + (size_t) j * xb + kbx * (F::qk / 32); + const float a = DotG::f(w0, xk, kbx, iqs, sgrid); + const float b = DotG::f(w1, xk, kbx, iqs, sgrid); + s0[j] += a; + s1[j] += b; + } + } +#pragma unroll + for (int j = 0; j < NTC; ++j) { + const float a = warp_sum(s0[j]), b = warp_sum(s1[j]); + if (lane == 0) { put(row0, c0 + j, a); if (two) put(row1, c0 + j, b); } + } + }; + switch (cn) { + case 1: pass(std::integral_constant{}); break; + case 2: pass(std::integral_constant{}); break; + case 3: pass(std::integral_constant{}); break; + default: pass(std::integral_constant{}); break; + } + continue; + } + for (int j = 0; j < cn; ++j) { + const block_q8_1* x = sx + (size_t) j * xb; + float s0 = 0.0f, s1 = 0.0f; + for (int k = lane; k < nb * F::ipb; k += 32) { + const int kbx = k / F::ipb, iqs = F::step * (k % F::ipb); + const block_q8_1* xk = x + kbx * (F::qk / 32); + const float a = DotG::f(w0, xk, kbx, iqs, sgrid); + const float b = DotG::f(w1, xk, kbx, iqs, sgrid); + s0 += a; + s1 += b; + } + s0 = warp_sum(s0); + s1 = warp_sum(s1); + if (lane == 0) { put(row0, c0 + j, s0); if (two) put(row1, c0 + j, s1); } + } + } + } +} + +// ---- 7 (down): the group's q8_1 h rows in LDS, the tokens inside the k loop, the R2 row pairs of mode 2 - +// the same per-(row, entry) sums in the same order: bitwise the mode-2 output. +template +__global__ void __launch_bounds__(256) native_down_lds_kernel(const unsigned long long* __restrict__ grp_ptr, + const int32_t* __restrict__ grp_start, + const int32_t* __restrict__ n_groups, + const int32_t* __restrict__ ent_dst, + const block_q8_1* __restrict__ hq, NativeExpertLayout L, + float* __restrict__ out) { + using F = Fmt; + extern __shared__ int sh_raw[]; // LDS_NT entries x hb blocks of q8_1 + const int g = blockIdx.y; + if (g >= *n_groups) return; + const int tid = threadIdx.x, lane = tid & 31, warp = tid >> 5; + const uint8_t* blob = (const uint8_t*) grp_ptr[g]; + const int nb = (int) (L.n_ff / F::qk), hb = (int) (L.n_ff / 32); + const int e0 = grp_start[g], e1 = grp_start[g + 1]; + const int nrow = (int) L.n_embd; + const block_q8_1* sh = (const block_q8_1*) sh_raw; + for (int c0 = e0; c0 < e1; c0 += LDS_NT) { + const int cn = min(LDS_NT, e1 - c0); + __syncthreads(); + { + const int* src = (const int*) (hq + (size_t) c0 * hb); // the chunk's entries are contiguous + for (int i = tid; i < cn * hb * 9; i += 256) sh_raw[i] = src[i]; + } + __syncthreads(); + for (int p = 0; p < LDS_RB / 16; ++p) { + const int r0 = blockIdx.x * LDS_RB + p * 16 + warp, r1 = r0 + 8; + if (r0 >= nrow) break; + const bool two = r1 < nrow; + const uint8_t* w0 = blob + L.down_off + (size_t) r0 * L.d_row; + const uint8_t* w1 = blob + L.down_off + (size_t) (two ? r1 : r0) * L.d_row; + auto pass = [&](auto ntc) { + constexpr int NTC = decltype(ntc)::value; + float s0[NTC], s1[NTC]; +#pragma unroll + for (int j = 0; j < NTC; ++j) { s0[j] = 0.0f; s1[j] = 0.0f; } + for (int k = lane; k < nb * F::ipb; k += 32) { + const int kbx = k / F::ipb, iqs = F::step * (k % F::ipb); +#pragma unroll + for (int j = 0; j < NTC; ++j) { + const block_q8_1* xk = sh + (size_t) j * hb + kbx * (F::qk / 32); + const float a = F::dot(w0, xk, kbx, iqs); + const float b = F::dot(w1, xk, kbx, iqs); + s0[j] += a; + s1[j] += b; + } + } +#pragma unroll + for (int j = 0; j < NTC; ++j) { + const float a = warp_sum(s0[j]), b = warp_sum(s1[j]); + if (lane == 0) { + out[(size_t) ent_dst[c0 + j] * L.n_embd + r0] = a; + if (two) out[(size_t) ent_dst[c0 + j] * L.n_embd + r1] = b; + } + } + }; + switch (cn) { + case 1: pass(std::integral_constant{}); break; + case 2: pass(std::integral_constant{}); break; + case 3: pass(std::integral_constant{}); break; + default: pass(std::integral_constant{}); break; + } + } + } +} + +// ---- 8: mode 7's gate/up with SwiGLU and the q8_1 quantization in the epilogue. A block takes h rows +// [r0, r0 + 32): gate rows r0.. and up rows r0.. (64 weight rows, the same per-(row, entry) sums as mode 7), so +// one 32-lane warp per entry holds exactly one q8_1 block of h and runs quantize_q8_1_kernel's own code on it - +// bitwise the separate kernels' hq, two launches fewer per call. +template +__global__ void __launch_bounds__(256) native_gu_fused_kernel(const unsigned long long* __restrict__ grp_ptr, + const int32_t* __restrict__ grp_start, + const int32_t* __restrict__ n_groups, + const int32_t* __restrict__ ent_tok, + const block_q8_1* __restrict__ xq, NativeExpertLayout L, + block_q8_1* __restrict__ hq) { + using GT = typename GridOf::T; + using F = Fmt; + __shared__ GT sgrid[GridOf::N]; + __shared__ float res[LDS_NT][64]; + extern __shared__ int sx_raw[]; + const int g = blockIdx.y; + if (g >= *n_groups) return; + const int tid = threadIdx.x, lane = tid & 31, warp = tid >> 5; + const GT* gsrc = GridOf::src(); + for (int i = tid; i < GridOf::N; i += 256) sgrid[i] = gsrc[i]; + const uint8_t* blob = (const uint8_t*) grp_ptr[g]; + const int nb = (int) (L.n_embd / F::qk), xb = (int) (L.n_embd / 32), hb = (int) (L.n_ff / 32); + const int e0 = grp_start[g], e1 = grp_start[g + 1]; + const int r0 = blockIdx.x * 32; + const block_q8_1* sx = (const block_q8_1*) sx_raw; + auto wrow = [&](int i) -> const uint8_t* { // i < 32: gate row r0 + i, else up row r0 + i - 32 + return blob + (i < 32 ? (size_t) 0 : L.up_off) + (size_t) (r0 + (i & 31)) * L.gu_row; + }; + for (int c0 = e0; c0 < e1; c0 += LDS_NT) { + const int cn = min(LDS_NT, e1 - c0); + __syncthreads(); + for (int j = 0; j < cn; ++j) { + const int* src = (const int*) (xq + (size_t) ent_tok[c0 + j] * xb); + for (int i = tid; i < xb * 9; i += 256) sx_raw[j * xb * 9 + i] = src[i]; + } + __syncthreads(); + for (int p = 0; p < 4; ++p) { + const int i0 = p * 16 + warp, i1 = i0 + 8; + const uint8_t* w0 = wrow(i0); + const uint8_t* w1 = wrow(i1); + auto pass = [&](auto ntc) { + constexpr int NTC = decltype(ntc)::value; + float s0[NTC], s1[NTC]; +#pragma unroll + for (int j = 0; j < NTC; ++j) { s0[j] = 0.0f; s1[j] = 0.0f; } + for (int k = lane; k < nb * F::ipb; k += 32) { + const int kbx = k / F::ipb, iqs = F::step * (k % F::ipb); +#pragma unroll + for (int j = 0; j < NTC; ++j) { + const block_q8_1* xk = sx + (size_t) j * xb + kbx * (F::qk / 32); + const float a = DotG::f(w0, xk, kbx, iqs, sgrid); + const float b = DotG::f(w1, xk, kbx, iqs, sgrid); + s0[j] += a; + s1[j] += b; + } + } +#pragma unroll + for (int j = 0; j < NTC; ++j) { + const float a = warp_sum(s0[j]), b = warp_sum(s1[j]); + if (lane == 0) { res[j][i0] = a; res[j][i1] = b; } + } + }; + switch (cn) { + case 1: pass(std::integral_constant{}); break; + case 2: pass(std::integral_constant{}); break; + case 3: pass(std::integral_constant{}); break; + default: pass(std::integral_constant{}); break; + } + } + __syncthreads(); + if (warp < cn) { // swiglu_entries_kernel + quantize_q8_1_kernel, one block of 32 h values per entry + const float gg = res[warp][lane], uu = res[warp][32 + lane]; + const float xi = (gg / (1.0f + __expf(-gg))) * uu; + float amax = fabsf(xi), sum = xi; +#pragma unroll + for (int o = 16; o > 0; o >>= 1) { + amax = fmaxf(amax, __shfl_xor_sync(0xffffffffu, amax, o)); + sum += __shfl_xor_sync(0xffffffffu, sum, o); + } + const float d = amax / 127.0f; + const int8_t q = amax == 0.0f ? 0 : roundf(xi / d); + block_q8_1* y = hq + (size_t) (c0 + warp) * hb + blockIdx.x; + y->qs[lane] = q; + if (lane == 0) y->ds = make_half2(d, sum); + } + } +} + +int g_exp_mode = -1; // native_expert_set_mode (the bench); -1 = STRATA_EXP_MODE, default 7 +int exp_mode() { + static const int m = [] { const char* v = std::getenv("STRATA_EXP_MODE"); return v ? std::atoi(v) : 7; }(); + return g_exp_mode >= 0 ? g_exp_mode : m; +} +#endif +int g_exp_phase = 0; // the bench: 0 all, 1 gate/up + swiglu + quantize only, 2 down only + +void native_expert_set_mode(int mode, int phase) { +#if defined(STRATA_HIP_GFX906) + g_exp_mode = mode; +#else + (void) mode; +#endif + g_exp_phase = phase; +} + namespace { bool g_grouped_v1 = env_on("STRATA_GROUPED_V1"); } // namespace @@ -1554,15 +2461,76 @@ void native_expert_grouped(const NativeExpertLayout& L, const unsigned long long const bool v1 = g_grouped_v1; const int64_t gy = (v1 || grid_groups <= 0 || grid_groups > cap_groups) ? cap_groups : grid_groups; const dim3 ggu((unsigned) ((2 * L.n_ff + GU_ROWS - 1) / GU_ROWS), (unsigned) gy); +#if defined(STRATA_HIP_GFX906) + const int em0 = exp_mode(); +#endif +#if defined(STRATA_HIP_GFX906) + const bool fused_gu = em0 == 8 && (L.gu_type == 18 || L.gu_type == 21 || L.gu_type == 22 || L.gu_type == 23) && + L.n_ff % 32 == 0; + if (fused_gu && g_exp_phase != 2) { + const dim3 gl((unsigned) (L.n_ff / 32), (unsigned) cap_groups); + const size_t sh = (size_t) LDS_NT * (size_t) (L.n_embd / 32) * sizeof(block_q8_1); + switch (L.gu_type) { + case 18: native_gu_fused_kernel<18><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, hq); break; + case 21: native_gu_fused_kernel<21><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, hq); break; + case 23: native_gu_fused_kernel<23><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, hq); break; + default: native_gu_fused_kernel<22><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, hq); break; + } + check("native_expert_grouped/gu fused"); + } + if (g_exp_phase != 2 && !fused_gu) { +#else + if (g_exp_phase != 2) { +#endif +#if defined(STRATA_HIP_GFX906) + const bool lds_gu = (em0 == 5 || em0 == 6 || em0 == 7 || em0 == 8) && (L.gu_type == 18 || L.gu_type == 21 || L.gu_type == 22 || L.gu_type == 23); + if (lds_gu) { + const dim3 gl((unsigned) ((2 * L.n_ff + LDS_RB - 1) / LDS_RB), (unsigned) cap_groups); + const size_t sh = (size_t) LDS_NT * (size_t) (L.n_embd / 32) * sizeof(block_q8_1); + switch (L.gu_type) { + case 18: if (em0 >= 7) native_gu_lds_kernel<18, 1, true><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); else if (em0 == 6) native_gu_lds_kernel<18, 1><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); else native_gu_lds_kernel<18, 0><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); break; + case 21: if (em0 >= 7) native_gu_lds_kernel<21, 1, true><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); else if (em0 == 6) native_gu_lds_kernel<21, 1><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); else native_gu_lds_kernel<21, 0><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); break; + case 23: if (em0 >= 7) native_gu_lds_kernel<23, 1, true><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); else native_gu_lds_kernel<23, 1><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); break; + default: if (em0 >= 7) native_gu_lds_kernel<22, 1, true><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); else if (em0 == 6) native_gu_lds_kernel<22, 1><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); else native_gu_lds_kernel<22, 0><<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); break; + } + } else { + const int em = exp_mode() >= 5 ? 2 : exp_mode(); + const dim3 ggu_amd((unsigned) ((2 * L.n_ff + (em == 1 ? 3 : em == 4 ? 31 : 15)) / (em == 1 ? 4 : em == 4 ? 32 : 16)), (unsigned) cap_groups); +#define STRATA_GU_AMD(T) \ + case T: if (em == 1) native_gu_amd_kernel<<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); \ + else if (em == 4) native_gu_amd_kernel<<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); \ + else native_gu_amd_kernel<<>>(grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); break; + // the AMD layouts cover the IQ packs' gate/up types; Unsloth's Q4_K / Q5_K take the CUDA layout below + const bool amd_gu = L.gu_type == 16 || L.gu_type == 17 || L.gu_type == 18 || L.gu_type == 21 || L.gu_type == 22 || + L.gu_type == 23 || L.gu_type == 29 || L.gu_type == 42; + if ((em == 1 || em == 2 || em == 4) && amd_gu) { + switch (L.gu_type) { + STRATA_GU_AMD(16) STRATA_GU_AMD(17) STRATA_GU_AMD(18) STRATA_GU_AMD(21) STRATA_GU_AMD(22) STRATA_GU_AMD(23) + STRATA_GU_AMD(29) STRATA_GU_AMD(42) + } + } else +#undef STRATA_GU_AMD +#endif switch (L.gu_type) { #define STRATA_GU(T) case T: launch_gu(ggu, s, grp_ptr, grp_start, n_groups, ent_tok, X, L, gate, up); break; STRATA_GU_FMTS(STRATA_GU) #undef STRATA_GU default: std::fprintf(stderr, "native_expert_grouped: gate/up type %d\n", L.gu_type); std::exit(1); } +#if defined(STRATA_HIP_GFX906) + } +#endif check("native_expert_grouped/gu"); const long long nh = (long long) cap_entries * L.n_ff; - if (v1) { +#if defined(STRATA_HIP_GFX906) + // gfx906: the separate SwiGLU and q8_1 passes stay the default (the A/B baseline); the RDNA one-pass + // kernel (bitwise the same) with STRATA_HIP_SWIGLU_FUSED=1 + static const bool sw_fused = env_on("STRATA_HIP_SWIGLU_FUSED"); + const bool sw_v1 = v1 || !sw_fused; +#else + const bool sw_v1 = v1; +#endif + if (sw_v1) { swiglu_entries_kernel<<<(unsigned) ((nh + 255) / 256), 256, 0, s>>>(gate, up, h, nh); quantize_q8_1_kernel<<<(unsigned) ((nh + 255) / 256), 256, 0, s>>>(h, hq, nh); } else { @@ -1570,7 +2538,34 @@ void native_expert_grouped(const NativeExpertLayout& L, const unsigned long long (int) L.n_ff, hq); } check("native_expert_grouped/swiglu"); - const dim3 gd((unsigned) ((L.n_embd + 7) / 8), (unsigned) gy); + } + if (g_exp_phase == 1) return; + const int d_rows = (!g_old_kernels && L.n_ff == 640) ? (L.d_type == 20 ? 32 : (L.d_type == 42 ? 16 : 8)) : 8; + const dim3 gd((unsigned) ((L.n_embd + d_rows - 1) / d_rows), (unsigned) gy); +#if defined(STRATA_HIP_GFX906) + if ((em0 == 7 || em0 == 8) && (L.d_type == 20 || L.d_type == 42)) { + const dim3 gl((unsigned) ((L.n_embd + LDS_RB - 1) / LDS_RB), (unsigned) cap_groups); + const size_t sh = (size_t) LDS_NT * (size_t) (L.n_ff / 32) * sizeof(block_q8_1); + if (L.d_type == 20) native_down_lds_kernel<20><<>>(grp_ptr, grp_start, n_groups, ent_dst, hq, L, out); + else native_down_lds_kernel<42><<>>(grp_ptr, grp_start, n_groups, ent_dst, hq, L, out); + check("native_expert_grouped/down"); + return; + } + const int em = exp_mode() >= 5 ? 2 : exp_mode(); + const dim3 gd_amd((unsigned) ((L.n_embd + (em == 1 ? 3 : em == 4 ? 31 : 15)) / (em == 1 ? 4 : em == 4 ? 32 : 16)), (unsigned) cap_groups); +#define STRATA_D_AMD(T) \ + case T: if (em == 1) native_down_amd_kernel<<>>(grp_ptr, grp_start, n_groups, ent_dst, hq, L, out); \ + else if (em == 4) native_down_amd_kernel<<>>(grp_ptr, grp_start, n_groups, ent_dst, hq, L, out); \ + else native_down_amd_kernel<<>>(grp_ptr, grp_start, n_groups, ent_dst, hq, L, out); break; + // the AMD layouts cover the IQ packs' down types; the others (Unsloth's Q4_K / Q5_K / Q5_1 / Q8_0) take the + // CUDA layout below + if ((em == 1 || em == 2 || em == 4) && (L.d_type == 20 || L.d_type == 23 || L.d_type == 42)) { + switch (L.d_type) { + STRATA_D_AMD(20) STRATA_D_AMD(23) STRATA_D_AMD(42) + } + } else +#undef STRATA_D_AMD +#endif switch (L.d_type) { #define STRATA_DOWN(T) case T: launch_down(gd, s, grp_ptr, grp_start, n_groups, ent_dst, hq, L, out); break; STRATA_D_FMTS(STRATA_DOWN) diff --git a/src/kernels/cuda/kv_stream.cu b/src/kernels/cuda/kv_stream.cu index ec01ff2294..914a836ef1 100644 --- a/src/kernels/cuda/kv_stream.cu +++ b/src/kernels/cuda/kv_stream.cu @@ -251,6 +251,16 @@ void kv_stage_from_host(const QsaAttnPools& stage, const KvHostPools& host, int check("stage"); } +void kv_unstage_to_host(const QsaAttnPools& stage, const KvHostPools& host, int fmt, int64_t b0, int64_t b1, + const QsaShapes& s, void* stream) { + if (b1 <= b0) return; + const Runs r = runs_of(stage, host, fmt, s); // src: the host copy, dst: the staging pool (identity layout both) + for (int a = 0; a < r.n; ++a) + if (cudaMemcpyAsync((void*) (r.src[a] + b0 * r.len[a]), r.dst[a] + b0 * r.len[a], (size_t) ((b1 - b0) * r.len[a]), + cudaMemcpyDefault, (cudaStream_t) stream) != cudaSuccess) + check("unstage"); +} + KvStreamCounters kv_stream_counters(const KvStreamMap& m) { int32_t c[kKvCtlInts] = {}; KvStreamCounters r; diff --git a/src/kernels/cuda/native_bf16.cu b/src/kernels/cuda/native_bf16.cu index 1390a6abec..fdbdc947c6 100644 --- a/src/kernels/cuda/native_bf16.cu +++ b/src/kernels/cuda/native_bf16.cu @@ -183,4 +183,17 @@ void bf16_gemv_fp32_mmvf(const float* x, const uint16_t* w, float* y, } + +void bf16_gemv_fp32_mmvf_cols(const float* x, const uint16_t* w, float* y, int64_t n_in, int64_t n_out, int ncols, + void* stream) { + // columns contiguous: the existing multi-row kernel (weight read once, each output bitwise its one-row call), + // up to 8 rows per launch + for (int c0 = 0; c0 < ncols; c0 += 8) { + const int nc = ncols - c0 < 8 ? ncols - c0 : 8; + if (nc == 1) bf16_gemv_fp32_mmvf(x + (size_t) c0 * n_in, w, y + (size_t) c0 * n_out, n_in, n_out, stream); + else bf16_gemv_fp32_mmvf_multi(x + (size_t) c0 * n_in, n_in, w, y + (size_t) c0 * n_out, n_out, n_in, n_out, nc, + stream); + } +} + } // namespace strata::kernels diff --git a/src/kernels/cuda/native_mmvq.cu b/src/kernels/cuda/native_mmvq.cu index e0a4c1bdb4..e0d11bf18c 100644 --- a/src/kernels/cuda/native_mmvq.cu +++ b/src/kernels/cuda/native_mmvq.cu @@ -25,12 +25,14 @@ #include "strata/kernels/native_mmvq.hpp" #include "strata/kernels/dp4a.hpp" +#include "strata/kernels/q8_1_finite.hpp" #include "strata/kernels/iq_kernels.hpp" #include #include #include +#include #include #include #include @@ -145,6 +147,22 @@ __global__ void native_quantize_q8_1_kernel(const float* __restrict__ x, const float xi = x[i]; const float amax = warp_max(fabsf(xi)); const float sum = warp_sum(xi); + const float d = q8_1_finite(amax / 127.0f); // #606: q8_1_finite.hpp - the same bits for every finite block + const int8_t q = q8_1_quant(xi, d, amax); + y[i / Q8K].qs[i % Q8K] = q; + if (i % Q8K == 0) y[i / Q8K].ds = q8_1_ds(d, sum); +} + +__launch_bounds__(QUANT_THREADS, 1) +__global__ void native_swiglu_quantize_q8_1_kernel(const float* __restrict__ gate, + const float* __restrict__ up, + Q81Block* __restrict__ y, int n_in) { + const int i = int(blockIdx.x) * QUANT_THREADS + int(threadIdx.x); + if (i >= n_in) return; // n_in is a multiple of 32: only whole warps return. + const float gi = gate[i]; + const float xi = __fmul_rn(__fdividef(gi, __fadd_rn(1.0f, __expf(-gi))), up[i]); + const float amax = warp_max(fabsf(xi)); + const float sum = warp_sum(xi); const float d = amax / 127.0f; const int8_t q = amax == 0.0f ? 0 : roundf(xi / d); y[i / Q8K].qs[i % Q8K] = q; @@ -417,6 +435,17 @@ __device__ __align__(4) int8_t iq4nl_values[16] = { }; __device__ __forceinline__ int2 iq4_table_lookup(int q4) { +#if defined(STRATA_HIP_GFX906) + // AMD: llama.cpp's HIP lookup (see iq_kernels.cu get_int_from_table_16): 4 v_perm_b32 per 8 values + const uint32_t* v32 = reinterpret_cast(iq4nl_values); + const uint32_t q_even = (uint32_t) q4, q_odd = (uint32_t) q4 >> 4; + const uint32_t el = __builtin_amdgcn_perm(v32[1], v32[0], q_even & 0x07070707u); + const uint32_t ol = __builtin_amdgcn_perm(v32[1], v32[0], q_odd & 0x07070707u); + const uint32_t eh = __builtin_amdgcn_perm(v32[3], v32[2], q_even & 0x07070707u); + const uint32_t oh = __builtin_amdgcn_perm(v32[3], v32[2], q_odd & 0x07070707u); + return make_int2((int) __builtin_amdgcn_perm(eh, el, 0x03020100u | ((q_even & 0x08080808u) >> 1)), + (int) __builtin_amdgcn_perm(oh, ol, 0x03020100u | ((q_odd & 0x08080808u) >> 1))); +#else const uint32_t* table32 = reinterpret_cast(iq4nl_values); uint32_t tmp[2]; const uint32_t low_high_selection_indices = 0x32103210 | ((q4 & 0x88888888) >> 1); @@ -428,6 +457,7 @@ __device__ __forceinline__ int2 iq4_table_lookup(int q4) { tmp[i] = __byte_perm(low, high, low_high_selection_indices >> shift); } return make_int2(__byte_perm(tmp[0], tmp[1], 0x6420), __byte_perm(tmp[0], tmp[1], 0x7531)); +#endif } // Exact pinned vec_dot_iq4_xs_q8_1: a lane consumes one 32-element subblock, @@ -997,7 +1027,7 @@ struct SmallTraits { bool g_multi_exact = true; // until the upstream layout is timed on an idle GPU (plan rule: default only what is measured) template -__launch_bounds__(NW * WARP, 1) +__launch_bounds__(NW * WARP, (ROWS <= 2 ? 4 : 1)) __global__ void native_mmvq_multi_kernel(const typename F::Block* __restrict__ w, const Q81Block* __restrict__ x, float* __restrict__ y, int n_in, int n_out) { @@ -1054,8 +1084,9 @@ void launch_multi_n(const void* weights, const void* x_q8_1, float* y, int n_in, } const dim3 threads(WARP, WARPS); if (n_in / F::DIV < F::BPI) { - const unsigned blocks = unsigned((std::size_t(n_out) + WARPS - 1) / WARPS); - native_mmvq_multi_kernel<<>>(w, x, y, n_in, n_out); + constexpr int ROWS = 2; + const unsigned blocks = unsigned((std::size_t(n_out) + ROWS - 1) / ROWS); + native_mmvq_multi_kernel<<>>(w, x, y, n_in, n_out); } else { native_mmvq_multi_kernel<<>>(w, x, y, n_in, n_out); } @@ -1098,6 +1129,77 @@ void launch_check() { } } + +#if defined(STRATA_HIP_GFX906) +// ---- AMD (wave64) layout: one wavefront per R rows, four wavefronts per block, the row's blocks strided over the +// 64 lanes exactly as the CUDA kernels stride them over a block (kbx = lane / T, stride 64 / T), and a 64-lane +// butterfly instead of the LDS partials and __syncthreads of the one-block-per-row layout: on gfx906 a block per +// 2 KB row spent most of its time being launched and joined. The SAME kernel serves every column count 1..8, so a +// column's sums do not depend on how many columns (verify tokens) ride along. STRATA_MMVQ_WAVE=0: the CUDA layout. +bool g_wave_off = std::getenv("STRATA_MMVQ_WAVE") && std::string(std::getenv("STRATA_MMVQ_WAVE")) == "0"; +template +__launch_bounds__(256) +__global__ void native_mmvq_wave_kernel(const typename F::Block* __restrict__ w, const Q81Block* __restrict__ x, + float* __restrict__ y, int n_in, int n_out) { + constexpr int BPIW = 64 / F::T; + static_assert(64 % F::T == 0, "a block's threads must tile the wavefront"); + const int lane = int(threadIdx.x) & 63; + const int row0 = (int(blockIdx.x) * 4 + (int(threadIdx.x) >> 6)) * R; + if (row0 >= n_out) return; + const int blocks_per_row = n_in / F::DIV; + const int x_stride = n_in / Q8K; + const int kqs = F::kqs(lane); + float tmp[NCOLS][R] = {}; + for (int kbx = lane / F::T; kbx < blocks_per_row; kbx += BPIW) { + const int kby = kbx * F::KBY; +#pragma unroll + for (int i = 0; i < R; ++i) { + if (row0 + i < n_out) { + const typename F::W wv = F::load(w + std::size_t(row0 + i) * blocks_per_row + kbx, kqs); +#pragma unroll + for (int j = 0; j < NCOLS; ++j) tmp[j][i] += F::apply(wv, x + std::size_t(j) * x_stride + kby, kqs); + } + } + } +#pragma unroll + for (int j = 0; j < NCOLS; ++j) +#pragma unroll + for (int i = 0; i < R; ++i) { + float v = tmp[j][i]; +#pragma unroll + for (int off = 32; off > 0; off >>= 1) v += __shfl_xor(v, off, 64); + if (lane == 0 && row0 + i < n_out) y[std::size_t(j) * n_out + row0 + i] = v; + } +} +template +void wave_launch_n(const void* weights, const void* x_q8_1, float* y, int n_in, int n_out, cudaStream_t s) { + const auto* w = static_cast(weights); + const auto* x = static_cast(x_q8_1); + constexpr int R = 1; + const unsigned blocks = unsigned((std::size_t(n_out) + 4 * R - 1) / (4 * R)); + native_mmvq_wave_kernel<<>>(w, x, y, n_in, n_out); +} +template +void wave_launch(const void* weights, const void* x_q8_1, float* y, int n_in, int n_out, int ncols, void* stream) { + const auto s = static_cast(stream); + switch (ncols) { + case 1: wave_launch_n(weights, x_q8_1, y, n_in, n_out, s); break; + case 2: wave_launch_n(weights, x_q8_1, y, n_in, n_out, s); break; + case 3: wave_launch_n(weights, x_q8_1, y, n_in, n_out, s); break; + case 4: wave_launch_n(weights, x_q8_1, y, n_in, n_out, s); break; + case 5: wave_launch_n(weights, x_q8_1, y, n_in, n_out, s); break; + case 6: wave_launch_n(weights, x_q8_1, y, n_in, n_out, s); break; + case 7: wave_launch_n(weights, x_q8_1, y, n_in, n_out, s); break; + case 8: wave_launch_n(weights, x_q8_1, y, n_in, n_out, s); break; + default: throw std::invalid_argument("native MMVQ (wave) requires 1 <= ncols <= 8"); + } +} +#define STRATA_WAVE_MMVQ(...) \ + if (!g_wave_off) { wave_launch<__VA_ARGS__>(weights, x_q8_1, y, n_in, n_out, ncols, stream); launch_check(); return; } +#else +#define STRATA_WAVE_MMVQ(...) +#endif + template void small_mmvq(const void* weights, const void* x_q8_1, float* y, int n_in, int n_out, int ncols, void* stream) { @@ -1107,6 +1209,7 @@ void small_mmvq(const void* weights, const void* x_q8_1, float* y, validate_pointer(x_q8_1); validate_pointer(y); validate_stream(stream); + STRATA_WAVE_MMVQ(SmallTraits) if (ncols > 1) { launch_multi>(weights, x_q8_1, y, n_in, n_out, ncols, stream); launch_check(); @@ -1163,6 +1266,21 @@ void native_quantize_q8_1(const float* x, void* x_q8_1, int n_in, int ncols, voi launch_check(); } +void native_swiglu_quantize_q8_1(const float* gate, const float* up, void* x_q8_1, + int n_in, int ncols, void* stream) { + validate_shape(n_in, ncols); + validate_pointer(gate); + validate_pointer(up); + validate_pointer(x_q8_1); + validate_stream(stream); + const int n_total = n_in * ncols; + const unsigned blocks = unsigned((std::size_t(n_total) + QUANT_THREADS - 1) / QUANT_THREADS); + native_swiglu_quantize_q8_1_kernel<<(stream)>>>( + gate, up, static_cast(x_q8_1), n_total); + launch_check(); +} + void native_q5_k_mmvq(const void* weights, const void* x_q8_1, float* y, int n_in, int n_out, int ncols, void* stream) { validate_shape(n_in, ncols, QK); @@ -1171,6 +1289,7 @@ void native_q5_k_mmvq(const void* weights, const void* x_q8_1, float* y, validate_pointer(x_q8_1); validate_pointer(y); validate_stream(stream); + STRATA_WAVE_MMVQ(Q5KTraits) if (ncols > 1) { launch_multi(weights, x_q8_1, y, n_in, n_out, ncols, stream); launch_check(); @@ -1211,6 +1330,7 @@ void native_q2_0_mmvq(const void* weights, const void* x_q8_1, float* y, validate_pointer(x_q8_1); validate_pointer(y); validate_stream(stream); + STRATA_WAVE_MMVQ(Q20Traits) if (ncols > 1) { launch_multi(weights, x_q8_1, y, n_in, n_out, ncols, stream); launch_check(); @@ -1250,6 +1370,7 @@ void native_q3_k_mmvq(const void* weights, const void* x_q8_1, float* y, validate_pointer(x_q8_1); validate_pointer(y); validate_stream(stream); + STRATA_WAVE_MMVQ(Q3KTraits) if (ncols > 1) { launch_multi(weights, x_q8_1, y, n_in, n_out, ncols, stream); launch_check(); @@ -1289,6 +1410,7 @@ void native_iq4_xs_mmvq(const void* weights, const void* x_q8_1, float* y, validate_pointer(x_q8_1); validate_pointer(y); validate_stream(stream); + STRATA_WAVE_MMVQ(IQ4XSTraits) if (ncols > 1) { launch_multi(weights, x_q8_1, y, n_in, n_out, ncols, stream); launch_check(); @@ -1328,6 +1450,7 @@ void native_q4_k_mmvq(const void* weights, const void* x_q8_1, float* y, validate_pointer(x_q8_1); validate_pointer(y); validate_stream(stream); + STRATA_WAVE_MMVQ(Q4KTraits) if (ncols > 1) { launch_multi(weights, x_q8_1, y, n_in, n_out, ncols, stream); launch_check(); @@ -1367,6 +1490,7 @@ void native_q6_k_mmvq(const void* weights, const void* x_q8_1, float* y, validate_pointer(x_q8_1); validate_pointer(y); validate_stream(stream); + STRATA_WAVE_MMVQ(Q6KTraits) if (ncols > 1) { launch_multi(weights, x_q8_1, y, n_in, n_out, ncols, stream); launch_check(); diff --git a/src/kernels/cuda/ple.cu b/src/kernels/cuda/ple.cu index 6585bbaab8..984512b0d6 100644 --- a/src/kernels/cuda/ple.cu +++ b/src/kernels/cuda/ple.cu @@ -197,6 +197,7 @@ void ck(cudaError_t e, const char* what) { } // namespace void ple_set_native_bf16(bool enabled) { native_bf16 = enabled; } +bool ple_native_bf16_enabled() { return native_bf16; } void ple_set_native_postops(bool enabled) { native_postops = enabled; } bool ple_native_postops_enabled() { return native_postops; } @@ -365,4 +366,21 @@ void ple_block(const float* emb, const float* hidden, const float* hist_rows, co // itself, and the engine's caller does not want that at all. } +void ple_block_projected(const float* projected_key, const float* projected_value, const float* hidden, + const float* hist_rows, const PleWeights& w, PleOut& out, void* scratch, void* stream) { + if (!projected_key || !projected_value || !hidden || !hist_rows || !out.result || !scratch) return; + const int n_embd = NG_N_EMBD, hc = NG_HC, hc_dim = NG_HC_DIM; + float* d_scratch = (float*) scratch; + float* d_key = d_scratch; + float* d_query = d_key + hc_dim; + float* d_norm = d_query + hc_dim; + float* d_gated = d_norm + hc_dim; + float* d_conv = d_gated + hc_dim; + float* d_value = d_conv + hc_dim; + float* d_gate = d_value + n_embd; + float* norm_dst = out.normalized ? out.normalized : d_norm; + NativePlePostopsBuffers buffers{d_query, norm_dst, d_gate, d_gated, norm_dst, d_conv, out.result}; + native_ple_postops(projected_key, hidden, projected_value, hist_rows, w, buffers, stream); +} + } // namespace strata::kernels diff --git a/src/kernels/cuda/qsa_decode_attn.cu b/src/kernels/cuda/qsa_decode_attn.cu index 4035999a95..f487c4f3e6 100644 --- a/src/kernels/cuda/qsa_decode_attn.cu +++ b/src/kernels/cuda/qsa_decode_attn.cu @@ -181,6 +181,217 @@ __global__ void __launch_bounds__(THREADS) attn_chunk_kernel(const float* __rest for (int h = 0; h < G; ++h) part_acc[((size_t) slot * G + h) * HD + t] = acc[h]; } +// PR #540 (sskver): the same attention with fewer shuffles and less shared-memory traffic, bit for bit the kernel +// above (same operands, same order); +7% prompt speed on a V100, where this kernel reads every prompt chunk. It +// uses more registers (80 vs 38 on sm_70), so it runs on cards below sm_75 only, and exists only in the experimental +// build (-DSTRATA_EXPERIMENTAL_SM60=ON): the ready-made engine keeps exactly the kernel above. +#if defined(STRATA_EXPERIMENTAL_SM60) +// The 12 heads' lane sums (`part[12..15]` zero), reduce-scattered with warp_sum's pairing order (xor 16, 8, 4, 2, 1): +// every output adds the same two operands at every level, so each sum is bit for bit warp_sum's (float add commutes), +// for 16 shuffles instead of 12 x 5. Lane l ends holding head head_of_lane(l); lanes l and l^1 agree. +__device__ __forceinline__ float reduce12(const float (&part)[16], int lane) { + float r8[8]; +#pragma unroll + for (int i = 0; i < 8; ++i) { + const bool hi = (lane & 16) != 0; + r8[i] = (hi ? part[i + 8] : part[i]) + __shfl_xor_sync(0xffffffffu, hi ? part[i] : part[i + 8], 16); + } + float r4[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) { + const bool hi = (lane & 8) != 0; + r4[i] = (hi ? r8[i + 4] : r8[i]) + __shfl_xor_sync(0xffffffffu, hi ? r8[i] : r8[i + 4], 8); + } + float r2[2]; +#pragma unroll + for (int i = 0; i < 2; ++i) { + const bool hi = (lane & 4) != 0; + r2[i] = (hi ? r4[i + 2] : r4[i]) + __shfl_xor_sync(0xffffffffu, hi ? r4[i] : r4[i + 2], 4); + } + const bool hi2 = (lane & 2) != 0; + const float r1 = (hi2 ? r2[1] : r2[0]) + __shfl_xor_sync(0xffffffffu, hi2 ? r2[0] : r2[1], 2); + return r1 + __shfl_xor_sync(0xffffffffu, r1, 1); +} +__device__ __forceinline__ int head_of_lane(int lane) { + return ((lane >> 4) & 1) * 8 + ((lane >> 3) & 1) * 4 + ((lane >> 2) & 1) * 2 + ((lane >> 1) & 1); +} + +// One V element of pool row `row`, dimension `d` (the value loop's own thread index), per KV format. +template +__device__ __forceinline__ float load_v1(const QsaAttnPools& p, long long row, int d) { + if constexpr (KV_MODE == 0) { + return __half2float(__ushort_as_half(p.v_pool[row * HD + d])); + } else if constexpr (KV_MODE == 1) { + const float sc = __half2float(__ushort_as_half(p.v_scale[row * (HD / KV_Q8_GROUP) + d / KV_Q8_GROUP])); + return (float) p.v_q[row * HD + d] * sc; + } else { // modes 2 and 3: V is rotated Q4_0 (kv_q4.hpp); the caller rotates the output back + constexpr int bytes_per_head = (HD / QK4_0) * sizeof(block_q4_0); + const int b = d / QK4_0; + const int rem = d % QK4_0; + const block_q4_0* blk = reinterpret_cast(p.v_q4 + row * bytes_per_head) + b; + const float dd = __half2float(__ushort_as_half(blk->d)); + const int j = rem < 16 ? rem : (rem - 16); + const uint8_t byte = blk->qs[j]; + const int nibble = (rem < 16) ? ((byte & 0x0F) - 8) : ((byte >> 4) - 8); + return (float) nibble * dd; + } +} + +template +__global__ void __launch_bounds__(THREADS) attn_chunk_kernel_pre75(const float* __restrict__ q, QsaAttnPools p, + const int32_t* __restrict__ ids, + const int32_t* __restrict__ step, int n_kv_heads, + int page_size, float scale, float* __restrict__ part_acc, + float* __restrict__ part_m, float* __restrict__ part_l, + int n_chunks, int cap = 0, long long scratch_stride = 0) { + // batched form: query blockIdx.z, with its own q row, selection, step and scratch + q += (size_t) blockIdx.z * (size_t) (n_kv_heads * G) * HD; + ids += (size_t) blockIdx.z * (size_t) cap; + step += (size_t) blockIdx.z * kStepCount; + part_acc += (size_t) blockIdx.z * (size_t) scratch_stride; + part_m += (size_t) blockIdx.z * (size_t) scratch_stride; + part_l += (size_t) blockIdx.z * (size_t) scratch_stride; + __shared__ __align__(16) float sq[G][HD]; // 12 KB: this KV head's query heads (dead once the scores are done) + __shared__ float sp[G][CHUNK]; // scores + __shared__ long long srow[CHUNK]; // pool row of each cell (page, kv head, slot) + // probabilities, cell-major: the value loop reads one cell's 12 as three float4. They are first written after the + // __syncthreads that ends the score phase, the last reader of sq, so they share its storage (64 x 12 of its 256 x 12 + // floats) and the block's shared memory stays what it was before the cell-major layout. + float (*const spt)[G] = reinterpret_cast(&sq[0][0]); + static_assert(CHUNK * G <= G * HD, "spt must fit in sq"); + const int n_ids = __ldg(step + kStepWidth); + const int chunk = blockIdx.x, kvh = blockIdx.y; + const int t = threadIdx.x, lane = t & 31, warp = t >> 5; + const int c0 = chunk * CHUNK; + const int n_here = min(CHUNK, n_ids - c0); + const int slot = kvh * n_chunks + chunk; + if (n_here <= 0) { + if (t < G) { part_m[slot * G + t] = -FLT_MAX; part_l[slot * G + t] = 0.0f; } + return; + } + for (int i = t; i < G * HD; i += THREADS) sq[i / HD][i % HD] = q[(size_t) (kvh * G) * HD + i]; + if (t < CHUNK) { + long long r = -1; + if (t < n_here) { + const int cell = ids[c0 + t]; + const long long page = (long long) p.page_table[cell / page_size]; + // a block the KV streaming could not make resident keeps page -1 (ctl[3]); its cells are masked + // (score -FLT_MAX, weight 0) instead of being read from before the pool. + if (page >= 0) r = (page * n_kv_heads + kvh) * page_size + (cell % page_size); + } + srow[t] = r; + } + __syncthreads(); + // scores: each warp takes cells warp, warp+8, ...; each lane holds 8 of the 256 dimensions. NC cells per step + // (warp + 8 * (i + j)): a lane's q slice comes out of shared memory once for all of them, which cuts the traffic + // that bounds this phase (24 x 16 B per lane per cell, against the FMAs) by NC; every dot product is the same + // expression as for a single cell, so the scores are unchanged. + constexpr int NC = 2; // 4 measured slower on a V100 (the registers cost occupancy) + const int hd = head_of_lane(lane); + for (int i = 0; i < CHUNK / WARPS; i += NC) { + int cc[NC]; + bool all_ok = true; +#pragma unroll + for (int j = 0; j < NC; ++j) { + cc[j] = warp + WARPS * (i + j); + all_ok = all_ok && cc[j] < n_here && srow[cc[j]] >= 0; + } + if (all_ok) { + float kk[NC][8]; +#pragma unroll + for (int j = 0; j < NC; ++j) load8(p, false, srow[cc[j]], lane * 8, kk[j]); + float pp[NC][16]; +#pragma unroll + for (int h = 0; h < G; ++h) { + const float4 qa = *reinterpret_cast(&sq[h][lane * 8]); + const float4 qb = *reinterpret_cast(&sq[h][lane * 8 + 4]); +#pragma unroll + for (int j = 0; j < NC; ++j) + pp[j][h] = kk[j][0] * qa.x + kk[j][1] * qa.y + kk[j][2] * qa.z + kk[j][3] * qa.w + + kk[j][4] * qb.x + kk[j][5] * qb.y + kk[j][6] * qb.z + kk[j][7] * qb.w; + } +#pragma unroll + for (int j = 0; j < NC; ++j) { +#pragma unroll + for (int h = G; h < 16; ++h) pp[j][h] = 0.0f; + const float sj = reduce12(pp[j], lane); + if ((lane & 1) == 0 && hd < G) sp[hd][cc[j]] = sj * scale; + } + } else { + for (int k = 0; k < NC; ++k) { // a masked or out-of-range cell in the group: one cell at a time + const int c = cc[k]; + if (c >= n_here || srow[c] < 0) { + if (lane < G) sp[lane][c] = -FLT_MAX; + continue; + } + float k8[8]; + load8(p, false, srow[c], lane * 8, k8); + float part[16]; +#pragma unroll + for (int h = 0; h < G; ++h) { + const float4 qa = *reinterpret_cast(&sq[h][lane * 8]); + const float4 qb = *reinterpret_cast(&sq[h][lane * 8 + 4]); + part[h] = k8[0] * qa.x + k8[1] * qa.y + k8[2] * qa.z + k8[3] * qa.w + + k8[4] * qb.x + k8[5] * qb.y + k8[6] * qb.z + k8[7] * qb.w; + } +#pragma unroll + for (int h = G; h < 16; ++h) part[h] = 0.0f; + const float s = reduce12(part, lane); + if ((lane & 1) == 0 && hd < G) sp[hd][c] = s * scale; + } + } + } + __syncthreads(); + // per-head chunk max and exp-sum: warp w handles heads w and w+8. + for (int h = warp; h < G; h += WARPS) { + const float a = sp[h][lane], b = sp[h][lane + 32]; + const float m = warp_max(fmaxf(a, b)); + const float ea = (lane < n_here && srow[lane] >= 0) ? __expf(a - m) : 0.0f; + const float eb = (lane + 32 < n_here && srow[lane + 32] >= 0) ? __expf(b - m) : 0.0f; + spt[lane][h] = ea; + spt[lane + 32][h] = eb; + const float l = warp_sum(ea + eb); + if (lane == 0) { part_m[slot * G + h] = m; part_l[slot * G + h] = l; } + } + __syncthreads(); + // values: thread t owns dimension t for all 12 heads. + float acc[G]; +#pragma unroll + for (int h = 0; h < G; ++h) acc[h] = 0.0f; + // One cell's 12 weights (three broadcast float4 loads) into the 12 accumulators; the cells are folded in + // ascending order, so the sums are the plain loop's, bit for bit. + const auto fold = [&](int c, float v) { + const float4* w4 = reinterpret_cast(spt[c]); + const float4 w0 = w4[0], w1 = w4[1], w2 = w4[2]; + acc[0] = fmaf(w0.x, v, acc[0]); acc[1] = fmaf(w0.y, v, acc[1]); + acc[2] = fmaf(w0.z, v, acc[2]); acc[3] = fmaf(w0.w, v, acc[3]); + acc[4] = fmaf(w1.x, v, acc[4]); acc[5] = fmaf(w1.y, v, acc[5]); + acc[6] = fmaf(w1.z, v, acc[6]); acc[7] = fmaf(w1.w, v, acc[7]); + acc[8] = fmaf(w2.x, v, acc[8]); acc[9] = fmaf(w2.y, v, acc[9]); + acc[10] = fmaf(w2.z, v, acc[10]); acc[11] = fmaf(w2.w, v, acc[11]); + }; + int c = 0; + // four cells at a time with their V loads issued together (the loop was one dependent load chain per cell); + // a group holding a masked cell (page -1, rare) leaves this loop and the scalar one below finishes the chunk. + for (; c + 4 <= n_here; c += 4) { + const long long r0 = srow[c], r1 = srow[c + 1], r2 = srow[c + 2], r3 = srow[c + 3]; + if ((r0 | r1 | r2 | r3) < 0) break; + const float v0 = load_v1(p, r0, t), v1 = load_v1(p, r1, t); + const float v2 = load_v1(p, r2, t), v3 = load_v1(p, r3, t); + fold(c, v0); + fold(c + 1, v1); + fold(c + 2, v2); + fold(c + 3, v3); + } + for (; c < n_here; ++c) { + if (srow[c] < 0) continue; // masked above, weight 0 + fold(c, load_v1(p, srow[c], t)); + } +#pragma unroll + for (int h = 0; h < G; ++h) part_acc[((size_t) slot * G + h) * HD + t] = acc[h]; +} +#endif // STRATA_EXPERIMENTAL_SM60 + __global__ void __launch_bounds__(HD) attn_merge_kernel(const float* __restrict__ part_acc, const float* __restrict__ part_m, const float* __restrict__ part_l, int n_chunks, @@ -206,6 +417,34 @@ __global__ void __launch_bounds__(HD) attn_merge_kernel(const float* __restrict_ attn[(size_t) h * HD + d] = L > 0.0f ? acc / L : 0.0f; } +#if defined(STRATA_EXPERIMENTAL_SM60) +// the current device is below sm_75 (per device: a layer split can mix cards); STRATA_ATTN_PRE75=0 turns PR #540's +// kernel off (A/B) +bool pre75_attn() { + static const bool off = [] { + const char* e = std::getenv("STRATA_ATTN_PRE75"); + return e != nullptr && e[0] == '0'; + }(); + static int cc[64] = {}; + int dev = 0; + if (off) return false; + if (cudaGetDevice(&dev) != cudaSuccess || dev < 0 || dev >= 64) { cudaGetLastError(); return false; } + if (cc[dev] == 0) { + int major = 0, minor = 0; + if (cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev) != cudaSuccess || + cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, dev) != cudaSuccess) { + cudaGetLastError(); + return false; + } + cc[dev] = 10 * major + minor; + } + return cc[dev] < 75; +} +#define STRATA_ATTN_CHUNK(M) (pre75_attn() ? attn_chunk_kernel_pre75 : attn_chunk_kernel) +#else +#define STRATA_ATTN_CHUNK(M) attn_chunk_kernel +#endif + } // namespace void qsa_decode_attn_batch(const float* q, const QsaAttnPools& pools, const int32_t* ids, const int32_t* steps, @@ -228,16 +467,16 @@ void qsa_decode_attn_batch(const float* q, const QsaAttnPools& pools, const int3 const dim3 grid((unsigned) n_chunks, (unsigned) s.n_head_kv, (unsigned) n_q); cudaStream_t st = (cudaStream_t) stream; if (kv_mode == 3) - attn_chunk_kernel<3><<>>(q, pools, ids, steps, (int) s.n_head_kv, (int) s.page_size, + STRATA_ATTN_CHUNK(3)<<>>(q, pools, ids, steps, (int) s.n_head_kv, (int) s.page_size, scale, part_acc, part_m, part_l, n_chunks, (int) cap, stride); else if (kv_mode == 2) - attn_chunk_kernel<2><<>>(q, pools, ids, steps, (int) s.n_head_kv, (int) s.page_size, + STRATA_ATTN_CHUNK(2)<<>>(q, pools, ids, steps, (int) s.n_head_kv, (int) s.page_size, scale, part_acc, part_m, part_l, n_chunks, (int) cap, stride); else if (kv_mode == 1) - attn_chunk_kernel<1><<>>(q, pools, ids, steps, (int) s.n_head_kv, (int) s.page_size, + STRATA_ATTN_CHUNK(1)<<>>(q, pools, ids, steps, (int) s.n_head_kv, (int) s.page_size, scale, part_acc, part_m, part_l, n_chunks, (int) cap, stride); else - attn_chunk_kernel<0><<>>(q, pools, ids, steps, (int) s.n_head_kv, (int) s.page_size, + STRATA_ATTN_CHUNK(0)<<>>(q, pools, ids, steps, (int) s.n_head_kv, (int) s.page_size, scale, part_acc, part_m, part_l, n_chunks, (int) cap, stride); attn_merge_kernel<<>>(part_acc, part_m, part_l, n_chunks, attn, stride); @@ -276,17 +515,17 @@ void qsa_decode_attn_step(const float* q, const QsaAttnPools& pools, const int32 const dim3 grid((unsigned) n_chunks, (unsigned) s.n_head_kv); cudaStream_t st = (cudaStream_t) stream; if (kv_mode == 3) - attn_chunk_kernel<3><<>>(q, pools, ids, step, (int) s.n_head_kv, (int) s.page_size, - scale, part_acc, part_m, part_l, n_chunks); + STRATA_ATTN_CHUNK(3)<<>>(q, pools, ids, step, (int) s.n_head_kv, (int) s.page_size, + scale, part_acc, part_m, part_l, n_chunks, 0, 0); else if (kv_mode == 2) - attn_chunk_kernel<2><<>>(q, pools, ids, step, (int) s.n_head_kv, (int) s.page_size, - scale, part_acc, part_m, part_l, n_chunks); + STRATA_ATTN_CHUNK(2)<<>>(q, pools, ids, step, (int) s.n_head_kv, (int) s.page_size, + scale, part_acc, part_m, part_l, n_chunks, 0, 0); else if (kv_mode == 1) - attn_chunk_kernel<1><<>>(q, pools, ids, step, (int) s.n_head_kv, (int) s.page_size, - scale, part_acc, part_m, part_l, n_chunks); + STRATA_ATTN_CHUNK(1)<<>>(q, pools, ids, step, (int) s.n_head_kv, (int) s.page_size, + scale, part_acc, part_m, part_l, n_chunks, 0, 0); else - attn_chunk_kernel<0><<>>(q, pools, ids, step, (int) s.n_head_kv, (int) s.page_size, - scale, part_acc, part_m, part_l, n_chunks); + STRATA_ATTN_CHUNK(0)<<>>(q, pools, ids, step, (int) s.n_head_kv, (int) s.page_size, + scale, part_acc, part_m, part_l, n_chunks, 0, 0); attn_merge_kernel<<<(unsigned) s.n_head, HD, 0, st>>>(part_acc, part_m, part_l, n_chunks, attn); const cudaError_t e = cudaGetLastError(); if (e != cudaSuccess) { diff --git a/src/kernels/cuda/qsa_prompt_attn.cu b/src/kernels/cuda/qsa_prompt_attn.cu index 6148566301..9b481929c5 100644 --- a/src/kernels/cuda/qsa_prompt_attn.cu +++ b/src/kernels/cuda/qsa_prompt_attn.cu @@ -736,6 +736,391 @@ bool launch(const float* q, const QsaAttnPools& pools, const int32_t* ids, const return true; } + +// PR #600: compiled only into the experimental build (-DSTRATA_EXPERIMENTAL_SM60=ON), the one build that runs on a +// Volta card; the ready-made engine has none of it (no extra kernels to load, the same code as before). +#if !defined(__HIPCC__) && defined(STRATA_EXPERIMENTAL_SM60) +// ---- sm_70 (Volta): v1's structure on mma.m8n8k4 ------------------------------------------------------------------ +// Volta's tensor cores only have m8n8k4 (FP16 in, FP32 accumulate): per warp FOUR independent 8x8x4 products, one per +// quad-pair (QP q = lanes 4q..4q+3 and 4q+16..4q+19; thread j = (lane & 3) + 4 * (lane >> 4) within it). Layout, measured on +// a V100 (tools-free microtest, and equal to turbomind's SM70_MMA_884): the thread holds row j of A (k 0..3 as two +// registers) and column j of B (k 0..3); C register i is row (lane & 1) + 4 * (lane >> 4) + 2 * ((i >> 1) & 1), +// column (lane & 2) + 4 * (i >> 2) + (i & 1) of its QP's 8x8 tile. +// q.k: warp w owns dims [64w, 64w+64) = int8 scale group w; QP q takes cells 8q..8q+7 of the 32-cell chunk; 16 k-steps +// of 4 dims, two 8-row blocks (12 heads + 4 zero rows), q as hi + lo halves. The warp's partial (times the +// cells' K scale) goes to S.part[w]; the softmax adds the four groups in a fixed order (deterministic). +// p.v: warp w owns dims [64w, 64w+64) again; QP q takes the 16 dims 64w+16q.. (two n-tiles of 8); k = the 32 cells in +// steps of 4; p carries the V scale (as v1) and is split into hi + lo halves. +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 700 && __CUDA_ARCH__ < 750 +#define STRATA_PA_VOLTA 1 +#else +#define STRATA_PA_VOLTA 0 +#endif + +__device__ __forceinline__ void mma884(float* c, uint32_t a0, uint32_t a1, uint32_t b0, uint32_t b1) { +#if STRATA_PA_VOLTA + asm volatile("mma.sync.aligned.m8n8k4.row.col.f32.f16.f16.f32 {%0,%1,%2,%3,%4,%5,%6,%7}, {%8,%9}, {%10,%11}, " + "{%0,%1,%2,%3,%4,%5,%6,%7};\n" + : "+f"(c[0]), "+f"(c[1]), "+f"(c[2]), "+f"(c[3]), "+f"(c[4]), "+f"(c[5]), "+f"(c[6]), "+f"(c[7]) + : "r"(a0), "r"(a1), "r"(b0), "r"(b1)); +#else + __trap(); // m8n8k4 is a Volta instruction; the host only launches this kernel on sm_70 +#endif +} + +template +struct Smem70 { + using KElem = typename std::conditional::type; + using VElem = typename std::conditional::type; + static constexpr int KROW = KV_MODE == 0 ? HD + 8 : HD + 16; + static constexpr int VROW = KV_MODE == 1 ? HD + 16 : HD + 8; + __half qh[16][QS]; + __half ql[16][QS]; + KElem k[CH][KROW]; + VElem v[CH][VROW]; + float ks[CH][4]; + float vs[CH][4]; + float part[4][16][CH + 4]; // q.k per dim group (scaled by the cell's K scale) + float s[16][CH + 4]; // the chunk's probabilities + float qmax[THREADS / 32]; + float alpha[16]; + float lsum[16]; + float mrow[16]; + long long row[CH]; +}; + +template +__global__ void __launch_bounds__(THREADS) prompt_attn_v70_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) { + extern __shared__ __align__(16) unsigned char smem_raw[]; + Smem70& S = *reinterpret_cast*>(smem_raw); + 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 qp = (lane >> 2) & 3, jj = (lane & 3) + 4 * (lane >> 4); // quad-pair, thread within it + const int crow = (lane & 1) + 4 * (lane >> 4); // C row of register 0 (+2 per register pair) + const int ccol = lane & 2; // C column of register 0 (+4 per half, +1 per reg) + + // q: 12 heads + 4 zero rows, scaled by a power of two that puts its largest value near 2^14 (exact, and the + // lo halves stay out of FP16's subnormal range), then split into hi + lo halves + 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; + __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); // qm < 2^qe + const float qup = ldexpf(1.0f, 14 - qe), qdown = ldexpf(scale_log2, qe - 14); + for (int i = t; i < 16 * HD; i += THREADS) { + const int h = i / HD, d = i % HD; + const float x = h < G ? q[(size_t) h * HD + d] * qup : 0.0f; + const __half hi = __float2half_rn(x); + S.qh[h][d] = hi; + S.ql[h][d] = __float2half_rn(x - __half2float(hi)); + } + if (t < 16) { S.mrow[t] = -INFINITY; S.lsum[t] = 0.0f; } + + float acc[2][2][8]; +#pragma unroll + for (int rb = 0; rb < 2; ++rb) +#pragma unroll + for (int nt = 0; nt < 2; ++nt) +#pragma unroll + for (int i = 0; i < 8; ++i) acc[rb][nt][i] = 0.0f; + + for (int c0 = 0; c0 < n; c0 += CH) { + const int nh = min(CH, n - c0); + if (t < CH) { + long long r = -1; + if (t < nh) { + const int cell = ids[c0 + t]; + const long long page = (long long) p.page_table[cell / page_size]; + r = (page * n_kv_heads + kvh) * page_size + (cell % page_size); + } + S.row[t] = r; + } + __syncthreads(); // rows ready; the previous chunk's p.v is done with k, v, s + // gather the chunk's K and V rows (16-byte pieces; K8V4's V as q4_0 blocks dequantized to fp16) + // and their scales + { + constexpr int KPIECES = HD * (int) sizeof(typename Smem::KElem) / 16; // per K row + for (int i = t; i < CH * KPIECES; i += THREADS) { + const int c = i / KPIECES, pc = i % KPIECES; + const long long r = S.row[c]; + uint4 kx = make_uint4(0, 0, 0, 0); + if (r >= 0) { + if constexpr (KV_MODE == 0) + kx = __ldg(reinterpret_cast(p.k_pool + r * HD) + pc); + else // modes 1 and 3: the K side is INT8 + kx = __ldg(reinterpret_cast(p.k_q + r * HD) + pc); + } + *reinterpret_cast(reinterpret_cast(&S.k[c][0]) + pc * 16) = kx; + } + if constexpr (KV_MODE == 3) { // V: dequantize the row's q4_0 blocks straight into the fp16 V row + constexpr int BLKS = HD / QK4_0; + constexpr int BYTES = BLKS * (int) sizeof(block_q4_0); + for (int i = t; i < CH * BLKS; i += THREADS) { + const int c = i / BLKS, b = i % BLKS; + const long long r = S.row[c]; +#pragma unroll + for (int j = 0; j < QK4_0; ++j) S.v[c][b * QK4_0 + j] = __half(0); + if (r >= 0) { + const block_q4_0* blk = reinterpret_cast(p.v_q4 + r * BYTES) + b; + const float d = __half2float(__ushort_as_half(blk->d)); +#pragma unroll + for (int j = 0; j < QK4_0 / 2; ++j) { + S.v[c][b * QK4_0 + j] = __float2half_rn((float) ((int)(blk->qs[j] & 0x0F) - 8) * d); + S.v[c][b * QK4_0 + j + QK4_0 / 2] = + __float2half_rn((float) ((int)(blk->qs[j] >> 4) - 8) * d); + } + } + } + } else { + constexpr int VPIECES = HD * (int) sizeof(typename Smem::VElem) / 16; // per V row + for (int i = t; i < CH * VPIECES; i += THREADS) { + const int c = i / VPIECES, pc = i % VPIECES; + const long long r = S.row[c]; + uint4 vx = make_uint4(0, 0, 0, 0); + if (r >= 0) { + if constexpr (KV_MODE == 1) + vx = __ldg(reinterpret_cast(p.v_q + r * HD) + pc); + else + vx = __ldg(reinterpret_cast(p.v_pool + r * HD) + pc); + } + *reinterpret_cast(reinterpret_cast(&S.v[c][0]) + pc * 16) = vx; + } + } + for (int i = t; i < CH * 4; i += THREADS) { + const int c = i / 4, g = i % 4; + const long long r = S.row[c]; + float a = 0.0f, b = 0.0f; + if (r >= 0) { + if constexpr (KV_MODE == 1) { + a = __half2float(__ushort_as_half(p.k_scale[r * (HD / KV_Q8_GROUP) + g])); + b = __half2float(__ushort_as_half(p.v_scale[r * (HD / KV_Q8_GROUP) + g])); + } else if constexpr (KV_MODE == 3) { // K as int8, V dequantized to fp16 (scale 1) + a = __half2float(__ushort_as_half(p.k_scale[r * (HD / KV_Q8_GROUP) + g])); + b = 1.0f; + } else { + a = b = 1.0f; + } + } + S.ks[c][g] = a; + S.vs[c][g] = b; + } + } + __syncthreads(); + // scores: warp w = dim group w (64 dims), QP q = cells 8q..8q+7 + { + float tg[2][8]; +#pragma unroll + for (int rb = 0; rb < 2; ++rb) +#pragma unroll + for (int i = 0; i < 8; ++i) tg[rb][i] = 0.0f; + const int cell_b = qp * 8 + jj; +#pragma unroll + for (int ks = 0; ks < 16; ++ks) { + const int d0 = warp * 64 + ks * 4; + uint32_t b0, b1; + if constexpr (KV_MODE != 0) { // modes 1 and 3: the K side is INT8 codes, exact in FP16 + const uint32_t raw = *reinterpret_cast(&S.k[cell_b][d0]); + b0 = i8x2_to_h2(raw & 0xffffu); + b1 = i8x2_to_h2(raw >> 16); + } else { + const uint2 raw = *reinterpret_cast(&S.k[cell_b][d0]); + b0 = raw.x; + b1 = raw.y; + } +#pragma unroll + for (int rb = 0; rb < 2; ++rb) { + const uint2 ah = *reinterpret_cast(&S.qh[rb * 8 + jj][d0]); + const uint2 al = *reinterpret_cast(&S.ql[rb * 8 + jj][d0]); + mma884(tg[rb], ah.x, ah.y, b0, b1); + mma884(tg[rb], al.x, al.y, b0, b1); + } + } +#pragma unroll + for (int rb = 0; rb < 2; ++rb) +#pragma unroll + for (int i = 0; i < 8; ++i) { + const int row = rb * 8 + crow + 2 * ((i >> 1) & 1); + const int cell = qp * 8 + ccol + 4 * (i >> 2) + (i & 1); + S.part[warp][row][cell] = tg[rb][i] * S.ks[cell][warp]; + } + } + __syncthreads(); + // online softmax: row t/8, 4 cells per thread, 8 threads per row (lanes 8r..8r+7 of a warp) + { + constexpr int PER = CH / 8; + const int r = t >> 3, sub = t & 7; + float x[PER], mx = -INFINITY; +#pragma unroll + for (int j = 0; j < PER; ++j) { + const int c = sub * PER + j; + const float sum4 = ((S.part[0][r][c] + S.part[1][r][c]) + S.part[2][r][c]) + S.part[3][r][c]; + x[j] = c < nh ? sum4 * 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 < PER; ++j) { + const float e = x[j] == -INFINITY ? 0.0f : exp2f(x[j] - m_new); + S.s[r][sub * PER + j] = e; + sum += e; + } +#pragma unroll + for (int o = 1; o < 8; o <<= 1) sum += __shfl_xor_sync(0xffffffffu, sum, o); + __syncwarp(); + 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(); + __syncthreads(); + // p.v: warp w owns dims [64w, 64w+64) (int8 scale group w), QP q its 16 dims 64w+16q..; the V scale is folded + // into p relative to the chunk's largest, times 2^14 (as v1) + { + float vmax = 0.0f; +#pragma unroll + for (int c = lane; c < CH; c += 32) vmax = fmaxf(vmax, S.vs[c][warp]); +#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); + float tmp[2][2][8]; +#pragma unroll + for (int rb = 0; rb < 2; ++rb) +#pragma unroll + for (int nt = 0; nt < 2; ++nt) +#pragma unroll + for (int i = 0; i < 8; ++i) tmp[rb][nt][i] = 0.0f; +#pragma unroll + for (int ks = 0; ks < CH / 4; ++ks) { + const int c0 = ks * 4; + float w[4]; +#pragma unroll + for (int e = 0; e < 4; ++e) w[e] = S.vs[c0 + e][warp] * vup; + uint2 ah[2], al[2]; +#pragma unroll + for (int rb = 0; rb < 2; ++rb) { + const float4 pv = *reinterpret_cast(&S.s[rb * 8 + jj][c0]); + const float p0 = pv.x * w[0], p1 = pv.y * w[1], p2 = pv.z * w[2], p3 = pv.w * w[3]; + ah[rb].x = pack_h2(p0, p1); + ah[rb].y = pack_h2(p2, p3); + const float2 f01 = __half22float2(*reinterpret_cast(&ah[rb].x)); + const float2 f23 = __half22float2(*reinterpret_cast(&ah[rb].y)); + al[rb].x = pack_h2(p0 - f01.x, p1 - f01.y); + al[rb].y = pack_h2(p2 - f23.x, p3 - f23.y); + } +#pragma unroll + for (int nt = 0; nt < 2; ++nt) { + const int d = warp * 64 + qp * 16 + nt * 8 + jj; + uint32_t b0, b1; + if constexpr (KV_MODE == 1) { + const uint32_t x0 = (uint8_t) S.v[c0][d] | ((uint32_t) (uint8_t) S.v[c0 + 1][d] << 8); + const uint32_t x1 = (uint8_t) S.v[c0 + 2][d] | ((uint32_t) (uint8_t) S.v[c0 + 3][d] << 8); + b0 = i8x2_to_h2(x0); + b1 = i8x2_to_h2(x1); + } else { + const __half2 h0 = __halves2half2(S.v[c0][d], S.v[c0 + 1][d]); + const __half2 h1 = __halves2half2(S.v[c0 + 2][d], S.v[c0 + 3][d]); + b0 = *reinterpret_cast(&h0); + b1 = *reinterpret_cast(&h1); + } +#pragma unroll + for (int rb = 0; rb < 2; ++rb) { + mma884(tmp[rb][nt], ah[rb].x, ah[rb].y, b0, b1); + mma884(tmp[rb][nt], al[rb].x, al[rb].y, b0, b1); + } + } + } +#pragma unroll + for (int rb = 0; rb < 2; ++rb) { + float a_[2]; + a_[0] = S.alpha[rb * 8 + crow]; + a_[1] = S.alpha[rb * 8 + crow + 2]; +#pragma unroll + for (int nt = 0; nt < 2; ++nt) +#pragma unroll + for (int i = 0; i < 8; ++i) + acc[rb][nt][i] = fmaf(acc[rb][nt][i], a_[(i >> 1) & 1], tmp[rb][nt][i] * vdown); + } + } + } + __syncthreads(); +#pragma unroll + for (int rb = 0; rb < 2; ++rb) +#pragma unroll + for (int h = 0; h < 2; ++h) { + const int row = rb * 8 + crow + 2 * h; + if (row >= G) continue; + const float l = S.lsum[row]; + const float inv = l > 0.0f ? 1.0f / l : 0.0f; +#pragma unroll + for (int nt = 0; nt < 2; ++nt) +#pragma unroll + for (int hh = 0; hh < 2; ++hh) { + const int i = hh * 4 + h * 2; // registers i, i+1: row h, columns ccol + 4 * hh + {0, 1} + const int d = warp * 64 + qp * 16 + nt * 8 + ccol + 4 * hh; + *reinterpret_cast(attn + (size_t) row * HD + d) = + make_float2(acc[rb][nt][i] * inv, acc[rb][nt][i + 1] * inv); + } + } +} + +template +bool launch70(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) { + static bool attr[64] = {}; + int dev = 0; + cudaGetDevice(&dev); + const int bytes = (int) sizeof(Smem70); + if (dev < 0 || dev >= 64) return false; + if (!attr[dev]) { + if (cudaFuncSetAttribute(prompt_attn_v70_kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, bytes) != + cudaSuccess) { + cudaGetLastError(); + return false; + } + attr[dev] = true; + } + 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_v70_kernel<<>>( + 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 (sm_70): %s\n", cudaGetErrorString(e)); + std::exit(1); + } + return true; +} +#else // AMD, or a build without STRATA_EXPERIMENTAL_SM60: no m8n8k4 kernel (the caller keeps the old one) +template +bool launch70(const float*, const QsaAttnPools&, const int32_t*, const int32_t*, int64_t, const QsaShapes&, float*, + int64_t, cudaStream_t) { + return false; +} +#endif // !__HIPCC__ && STRATA_EXPERIMENTAL_SM60 + #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 @@ -1022,11 +1407,14 @@ bool launch_wmma(const float* q, const QsaAttnPools& pools, const int32_t* ids, bool qsa_prompt_attn_batch(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, void* stream) { if (n_q <= 0) return true; - bool turing = false; // per call, from the CURRENT device (a layer split can mix Turing with newer cards) + bool volta = false, turing = false; // per call, from the CURRENT device (a layer split can mix Turing with newer cards) { // 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. // #371: the compute capability with its minor - sm_70 (V100) has no m16n8k8 (the kernels trap below sm_75) +#if defined(STRATA_HIP_GFX906) + return false; // gfx906: the tensor-core and WMMA kernels are not for it; the FP32 kernel runs +#endif static int cc[64] = {}; int dev = 0; if (cudaGetDevice(&dev) != cudaSuccess || dev < 0 || dev >= 64) { cudaGetLastError(); return false; } @@ -1042,13 +1430,14 @@ bool qsa_prompt_attn_batch(const float* q, const QsaAttnPools& pools, const int3 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[dev] < 75) return false; + if (cc[dev] < 70) return false; + volta = cc[dev] < 75; // sm_70: the m8n8k4 kernel (STRATA_PA_VOLTA); STRATA_QSA_WARP=1 still forces the old one turing = cc[dev] < 80; } #if defined(__HIPCC__) // 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; + (void) turing; (void) volta; 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()) @@ -1069,6 +1458,7 @@ bool qsa_prompt_attn_batch(const float* q, const QsaAttnPools& pools, const int3 } if (pools.k_q != nullptr && pools.v_q4 != nullptr) { // hybrid K8V4: int8 K + dequantized-q4 V if (!pools.k_scale) return false; + if (volta) return launch70<3>(q, pools, ids, steps, cap, s, attn, n_q, st); return launch<3>(q, pools, ids, steps, cap, s, attn, n_q, st); } if (pools.k_q != nullptr) { @@ -1077,10 +1467,12 @@ bool qsa_prompt_attn_batch(const float* q, const QsaAttnPools& pools, const int3 // for how far the model amplifies an FP32-level change. Turing always takes it: v2's cp.async does not // exist before sm_80. static const bool v1 = std::getenv("STRATA_PROMPT_ATTN_V1") != nullptr; + if (volta) return launch70<1>(q, pools, ids, steps, cap, s, attn, n_q, st); if (v1 || turing) return launch<1>(q, pools, ids, steps, cap, s, attn, n_q, st); return launch_i8(q, pools, ids, steps, cap, s, attn, n_q, st); } if (!pools.k_pool || !pools.v_pool) return false; + if (volta) return launch70<0>(q, pools, ids, steps, cap, s, attn, n_q, st); return launch<0>(q, pools, ids, steps, cap, s, attn, n_q, st); } diff --git a/src/kernels/cuda/qsa_select.cu b/src/kernels/cuda/qsa_select.cu index ad705b1dff..56222c5190 100644 --- a/src/kernels/cuda/qsa_select.cu +++ b/src/kernels/cuda/qsa_select.cu @@ -596,6 +596,125 @@ __global__ void __launch_bounds__(TK_T) block_topk_reg_kernel(const float* __res } } +#if !defined(__HIPCC__) +// ---- a query with more blocks than the register kernel holds (contexts past 4 * 1024 * TK_PER cells): the register +// kernel's threads, per-warp histograms and warp scans, with each key read from memory again on every pass. A +// histogram has no order, so on the four radix passes thread t reads blocks t, t + 1024, ...: a warp reads 32 +// neighbours at a time. The cells are emitted ascending in the order (warp, row, lane): warp w holds the `per` rows +// of 32 consecutive blocks from block w * 32 * per. block_topk_kernel's selection rule (radix threshold, ties to the +// lowest index): identical ids. +__global__ void __launch_bounds__(TK_T) block_topk_wide_kernel(const float* __restrict__ scores, + const int32_t* __restrict__ steps, int64_t max_blocks, + int64_t cap, int32_t* __restrict__ ids) { + __shared__ int hist[TK_T / 32][256]; + __shared__ int s_gt[TK_T / 32], s_eq[TK_T / 32]; + __shared__ int s_digit, s_above; + const int64_t qi = blockIdx.x; + 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; + const int t = threadIdx.x, lane = t & 31, warp = t >> 5; + if (n_kv <= width) { + for (int64_t j = t; j < n_kv; j += TK_T) out[j] = (int32_t) j; + return; + } + const float* sc = scores + qi * max_blocks; + const int64_t nb = n_bid + 1; + const int64_t per = (nb + TK_T - 1) / TK_T; // rows of 32 blocks per warp + auto weight = [&](int64_t b) -> int { return b < n_bid ? R : (int) (n_kv - n_bid * R); }; + uint32_t prefix = 0; + int above = 0; + for (int shift = 24; shift >= 0; shift -= 8) { + for (int i = lane; i < 256; i += 32) hist[warp][i] = 0; + __syncwarp(); + const uint32_t hi_mask = shift == 24 ? 0u : (0xffffffffu << (shift + 8)); + for (int64_t b = t; b < nb; b += TK_T) { + const int w = weight(b); + if (w == 0) continue; + const uint32_t k = order_key(sc[b]); + if ((k & hi_mask) == (prefix & hi_mask)) atomicAdd(&hist[warp][(k >> shift) & 255], w); + } + __syncthreads(); + if (t < 256) { // fold the warps' histograms into warp 0's + int sum = 0; + for (int w2 = 0; w2 < TK_T / 32; ++w2) sum += hist[w2][t]; + hist[0][t] = sum; + } + __syncthreads(); + if (t == 0) { + int cum = above, d = 255; + for (; d > 0; --d) { + if (cum + hist[0][d] >= width) break; + cum += hist[0][d]; + } + s_digit = d; + s_above = cum; + } + __syncthreads(); + prefix |= (uint32_t) s_digit << shift; + above = s_above; + __syncthreads(); + } + const uint32_t thr = prefix; + const int eq_budget = (int) (width - above); // cells equal to thr that fit, lowest index first + const int64_t w0 = (int64_t) warp * 32 * per; + int gt = 0, eq = 0; + for (int64_t b = w0 + lane; b < nb && b < w0 + 32 * per; b += 32) { + const int w = weight(b); + if (w == 0) continue; + const uint32_t k = order_key(sc[b]); + if (k > thr) gt += w; + else if (k == thr) eq += w; + } +#pragma unroll + for (int o = 16; o > 0; o >>= 1) { + gt += __shfl_xor_sync(0xffffffffu, gt, o); + eq += __shfl_xor_sync(0xffffffffu, eq, o); + } + if (lane == 0) { s_gt[warp] = gt; s_eq[warp] = eq; } + __syncthreads(); + if (t == 0) { // per warp: the selected cells and the tied cells before it + int eb = 0, sb = 0; + for (int w2 = 0; w2 < TK_T / 32; ++w2) { + const int g2 = s_gt[w2], e2 = s_eq[w2]; + const int take = eq_budget - eb < 0 ? 0 : (eq_budget - eb > e2 ? e2 : eq_budget - eb); + s_gt[w2] = sb; + s_eq[w2] = eb; + sb += g2 + take; + eb += e2; + } + } + __syncthreads(); + int run_sel = s_gt[warp], run_eq = s_eq[warp]; + for (int64_t r0 = w0; r0 < nb && r0 < w0 + 32 * per; r0 += 32) { + const int64_t b = r0 + lane; + const int w = b < nb ? weight(b) : 0; + const uint32_t k = w ? order_key(sc[b]) : 0u; + const int my_gt = (w && k > thr) ? w : 0, my_eq = (w && k == thr) ? w : 0; + if (__ballot_sync(0xffffffffu, (my_gt | my_eq) != 0) == 0u) continue; // a row without a selected block + int pe = my_eq; // tied cells up to and with this lane +#pragma unroll + for (int o = 1; o < 32; o <<= 1) { + const int y = __shfl_up_sync(0xffffffffu, pe, o); + if (lane >= o) pe += y; + } + const int left = eq_budget - (run_eq + pe - my_eq); + const int take = left < 0 ? 0 : (left > my_eq ? my_eq : left); + const int my_sel = my_gt + take; + int ps = my_sel; +#pragma unroll + for (int o = 1; o < 32; o <<= 1) { + const int y = __shfl_up_sync(0xffffffffu, ps, o); + if (lane >= o) ps += y; + } + int32_t* dst = out + run_sel + ps - my_sel; + for (int c = 0; c < my_sel; ++c) dst[c] = (int32_t) (b * R + c); + run_eq += __shfl_sync(0xffffffffu, pe, 31); + run_sel += __shfl_sync(0xffffffffu, ps, 31); + } +} +#endif + // Block scores with every key block read ONCE for all of a call's queries (block_scores_kernel's grid is // (max_blocks / 8) x nq: ~24,600 mostly-idle blocks per layer at a decode window, each key re-read per query). A fixed @@ -1054,8 +1173,8 @@ bool qsa_block_topk_cluster(const float* scores, const int32_t* steps, int64_t n 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, 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 + // keys in registers when every query's blocks fit (contexts up to ~135K cells), else (CUDA) the same threads + // reading them from memory; the same ids. STRATA_TOPK_OLD=1: the original kernel static const bool old = std::getenv("STRATA_TOPK_OLD") != nullptr; if (nq <= 0) return; #if !defined(__HIPCC__) @@ -1089,10 +1208,12 @@ void qsa_block_topk(const float* scores, const int32_t* steps, int64_t nq, int64 #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; + if (old || too_small || reach > fit) { #else - const bool too_small = false; + constexpr int64_t kWideMinBlocks = 4608; // RTX 3060: a prompt batch below ~18K cells is faster on the ref + const bool too_small = reach > fit && active_blocks > 0 && active_blocks < kWideMinBlocks; + if (old || too_small) { #endif - if (old || too_small || reach > fit) { qsa_block_topk_ref(scores, steps, nq, max_blocks, cap, s, ids, stream); return; } @@ -1100,6 +1221,11 @@ 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); } +#if !defined(__HIPCC__) + if (reach > fit) // past the register kernel's reach + block_topk_wide_kernel<<<(unsigned) nq, TK_T, 0, (cudaStream_t) stream>>>(scores, steps, max_blocks, cap, ids); + else +#endif if (reach <= (int64_t) TK_T * TK_PER) block_topk_reg_kernel<<<(unsigned) nq, TK_T, 0, (cudaStream_t) stream>>>(scores, steps, max_blocks, cap, ids); else diff --git a/src/kernels/cuda/router_top10.cu b/src/kernels/cuda/router_top10.cu index f23a63d78e..720cec67dc 100644 --- a/src/kernels/cuda/router_top10.cu +++ b/src/kernels/cuda/router_top10.cu @@ -186,6 +186,83 @@ __global__ void router_top10_kernel(const float* __restrict__ logits, int n_toke } } + +#if defined(STRATA_HIP_GFX906) +// AMD (wave64): the same routing in ONE wavefront per token. The block-wide kernel above spends its time in +// barriers - 10 selection passes with two __syncthreads each over 8 logical warps - not in arithmetic (43 us per +// token on gfx906). Here every lane holds up to RW_PER probabilities in registers and each pass is a 64-lane +// butterfly argmax: no LDS round trip, no block barrier. Bit-identical to the kernel above: fmaxf trees are +// exact, the exponentials and the (float) products are the same expressions, the sum stays one serial ascending +// scan, and the argmax keeps the lowest index on a tie (a total order, so the butterfly's association is free). +constexpr int RW_PER = 8; // n_expert <= 64 * 8 +__global__ void __launch_bounds__(64) router_top10_wave_kernel(const float* __restrict__ logits, int n_tokens, + int n_expert, int k, int* __restrict__ ids, + float* __restrict__ weights) { + __shared__ double s_ex[64 * RW_PER]; + __shared__ double s_sum; + const int t = blockIdx.x; + if (t >= n_tokens) return; + const int lane = threadIdx.x; + const float* l = logits + (size_t) t * n_expert; + float lv[RW_PER]; + float mx = -INFINITY; +#pragma unroll + for (int j = 0; j < RW_PER; ++j) { + const int e = lane + 64 * j; + lv[j] = e < n_expert ? l[e] : -INFINITY; + mx = fmaxf(mx, lv[j]); + } +#pragma unroll + for (int off = 32; off > 0; off >>= 1) mx = fmaxf(mx, __shfl_xor(mx, off, 64)); +#pragma unroll + for (int j = 0; j < RW_PER; ++j) { + const int e = lane + 64 * j; + if (e < n_expert) s_ex[e] = exp((double) lv[j] - (double) mx); + } + __syncthreads(); + if (lane == 0) { + double sum = 0.0; + for (int e = 0; e < n_expert; ++e) sum += s_ex[e]; + s_sum = sum; + } + __syncthreads(); + const float inv = (float) (1.0 / s_sum); + float p[RW_PER]; +#pragma unroll + for (int j = 0; j < RW_PER; ++j) { + const int e = lane + 64 * j; + p[j] = e < n_expert ? (float) (s_ex[e] * inv) : -INFINITY; + } + unsigned taken = 0; + for (int i = 0; i < k; ++i) { + float bv = -INFINITY; + int bi = n_expert; +#pragma unroll + for (int j = 0; j < RW_PER; ++j) { + const int e = lane + 64 * j; + if (e >= n_expert || (taken >> j & 1u)) continue; + if (p[j] > bv || (p[j] == bv && e < bi)) { bv = p[j]; bi = e; } + } +#pragma unroll + for (int off = 32; off > 0; off >>= 1) { + const float ov = __shfl_xor(bv, off, 64); + const int oi = __shfl_xor(bi, off, 64); + if (ov > bv || (ov == bv && oi < bi)) { bv = ov; bi = oi; } + } + if (bi >= n_expert) break; + if ((bi & 63) == lane) taken |= 1u << (bi >> 6); + if (lane == 0) { ids[(size_t) t * k + i] = bi; weights[(size_t) t * k + i] = bv; } + } + 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); // 2**-14 + for (int i = 0; i < k; ++i) + weights[(size_t) t * k + i] = (float) ((double) weights[(size_t) t * k + i] / sc); + } +} +#endif // STRATA_HIP_GFX906 + #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 @@ -346,7 +423,13 @@ void router_top10(const float* logits, int n_tokens, int n_expert, int k, int* i void* stream) { #if defined(__HIPCC__) { +#if defined(STRATA_HIP_GFX906) + // gfx906 (wave64): the one-wavefront kernel below stays the default; the S6 kernel is wave32-shaped and only + // runs here on request (STRATA_HIP_ROUTER_FAST=1) until it is measured on this card + static const bool old = std::getenv("STRATA_HIP_ROUTER_FAST") == nullptr; +#else static const bool old = std::getenv("STRATA_HIP_ROUTER_OLD") != nullptr; +#endif 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(); @@ -375,6 +458,19 @@ void router_top10(const float* logits, int n_tokens, int n_expert, int k, int* i RT_MAX_THREADS * 64); std::exit(1); } +#if defined(STRATA_HIP_GFX906) + if (n_expert <= 64 * RW_PER && !std::getenv("STRATA_ROUTER_BLOCK")) { + router_top10_wave_kernel<<<(unsigned) n_tokens, 64, 0, (cudaStream_t) stream>>>(logits, n_tokens, n_expert, k, + ids, weights); + const cudaError_t e = cudaGetLastError(); + if (e != cudaSuccess) { + std::fprintf(stderr, "router_top10 launch: %s\n", cudaGetErrorString(e)); + std::exit(1); + } + if (stream == nullptr && cudaDeviceSynchronize() != cudaSuccess) std::exit(1); + return; + } +#endif int threads = n_expert < RT_MAX_THREADS ? n_expert : RT_MAX_THREADS; threads = (threads + 31) & ~31; // at least one full warp, for the reductions // the selection's taken-mask, then n_expert doubles for the exponentials, then n_expert floats for the diff --git a/src/kernels/cuda/shared_expert.cu b/src/kernels/cuda/shared_expert.cu index d795bf8400..28bb7edc17 100644 --- a/src/kernels/cuda/shared_expert.cu +++ b/src/kernels/cuda/shared_expert.cu @@ -27,6 +27,7 @@ #include "strata/kernels/s_gemv.hpp" #include "strata/kernels/native_mmvq.hpp" +#include #include #include @@ -157,6 +158,7 @@ __global__ void moe_combine_kernel(const float* __restrict__ parts, const float* } // namespace void shared_expert_set_native_bf16(bool enabled) { native_bf16 = enabled; } +bool shared_expert_native_bf16_enabled() { return native_bf16; } namespace { __global__ void scale_rows_kernel(float* __restrict__ out, const float* __restrict__ g, int n) { @@ -164,6 +166,23 @@ __global__ void scale_rows_kernel(float* __restrict__ out, const float* __restri const int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < n) out[(size_t) t * n + i] *= g[t]; } + +__global__ void sigmoid_scale_rows_kernel(float* __restrict__ out, const float* __restrict__ g, int n) { + const int t = blockIdx.y; + const int i = blockIdx.x * blockDim.x + threadIdx.x; + if (i < n) { + const float gt = __fdividef(1.0f, 1.0f + __expf(-g[t])); + out[(size_t) t * n + i] *= gt; + } +} + +bool fused_swiglu_q81_enabled() { + static const bool on = [] { + const char* v = std::getenv("STRATA_FUSED_SWIGLU_Q81"); + return v == nullptr || std::atoi(v) != 0; + }(); + return on; +} } // namespace void shared_expert_multi(int n_tok, const float* x, const uint16_t* x_bf16, const NativeSharedWeights& nw, @@ -172,28 +191,42 @@ void shared_expert_multi(int n_tok, const float* x, const uint16_t* x_bf16, cons if (n_tok < 1 || n_tok > 8 || !nw.q8_1 || !nw.gate_data || !nw.up_data || !nw.down_data || !stream) throw std::invalid_argument("shared_expert_multi: needs 1..8 tokens, native weights, scratch and a stream"); cudaStream_t cs = (cudaStream_t) stream; - native_quantize_q8_1(x, nw.q8_1, (int) n_embd, n_tok, stream); - native_mmvq(nw.gate_type, nw.gate_data, nw.q8_1, gate, (int) n_embd, (int) n_ff, n_tok, stream); - native_mmvq(nw.up_type, nw.up_data, nw.q8_1, up, (int) n_embd, (int) n_ff, n_tok, stream); + const void* x_q8_1 = nw.x_q8_1; + if (!x_q8_1) { + native_quantize_q8_1(x, nw.q8_1, (int) n_embd, n_tok, stream); + x_q8_1 = nw.q8_1; + } + native_mmvq(nw.gate_type, nw.gate_data, x_q8_1, gate, (int) n_embd, (int) n_ff, n_tok, stream); + native_mmvq(nw.up_type, nw.up_data, x_q8_1, up, (int) n_embd, (int) n_ff, n_tok, stream); const int n = (int) (n_ff * n_tok); - native_swiglu_kernel<<<(unsigned) ((n + THREADS - 1) / THREADS), THREADS, 0, cs>>>(gate, up, gate, n); - native_quantize_q8_1(gate, nw.q8_1, (int) n_ff, n_tok, stream); + if (fused_swiglu_q81_enabled()) { + native_swiglu_quantize_q8_1(gate, up, nw.q8_1, (int) n_ff, n_tok, stream); + } else { + native_swiglu_kernel<<<(unsigned) ((n + THREADS - 1) / THREADS), THREADS, 0, cs>>>(gate, up, gate, n); + native_quantize_q8_1(gate, nw.q8_1, (int) n_ff, n_tok, stream); + } native_mmvq(nw.down_type, nw.down_data, nw.q8_1, out, (int) n_ff, (int) n_embd, n_tok, stream); static const bool batch = [] { const char* v = std::getenv("STRATA_DEC_BATCH"); return v == nullptr || std::atoi(v) != 0; }(); - if (native_bf16 && batch && n_tok > 1) { // one gemv for all rows (outputs identical), one sigmoid launch + if (native_bf16 && batch && n_tok > 1) { // one gemv for all rows (outputs identical), fused sigmoid+scale bf16_gemv_fp32_mmvf_multi(x, n_embd, gate_inp_bf16, g, 1, n_embd, 1, n_tok, stream); - native_scalar_sigmoid_multi_kernel<<<1, n_tok, 0, cs>>>(g); - } else - for (int t = 0; t < n_tok; ++t) { - if (native_bf16) { - bf16_gemv_fp32_mmvf(x + (size_t) t * n_embd, gate_inp_bf16, g + t, n_embd, 1, stream); - native_scalar_sigmoid_kernel<<<1, 1, 0, cs>>>(g + t); - } else { - scalar_gate_kernel<<<1, 256, 0, cs>>>(x_bf16 + (size_t) t * n_embd, gate_inp_bf16, g + t, (int) n_embd); + sigmoid_scale_rows_kernel<<>>( + out, g, (int) n_embd); + } else if (native_bf16 && n_tok == 1) { + bf16_gemv_fp32_mmvf(x, gate_inp_bf16, g, n_embd, 1, stream); + sigmoid_scale_rows_kernel<<>>( + out, g, (int) n_embd); + } else { + for (int t = 0; t < n_tok; ++t) { + if (native_bf16) { + bf16_gemv_fp32_mmvf(x + (size_t) t * n_embd, gate_inp_bf16, g + t, n_embd, 1, stream); + native_scalar_sigmoid_kernel<<<1, 1, 0, cs>>>(g + t); + } else { + scalar_gate_kernel<<<1, 256, 0, cs>>>(x_bf16 + (size_t) t * n_embd, gate_inp_bf16, g + t, (int) n_embd); + } } + scale_rows_kernel<<>>( + out, g, (int) n_embd); } - scale_rows_kernel<<>>( - out, g, (int) n_embd); const cudaError_t e = cudaGetLastError(); if (e != cudaSuccess) throw std::runtime_error(std::string("shared_expert_multi: ") + cudaGetErrorString(e)); } @@ -273,31 +306,36 @@ void shared_expert(const uint8_t* x_q8_0, const uint8_t* x_q8k, const uint16_t* native_mmvq(native->up_type, native->up_data, native->q8_1, up, (int) n_embd, (int) n_ff, 1, stream); else gemv(up_form, up_codes, up_scales, up_off, x_q8_0, x_q8k, up, n_embd, n_ff); - if (native_projection) - native_swiglu_kernel<<>>(gate, up, gate, (int) n_ff); - else - swiglu_kernel<<>>(gate, up, gate, (int) n_ff); - - // down: (n_ff) -> (n_embd), and THE INTERMEDIATE IS QUANTIZED TO THE DOWN WEIGHT'S OWN CONTRACT - which is - // what `ggml_mul_mat` does for every matmul in the model. It used to be rounded to fp16 with no - // justification beyond "the kernel takes fp16". - if (native_down) { - native_quantize_q8_1(gate, native->q8_1, (int) n_ff, 1, stream); + if (native_projection && native_down && fused_swiglu_q81_enabled()) { + native_swiglu_quantize_q8_1(gate, up, native->q8_1, (int) n_ff, 1, stream); native_mmvq(native->down_type, native->down_data, native->q8_1, out, (int) n_ff, (int) n_embd, 1, stream); - } else if (down_form.act_kind == 1) { - if (n_ff % 256 != 0) { - std::fprintf(stderr, "shared_expert: the down weight wants Q8_K but n_ff %lld is not a multiple " - "of 256; Q8_K is structurally impossible here\n", (long long) n_ff); - std::exit(1); - } - quantize_q8_K(gate, h_q8k, n_ff, stream); - gemv(down_form, down_codes, down_scales, down_off, h_q8_0, h_q8k, out, n_ff, n_embd); - } else if (down_form.code_bits == 2) { - quantize_q8_0(gate, h_q8_0, n_ff, stream); - gemv(down_form, down_codes, down_scales, down_off, h_q8_0, h_q8k, out, n_ff, n_embd); } else { - quantize_q8_0(gate, h_q8_0, n_ff, stream); - gemv(down_form, down_codes, down_scales, down_off, h_q8_0, h_q8k, out, n_ff, n_embd); + if (native_projection) + native_swiglu_kernel<<>>(gate, up, gate, (int) n_ff); + else + swiglu_kernel<<>>(gate, up, gate, (int) n_ff); + + // down: (n_ff) -> (n_embd), and THE INTERMEDIATE IS QUANTIZED TO THE DOWN WEIGHT'S OWN CONTRACT - which is + // what `ggml_mul_mat` does for every matmul in the model. It used to be rounded to fp16 with no + // justification beyond "the kernel takes fp16". + if (native_down) { + native_quantize_q8_1(gate, native->q8_1, (int) n_ff, 1, stream); + native_mmvq(native->down_type, native->down_data, native->q8_1, out, (int) n_ff, (int) n_embd, 1, stream); + } else if (down_form.act_kind == 1) { + if (n_ff % 256 != 0) { + std::fprintf(stderr, "shared_expert: the down weight wants Q8_K but n_ff %lld is not a multiple " + "of 256; Q8_K is structurally impossible here\n", (long long) n_ff); + std::exit(1); + } + quantize_q8_K(gate, h_q8k, n_ff, stream); + gemv(down_form, down_codes, down_scales, down_off, h_q8_0, h_q8k, out, n_ff, n_embd); + } else if (down_form.code_bits == 2) { + quantize_q8_0(gate, h_q8_0, n_ff, stream); + gemv(down_form, down_codes, down_scales, down_off, h_q8_0, h_q8k, out, n_ff, n_embd); + } else { + quantize_q8_0(gate, h_q8_0, n_ff, stream); + gemv(down_form, down_codes, down_scales, down_off, h_q8_0, h_q8k, out, n_ff, n_embd); + } } // the per-token scalar gate, then the multiply. Note the gate is computed from `x`, the ORIGINAL hidden @@ -307,11 +345,11 @@ void shared_expert(const uint8_t* x_q8_0, const uint8_t* x_q8k, const uint16_t* // round trip. 256 threads is the reduction's width, not the problem's size. if (use_native) { bf16_gemv_fp32_mmvf(x_f32, gate_inp_bf16, g, n_embd, 1, stream); - native_scalar_sigmoid_kernel<<<1, 1, 0, (cudaStream_t) stream>>>(g); + sigmoid_scale_rows_kernel<<>>(out, g, (int) n_embd); } else { scalar_gate_kernel<<<1, 256, 0, (cudaStream_t) stream>>>(x_bf16, gate_inp_bf16, g, (int) n_embd); + scale_kernel<<>>(out, g, (int) n_embd); } - scale_kernel<<>>(out, g, (int) n_embd); if (stream == nullptr) { const cudaError_t e = cudaDeviceSynchronize(); diff --git a/src/kernels/cuda/verify_kernels.cu b/src/kernels/cuda/verify_kernels.cu index 7908ce404f..f7c4843893 100644 --- a/src/kernels/cuda/verify_kernels.cu +++ b/src/kernels/cuda/verify_kernels.cu @@ -67,23 +67,24 @@ __global__ void gdn_conv_commit_kernel(float* __restrict__ hist, const float* __ hist[c * 3 + 2] = seq[2]; } -__global__ void __launch_bounds__(256) gdn_ab_multi_kernel(const float* __restrict__ x, const uint16_t* __restrict__ wa, - const uint16_t* __restrict__ wb, - const float* __restrict__ dt, - const float* __restrict__ ssm_a, float* __restrict__ gate, - float* __restrict__ beta, int n, int h_v, int T) { - const int row = blockIdx.x * 8 + (threadIdx.x >> 5), lane = threadIdx.x & 31; +template +__global__ void __launch_bounds__(64) gdn_ab_multi_kernel(const float* __restrict__ x, const uint16_t* __restrict__ wa, + const uint16_t* __restrict__ wb, + const float* __restrict__ dt, + const float* __restrict__ ssm_a, float* __restrict__ gate, + float* __restrict__ beta, int n, int h_v, int T) { + const int row = blockIdx.x * 2 + (threadIdx.x >> 5), lane = threadIdx.x & 31; if (row >= 2 * h_v) return; const bool is_beta = row >= h_v; const int r = is_beta ? row - h_v : row; const uint4* w4 = reinterpret_cast((is_beta ? wb : wa) + (size_t) r * n); - float acc[kVerifyMaxT]; + float acc[MAX_T]; #pragma unroll - for (int t = 0; t < kVerifyMaxT; ++t) acc[t] = 0.0f; + for (int t = 0; t < MAX_T; ++t) acc[t] = 0.0f; for (int j = lane; j < n / 8; j += 32) { const uint4 wv = __ldg(w4 + j); #pragma unroll - for (int t = 0; t < kVerifyMaxT; ++t) { + for (int t = 0; t < MAX_T; ++t) { if (t >= T) break; const float* xt = x + (size_t) t * n; const float4 xa = *reinterpret_cast(xt + j * 8); @@ -97,7 +98,7 @@ __global__ void __launch_bounds__(256) gdn_ab_multi_kernel(const float* __restri } } #pragma unroll - for (int t = 0; t < kVerifyMaxT; ++t) { + for (int t = 0; t < MAX_T; ++t) { if (t >= T) break; float a = acc[t]; for (int o = 16; o > 0; o >>= 1) a += __shfl_xor_sync(0xffffffffu, a, o); @@ -112,6 +113,50 @@ __global__ void __launch_bounds__(256) gdn_ab_multi_kernel(const float* __restri } } +// State-only commit kernel: each head's 128 independent state columns are split across 4 blocks of 32 columns +// (48 * 4 = 192 blocks of 128 threads across all SMs, vs 48 blocks of 512 threads), with no sq/o/norm/z/y work. +__global__ void __launch_bounds__(32 * RG) gdn_step_commit_kernel(float* __restrict__ state, + const float* __restrict__ hbuf, int C, + const float* __restrict__ gate, + const float* __restrict__ beta, + int h_k, int h_v, + const int32_t* __restrict__ n_keep) { + const int n = *n_keep; + if (n <= 0) return; + __shared__ float sk[S]; + __shared__ float red[RG][32]; + const int head = blockIdx.x; + const int col_local = threadIdx.x; + const int col = blockIdx.y * 32 + col_local; + const int rg = threadIdx.y; + const int tid = rg * 32 + col_local; // 0 .. 127 + const int qh = head % h_k; + const int qk = S * h_k; + float s[RPG]; + float* base = state + ((size_t) (rg * RPG) * h_v + head) * S + col; + const size_t row_stride = (size_t) h_v * S; +#pragma unroll + for (int r = 0; r < RPG; ++r) s[r] = base[r * row_stride]; + for (int t = 0; t < n; ++t) { + const float* ht = hbuf + (size_t) t * C; + __syncthreads(); + sk[tid] = ht[qk + qh * S + tid]; + __syncthreads(); + const float g = __expf(gate[(size_t) t * h_v + head]); + float kv = 0.0f; +#pragma unroll + for (int r = 0; r < RPG; ++r) kv = fmaf(s[r], sk[rg * RPG + r], kv); + red[rg][col_local] = kv; + __syncthreads(); + const float kv_col = red[0][col_local] + red[1][col_local] + red[2][col_local] + red[3][col_local]; + const float delta = (ht[2 * qk + head * S + col] - g * kv_col) * beta[(size_t) t * h_v + head]; +#pragma unroll + for (int r = 0; r < RPG; ++r) s[r] = fmaf(g, s[r], sk[rg * RPG + r] * delta); + } +#pragma unroll + for (int r = 0; r < RPG; ++r) base[r * row_stride] = s[r]; +} + __global__ void __launch_bounds__(S * RG) gdn_step_norm_multi_kernel(float* __restrict__ state, const float* __restrict__ hbuf, int C, const float* __restrict__ gate, @@ -138,8 +183,12 @@ __global__ void __launch_bounds__(S * RG) gdn_step_norm_multi_kernel(float* __re for (int r = 0; r < RPG; ++r) s[r] = base[r * row_stride]; for (int t = 0; t < n; ++t) { const float* ht = hbuf + (size_t) t * C; + const bool need_out = (t >= t_out_begin); __syncthreads(); // the previous token is done with sk/sq/red/wsum - if (tid < S) { sk[tid] = ht[qk + qh * S + tid]; sq[tid] = ht[qh * S + tid]; } + if (tid < S) { + sk[tid] = ht[qk + qh * S + tid]; + if (need_out) sq[tid] = ht[qh * S + tid]; + } __syncthreads(); const float g = __expf(gate[(size_t) t * h_v + head]); float kv = 0.0f; @@ -149,6 +198,11 @@ __global__ void __launch_bounds__(S * RG) gdn_step_norm_multi_kernel(float* __re __syncthreads(); const float kv_col = red[0][col] + red[1][col] + red[2][col] + red[3][col]; const float delta = (ht[2 * qk + head * S + col] - g * kv_col) * beta[(size_t) t * h_v + head]; + if (!need_out) { +#pragma unroll + for (int r = 0; r < RPG; ++r) s[r] = fmaf(g, s[r], sk[rg * RPG + r] * delta); + continue; + } float o = 0.0f; #pragma unroll for (int r = 0; r < RPG; ++r) { @@ -163,7 +217,6 @@ __global__ void __launch_bounds__(S * RG) gdn_step_norm_multi_kernel(float* __re oc = (red[0][col] + red[1][col] + red[2][col] + red[3][col]) * rsqrtf((float) S); sq_part = oc * oc; } - if (t < t_out_begin) continue; // a replayed token: its state update is needed, its output is not for (int o2 = 16; o2 > 0; o2 >>= 1) sq_part += __shfl_xor_sync(0xffffffffu, sq_part, o2); if ((tid & 31) == 0) wsum[tid >> 5] = sq_part; __syncthreads(); @@ -285,11 +338,13 @@ __global__ void mtp_select_kernel(const float* __restrict__ R_src, int64_t strid const int row = *row_dev; for (int64_t i = (int64_t) blockIdx.x * blockDim.x + threadIdx.x; i < stride; i += (int64_t) gridDim.x * blockDim.x) R_dst[i] = R_src[(size_t) row * stride + i]; + __syncthreads(); if (blockIdx.x == 0 && threadIdx.x == 0) { const int32_t tok = ids[row]; *tok_dst = tok; - if (out != nullptr) ((volatile int32_t*) out)[j] = tok; if (probs != nullptr && out_p != nullptr) ((volatile float*) out_p)[j] = probs[row]; + __threadfence_system(); + if (out != nullptr) ((volatile int32_t*) out)[j] = tok; } } @@ -331,8 +386,8 @@ void ident_hits(const int32_t* ids, int n, int32_t* slot, int32_t* dst, int32_t* void mtp_select(const float* R_src, int64_t R_stride, const int32_t* ids, const int32_t* row_dev, float* R_dst, int32_t* tok_dst, int32_t* out, int j, void* stream, const float* probs, float* out_p) { - mtp_select_kernel<<<16, 256, 0, (cudaStream_t) stream>>>(R_src, R_stride, ids, row_dev, R_dst, tok_dst, out, j, - probs, out_p); + mtp_select_kernel<<<1, 256, 0, (cudaStream_t) stream>>>(R_src, R_stride, ids, row_dev, R_dst, tok_dst, out, j, + probs, out_p); check("mtp_select"); } @@ -404,8 +459,14 @@ void gdn_ab_multi(const float* x, const uint16_t* w_alpha, const uint16_t* w_bet std::fprintf(stderr, "gdn_ab_multi: invalid arguments\n"); std::exit(1); } - gdn_ab_multi_kernel<<<(unsigned) ((2 * h_v + 7) / 8), 256, 0, (cudaStream_t) stream>>>( - x, w_alpha, w_beta, dt, ssm_a, gate, beta, n_embd, h_v, n_tok); + const unsigned blocks = (unsigned) ((2 * h_v + 1) / 2); + if (n_tok <= 4) { + gdn_ab_multi_kernel<4><<>>( + x, w_alpha, w_beta, dt, ssm_a, gate, beta, n_embd, h_v, n_tok); + } else { + gdn_ab_multi_kernel<<>>( + x, w_alpha, w_beta, dt, ssm_a, gate, beta, n_embd, h_v, n_tok); + } check("gdn_ab_multi"); } @@ -417,8 +478,17 @@ void gdn_step_norm_multi(float* state, const float* h, int conv_channels, const std::fprintf(stderr, "gdn_step_norm_multi: invalid arguments\n"); std::exit(1); } - gdn_step_norm_multi_kernel<<<(unsigned) h_v, dim3(S, RG), 0, (cudaStream_t) stream>>>( - state, h, conv_channels, gate, beta, z, gamma, eps, y, h_k, h_v, n_tok, n_keep, t_out_begin); + static const bool commit_split = [] { + const char* e = std::getenv("STRATA_GDN_COMMIT_SPLIT"); + return !e || e[0] != '0'; + }(); + if (commit_split && n_keep != nullptr && t_out_begin >= n_tok) { + gdn_step_commit_kernel<<>>( + state, h, conv_channels, gate, beta, h_k, h_v, n_keep); + } else { + gdn_step_norm_multi_kernel<<<(unsigned) h_v, dim3(S, RG), 0, (cudaStream_t) stream>>>( + state, h, conv_channels, gate, beta, z, gamma, eps, y, h_k, h_v, n_tok, n_keep, t_out_begin); + } check("gdn_step_norm_multi"); } @@ -430,15 +500,58 @@ __global__ void wait_flag_ge_kernel(const volatile uint32_t* flag, uint32_t valu } // namespace namespace { -__global__ void resident_plan_kernel(const int32_t* __restrict__ ids, int n, int k, const int32_t* __restrict__ res, +// one thread per window entry: up to kVerifyMaxT tokens x 10 routed experts (80), so 128 (#646 had 64: a window of +// 7+ tokens lost its last entries) +constexpr int kResidentPlanMax = 128; +static_assert(kVerifyMaxT * 10 <= kResidentPlanMax, "resident_plan: one thread per entry"); +__global__ void __launch_bounds__(kResidentPlanMax) resident_plan_kernel(const int32_t* __restrict__ ids, int n, int k, const int32_t* __restrict__ res, int n_expert, const uint8_t* cache_base, const unsigned long long* slot_off, long long blob, int32_t* __restrict__ pl, long long capx, uint32_t* skip, uint32_t ring) { - // one thread: at most kVerifyMaxT * 10 entries, the host's exact loop - for (int i = 0; i < n; ++i) { - const int32_t e = ids[i]; - if (e < 0 || e >= n_expert || res[e] < 0) { *skip = 0; return; } + __shared__ int32_t s_ids[kResidentPlanMax]; + __shared__ unsigned long long s_ptr[kResidentPlanMax]; + __shared__ int32_t s_first[kResidentPlanMax]; + __shared__ int32_t s_cnt[kResidentPlanMax]; + __shared__ int32_t s_gstart[kResidentPlanMax]; + __shared__ int s_bad; + const int tid = threadIdx.x; + if (tid == 0) s_bad = 0; + __syncthreads(); + + int32_t eid = -1; + if (tid < n) { + eid = ids[tid]; + s_ids[tid] = eid; + const int32_t slot = (eid >= 0 && eid < n_expert) ? res[eid] : -1; + if (slot < 0) { + atomicOr(&s_bad, 1); + } else { + s_ptr[tid] = (unsigned long long) (cache_base + (slot_off ? (size_t) slot_off[slot] : (size_t) slot * (size_t) blob)); + } + } + __syncthreads(); + if (s_bad) { + if (tid == 0 && skip != nullptr) *skip = 0; + return; + } + + int first_j = tid; + int rank_in_group = 0; + int count_same = 0; + if (tid < n) { + for (int j = 0; j < n; ++j) { + if (s_ids[j] == eid) { + if (j < first_j) first_j = j; + if (j < tid) ++rank_in_group; + ++count_same; + } + } + const bool is_first = (first_j == tid); + s_first[tid] = is_first ? 1 : 0; + s_cnt[tid] = is_first ? count_same : 0; } + __syncthreads(); + int32_t* counts = pl; int32_t* start = pl + 4; int32_t* dst = start + capx + 1; @@ -446,30 +559,38 @@ __global__ void resident_plan_kernel(const int32_t* __restrict__ ids, int n, int const long long ptr_off = ((4 + (capx + 1) + 2 * capx) + 1) & ~1ll; unsigned long long* ptr = (unsigned long long*) (pl + ptr_off); int32_t* start2 = pl + ptr_off + 4 * capx; - int groups = 0, entries = 0; - for (int i0 = 0; i0 < n; ++i0) { - bool first = true; - for (int j = 0; j < i0; ++j) if (ids[j] == ids[i0]) { first = false; break; } - if (!first) continue; - const int32_t slot = res[ids[i0]]; - ptr[groups] = (unsigned long long) (cache_base + (slot_off ? (size_t) slot_off[slot] : (size_t) slot * (size_t) blob)); - start[groups] = entries; - for (int i = i0; i < n; ++i) - if (ids[i] == ids[i0]) { - // an entry belongs to i0's group when its first occurrence is i0: the same expert id - dst[entries] = i; - tok[entries] = i / k; - ++entries; - } - ++groups; + + if (tid < n && first_j == tid) { + int grp_idx = 0; + int ent_start = 0; + for (int j = 0; j < tid; ++j) { + grp_idx += s_first[j]; + ent_start += s_cnt[j]; + } + ptr[grp_idx] = s_ptr[tid]; + start[grp_idx] = ent_start; + s_gstart[tid] = ent_start; + } + __syncthreads(); + + if (tid < n) { + const int out_idx = s_gstart[first_j] + rank_in_group; + dst[out_idx] = tid; + tok[out_idx] = tid / k; + } + if (tid == 0) { + int groups = 0; + for (int j = 0; j < n; ++j) groups += s_first[j]; + start[groups] = n; + start2[0] = n; + counts[0] = groups; + counts[1] = n; + counts[2] = 0; + if (skip != nullptr) { + __threadfence(); + *skip = ring; + } } - start[groups] = entries; - start2[0] = entries; - counts[0] = groups; - counts[1] = entries; - counts[2] = 0; - __threadfence(); - *skip = ring; } __global__ void wait_flag_ge_or_kernel(const volatile uint32_t* flag, uint32_t value, const volatile uint32_t* skip) { if (*skip == value) return; @@ -492,8 +613,8 @@ __global__ void copy_or_zero_kernel(float4* __restrict__ dst, const volatile flo void resident_plan(const int32_t* ids, int n_entries, int k, const int32_t* res_layer, int n_expert, const uint8_t* cache_base, const unsigned long long* slot_off, long long blob, int32_t* plan, long long capx, uint32_t* skip, uint32_t ring, void* stream) { - resident_plan_kernel<<<1, 1, 0, (cudaStream_t) stream>>>(ids, n_entries, k, res_layer, n_expert, cache_base, slot_off, - blob, plan, capx, skip, ring); + resident_plan_kernel<<<1, kResidentPlanMax, 0, (cudaStream_t) stream>>>(ids, n_entries, k, res_layer, n_expert, cache_base, slot_off, + blob, plan, capx, skip, ring); check("resident_plan"); } void wait_flag_ge_or(const uint32_t* flag, uint32_t value, const uint32_t* skip, void* stream) { @@ -545,7 +666,9 @@ void copy_indexed(float* dst, const float* src, int64_t stride, const int32_t* i // a GPU timestamp (ns, %globaltimer) into buf[i] - the verify window's stage profiler namespace { __global__ void gpu_stamp_kernel(unsigned long long* buf, int i) { unsigned long long t; -#if defined(__HIPCC__) +#if defined(STRATA_HIP_GFX906) + t = wall_clock64() * 40ull; // gfx906: the wall clock runs at 25 MHz (hipDeviceAttributeWallClockRate) -> ns +#elif defined(__HIPCC__) t = wall_clock64() * 10ull; // gfx10.3 / gfx11 / gfx12: a constant 100 MHz counter, in ns #else asm volatile("mov.u64 %0, %%globaltimer;" : "=l"(t)); diff --git a/src/kernels/fused_gr_bench.cpp b/src/kernels/fused_gr_bench.cpp new file mode 100644 index 0000000000..b7b8452f3a --- /dev/null +++ b/src/kernels/fused_gr_bench.cpp @@ -0,0 +1,116 @@ +// src/kernels/fused_gr_bench.cpp - fused_gr_read_multi: the AMD fast kernels (STRATA_GR_FAST) against the old +// ones, bit for bit on every output (lo, rs, inject, mixed, the in-place R), and each one's time. +// +// build/fused_gr_bench [iters] [T min] [T max] +#include "strata/kernels/fused_gr.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace K = strata::kernels; + +static uint16_t bf16(float x) { + uint32_t u; + std::memcpy(&u, &x, 4); + return (uint16_t) ((u + 0x7fff + ((u >> 16) & 1)) >> 16); +} + +int main(int argc, char** argv) { + setvbuf(stdout, nullptr, _IONBF, 0); + const int iters = argc > 1 ? std::atoi(argv[1]) : 500; + const int t_lo = argc > 2 ? std::atoi(argv[2]) : 1, t_hi = argc > 3 ? std::atoi(argv[3]) : 6; + const int N = 2560, HC = 4, D = N * HC, LR = 320, TM = K::kFusedGrMaxT; + std::mt19937 rng(7); + std::normal_distribution nd(0.f, 1.f); + auto fill = [&](std::vector& v, float sc) { for (auto& x : v) x = sc * nd(rng); }; + std::vector R((size_t) TM * D), bo((size_t) TM * N), inj((size_t) TM * HC), wn(D); + fill(R, 1.0f); fill(bo, 0.5f); fill(inj, 1.0f); fill(wn, 0.3f); + for (auto& x : wn) x += 1.0f; + std::vector wd((size_t) LR * D), wu((size_t) D * LR), wi((size_t) HC * D); + for (auto& x : wd) x = bf16(0.02f * nd(rng)); + for (auto& x : wu) x = bf16(0.05f * nd(rng)); + for (auto& x : wi) x = bf16(0.02f * nd(rng)); + float *dR, *dbo, *dinj, *dwn, *dxn, *dlo, *drs, *dio, *dmix; + uint16_t *dwd, *dwu, *dwi; + cudaMalloc((void**) &dR, R.size() * 4); + cudaMalloc((void**) &dbo, bo.size() * 4); + cudaMalloc((void**) &dinj, inj.size() * 4); + cudaMalloc((void**) &dwn, wn.size() * 4); + cudaMalloc((void**) &dxn, (size_t) TM * D * 4); + cudaMalloc((void**) &dlo, (size_t) TM * LR * 4); + cudaMalloc((void**) &drs, (size_t) TM * HC * 4); + cudaMalloc((void**) &dio, (size_t) TM * HC * 4); + cudaMalloc((void**) &dmix, (size_t) TM * N * 4); + cudaMalloc((void**) &dwd, wd.size() * 2); + cudaMalloc((void**) &dwu, wu.size() * 2); + cudaMalloc((void**) &dwi, wi.size() * 2); + cudaMemcpy(dbo, bo.data(), bo.size() * 4, cudaMemcpyHostToDevice); + cudaMemcpy(dinj, inj.data(), inj.size() * 4, cudaMemcpyHostToDevice); + cudaMemcpy(dwn, wn.data(), wn.size() * 4, cudaMemcpyHostToDevice); + cudaMemcpy(dwd, wd.data(), wd.size() * 2, cudaMemcpyHostToDevice); + cudaMemcpy(dwu, wu.data(), wu.size() * 2, cudaMemcpyHostToDevice); + cudaMemcpy(dwi, wi.data(), wi.size() * 2, cudaMemcpyHostToDevice); + cudaStream_t s; + cudaStreamCreate(&s); + cudaEvent_t e0, e1; + cudaEventCreate(&e0); + cudaEventCreate(&e1); + int failures = 0; + for (int T = t_lo; T <= t_hi; ++T) + for (int apply = 0; apply < 2; ++apply) + for (int inject = 0; inject < 2; ++inject) { + K::FusedGrArgs a[TM]; + for (int t = 0; t < T; ++t) { + a[t].R = dR + (size_t) t * D; a[t].R_out = dR + (size_t) t * D; a[t].apply = apply; + a[t].bo_prev = dbo + (size_t) t * N; a[t].inj_prev = dinj + (size_t) t * HC; + a[t].w_norm = dwn; a[t].w_down = dwd; a[t].w_up = dwu; a[t].w_inject = inject ? dwi : nullptr; + a[t].lo = dlo + (size_t) t * LR; a[t].rs = drs + (size_t) t * HC; + a[t].inject_out = dio + (size_t) t * HC; a[t].mixed = dmix + (size_t) t * N; + } + std::vector out[2]; + for (int f = 0; f < 2; ++f) { + cudaMemcpy(dR, R.data(), R.size() * 4, cudaMemcpyHostToDevice); + cudaMemset(dlo, 0xff, (size_t) TM * LR * 4); + cudaMemset(drs, 0xff, (size_t) TM * HC * 4); + cudaMemset(dio, 0xff, (size_t) TM * HC * 4); + cudaMemset(dmix, 0xff, (size_t) TM * N * 4); + K::fused_gr_set_fast(f); + K::fused_gr_read_multi(a, T, dxn, s); + cudaStreamSynchronize(s); + auto& o = out[f]; + o.resize((size_t) TM * (D + LR + HC + HC + N)); + float* p = o.data(); + cudaMemcpy(p, dR, (size_t) TM * D * 4, cudaMemcpyDeviceToHost); p += (size_t) TM * D; + cudaMemcpy(p, dlo, (size_t) TM * LR * 4, cudaMemcpyDeviceToHost); p += (size_t) TM * LR; + cudaMemcpy(p, drs, (size_t) TM * HC * 4, cudaMemcpyDeviceToHost); p += (size_t) TM * HC; + cudaMemcpy(p, dio, (size_t) TM * HC * 4, cudaMemcpyDeviceToHost); p += (size_t) TM * HC; + cudaMemcpy(p, dmix, (size_t) TM * N * 4, cudaMemcpyDeviceToHost); + } + const bool same = std::memcmp(out[0].data(), out[1].data(), out[0].size() * 4) == 0; + double us[2] = {1e30, 1e30}; + for (int round = 0; round < 3; ++round) + for (int f = 0; f < 2; ++f) { + K::fused_gr_set_fast(f); + for (int i = 0; i < 20; ++i) K::fused_gr_read_multi(a, T, dxn, s); + cudaEventRecord(e0, s); + for (int i = 0; i < iters; ++i) K::fused_gr_read_multi(a, T, dxn, s); + cudaEventRecord(e1, s); + cudaEventSynchronize(e1); + float ms = 0; + cudaEventElapsedTime(&ms, e0, e1); + us[f] = std::fmin(us[f], 1e3 * ms / iters); + } + std::printf("T %d apply %d inject %d | old %6.1f us fast %6.1f us | %s\n", T, apply, inject, us[0], us[1], + same ? "bitwise equal" : "DIFFERS"); + if (!same) ++failures; + } + std::printf("fused_gr_bench: %d differing\n", failures); + return failures ? 1 : 0; +} diff --git a/src/kernels/iq_multi_parity.cpp b/src/kernels/iq_multi_parity.cpp index b497b747ea..ace6facf85 100644 --- a/src/kernels/iq_multi_parity.cpp +++ b/src/kernels/iq_multi_parity.cpp @@ -16,6 +16,7 @@ // are set, small enough that the grouped path's SwiGLU output keeps a finite fp16 q8_1 scale. #include "strata/kernels/f16_bits.hpp" #include "strata/kernels/iq_kernels.hpp" +#include "strata/kernels/native_mmvq.hpp" #include @@ -325,6 +326,71 @@ void bench(cudaStream_t s, std::mt19937& rng) { } } +// #606: both q8_1 activation quantizers (quantize_q8_1_rows and native_quantize_q8_1) against a host transcription +// of the 0.1.38 formula: every finite block bit for bit; a block whose sum (or scale) overflowed fp16 is now stored +// finite (the largest half, its sign), with its values in the int8 range. +void check_q8_1_finite(cudaStream_t s, std::mt19937& rng) { + const int n = 2560, rows = 3, nb = n / 32; + std::vector x = random_x((size_t) rows * n, rng); + for (int i = 0; i < 32; ++i) x[(size_t) 5 * 32 + i] = 3000.0f; // sum 96,000: overflowed + for (int i = 0; i < 32; ++i) x[(size_t) 9 * 32 + i] = i == 3 ? -70000.0f : 0.5f; // sum -69,984.5 + for (int i = 0; i < 32; ++i) x[(size_t) 13 * 32 + i] = 2000.0f + i; // sum 64,496: finite, kept + x[(size_t) n + 40] = 2.0e7f; // amax / 127 overflowed too + float* dx = dalloc(x.size()); + ck(cudaMemcpy(dx, x.data(), x.size() * 4, cudaMemcpyHostToDevice), "x"); + uint8_t* dq = dalloc((size_t) rows * nb * 36); + for (int path = 0; path < 2; ++path) { + if (path == 0) k::quantize_q8_1_rows(dx, rows, n, dq, s); + else k::native_quantize_q8_1(dx, dq, n, rows, s); + ck(cudaStreamSynchronize(s), "quantize"); + std::vector got((size_t) rows * nb * 36); + ck(cudaMemcpy(got.data(), dq, got.size(), cudaMemcpyDeviceToHost), "q"); + int same = 0, clamped = 0, bad = 0; + for (int b = 0; b < rows * nb; ++b) { + const float* v = x.data() + (size_t) b * 32; + float lane[32], amax = 0.0f; + for (int i = 0; i < 32; ++i) { lane[i] = v[i]; amax = std::max(amax, std::fabs(v[i])); } + for (int o = 16; o > 0; o >>= 1) { // the warp's butterfly: every lane ends with the same sum + float nx[32]; + for (int i = 0; i < 32; ++i) nx[i] = lane[i] + lane[i ^ o]; + std::memcpy(lane, nx, sizeof lane); + } + const float d = amax / 127.0f, sum = lane[0]; + const uint8_t* g = got.data() + (size_t) b * 36; + uint16_t gd, gs; + std::memcpy(&gd, g, 2); + std::memcpy(&gs, g + 2, 2); + const uint16_t od = k::f16_from_f32(d), os = k::f16_from_f32(sum); + const bool finite_before = (od & 0x7c00) != 0x7c00 && (os & 0x7c00) != 0x7c00; + if (finite_before) { + uint8_t want[36]; + std::memcpy(want, &od, 2); + std::memcpy(want + 2, &os, 2); + for (int i = 0; i < 32; ++i) // CUDA's roundf: half away from zero, as std::round + want[4 + i] = (uint8_t) (int8_t) (amax == 0.0f ? 0.0f : std::round(v[i] / d)); + // native_mmvq.cu is built with --use_fast_math (its divisions are approximate): there the sum must be + // the same bits, the scale within one fp16 step and each value within one step + bool near = path == 1 && gs == os && std::abs((int) gd - (int) od) <= 1; + for (int i = 0; near && i < 32; ++i) near = std::abs((int) (int8_t) g[4 + i] - (int) (int8_t) want[4 + i]) <= 1; + if (std::memcmp(want, g, 36) == 0 || near) ++same; + else { ++bad; std::printf(" q8_1 path %d block %d: changed although it was finite\n", path, b); } + } else { + const float fd = k::f32_from_f16(gd), fs = k::f32_from_f16(gs); + bool ok = std::isfinite(fd) && std::isfinite(fs) && fd > 0.0f && + ((os & 0x7c00) != 0x7c00 ? gs == os : std::fabs(fs) == 65504.0f && (fs < 0) == (sum < 0)); + for (int i = 0; i < 32; ++i) ok = ok && (int8_t) g[4 + i] >= -127 && (int8_t) g[4 + i] <= 127; + if (ok) ++clamped; + else { ++bad; std::printf(" q8_1 path %d block %d: d %g sum %g not finite and clamped\n", path, b, fd, fs); } + } + } + std::printf("q8_1 finite (%s): %d finite blocks as before, %d overflowed blocks clamped finite%s\n", + path == 0 ? "quantize_q8_1_rows" : "native_quantize_q8_1", same, clamped, bad ? " FAIL" : ""); + g_fail += bad + (clamped != 3); + } + cudaFree(dx); + cudaFree(dq); +} + } // namespace int main(int argc, char** argv) { @@ -340,6 +406,7 @@ int main(int argc, char** argv) { for (int dt : {20, 42}) check_grouped(gu, dt, 2560, 640, s, rng); // the model's shape check_grouped(gu, 23, 1024, 512, s, rng); // IQ4_XS down needs n_ff % 256 == 0 } + check_q8_1_finite(s, rng); if (do_bench) bench(s, rng); std::printf("iq_multi_parity: %d failures\n", g_fail); cudaStreamDestroy(s); diff --git a/src/kernels/kv_hybrid_parity.cpp b/src/kernels/kv_hybrid_parity.cpp index 98f91b3c87..ca371782cc 100644 --- a/src/kernels/kv_hybrid_parity.cpp +++ b/src/kernels/kv_hybrid_parity.cpp @@ -329,7 +329,7 @@ int main() { float* d_at4 = dalloc((size_t) QH * D); const bool took = k::qsa_prompt_attn_batch(d_q, pools, d_ids, d_step, cells, s, d_at4, 1, nullptr); if (!took) { -#if defined(STRATA_USE_HIP) +#if defined(STRATA_USE_HIP) || defined(STRATA_HIP_GFX906) // AMD: the tensor-core prompt path is CUDA-only, so it refuses every pool and the old kernel runs std::printf("[5/5] qsa_prompt_attn mode 3: PASS (refused on HIP - the old kernel runs)\n"); #else diff --git a/src/kernels/mmvq_multi_parity.cpp b/src/kernels/mmvq_multi_parity.cpp index 4902670b8e..fd921d3449 100644 --- a/src/kernels/mmvq_multi_parity.cpp +++ b/src/kernels/mmvq_multi_parity.cpp @@ -36,6 +36,8 @@ #include #include #include +#include +#include #include #include #include @@ -236,6 +238,16 @@ int main() { } // THE NEGATIVE CONTROL, asserted where it can see something: past T = 4 the generic layout is a different one, // so if it still finds no difference the comparison cannot tell the layouts apart and the pass proves nothing. +#if defined(STRATA_HIP_GFX906) + // gfx906: the wave64 layout (one wavefront per row) is the same kernel for every column count, so multi_exact + // has nothing to switch and the control cannot see a difference. The control with power is the CUDA layout: + // ctest's mmvq_multi_parity_cuda_layout runs this with STRATA_MMVQ_WAVE=0. + if (off.powerless_cases != 0 && !(std::getenv("STRATA_MMVQ_WAVE") && std::string(std::getenv("STRATA_MMVQ_WAVE")) == "0")) { + std::printf("\nmmvq_multi_parity: ok (gfx906 wave64 layout: every column count runs the same exact kernel, " + "so the negative control has no other layout to find; mmvq_multi_parity_cuda_layout checks it).\n"); + return 0; + } +#endif if (off.powerless_cases != 0) { std::printf("\nmmvq_multi_parity: FAILED (no power). In %d case(s) no output differs with multi_exact off at " "any T > %d, where the generic layout is not the exact one: for those cases this comparison " diff --git a/src/kernels/native_expert_bench.cpp b/src/kernels/native_expert_bench.cpp new file mode 100644 index 0000000000..7f16e220a2 --- /dev/null +++ b/src/kernels/native_expert_bench.cpp @@ -0,0 +1,170 @@ +// src/kernels/native_expert_bench.cpp - the grouped native experts on real GGUF rows: one AMD layout against +// another, bit for bit, and each one's time. +// +// build/native_expert_bench [iters] +// +// Modes are STRATA_EXP_MODE values. A group is one expert (distinct blobs, ~2 MB each, so G >= 8 does not fit +// in L2); its entries read distinct tokens of an 8-token window. The output must be bitwise equal to the +// reference mode's: the verify window's text depends on it. +#include "strata/artifact/gguf_reader.hpp" +#include "strata/kernels/cpu/native_expert.hpp" +#include "strata/kernels/iq_kernels.hpp" + +#include "ggml.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cpu = strata::kernels::cpu; +namespace K = strata::kernels; + +int main(int argc, char** argv) { + setvbuf(stdout, nullptr, _IONBF, 0); + if (argc < 7) { + std::fprintf(stderr, "usage: native_expert_bench [iters]\n"); + return 2; + } + strata::GgufFile gguf(argv[1]); + std::vector layers; + { + std::stringstream ss(argv[2]); + std::string t; + while (std::getline(ss, t, ',')) layers.push_back(std::atoi(t.c_str())); + } + const int G = std::atoi(argv[3]), T = std::atoi(argv[4]), mref = std::atoi(argv[5]), mode = std::atoi(argv[6]); + const int iters = argc > 7 ? std::atoi(argv[7]) : 200; + const int NTOK = 8; + if (T < 1 || T > NTOK || G < 1) { std::fprintf(stderr, "tokens 1..8, groups >= 1\n"); return 2; } + const int64_t H = 2560, FF_FULL = 640, FF = FF_FULL; // Flash-Next's expert geometry + int failures = 0; + cudaStream_t s; + cudaStreamCreate(&s); + cudaEvent_t e0, e1; + cudaEventCreate(&e0); + cudaEventCreate(&e1); + for (int l : layers) { + const strata::TensorInfo* t[3] = {}; + const char* roles[3] = {"gate", "up", "down"}; + for (const auto& ti : gguf.tensors()) + for (int r = 0; r < 3; ++r) + if (ti.name == "blk." + std::to_string(l) + ".ffn_" + roles[r] + "_exps.weight") t[r] = &ti; + if (!t[0] || !t[1] || !t[2]) { std::printf("layer %d: no expert tensors\n", l); ++failures; continue; } + cpu::NativeFmt f; + std::string err; + if (!cpu::native_fmt((int) t[0]->type, (int) t[2]->type, H, FF, f, err)) { + std::printf("layer %d: %s\n", l, err.c_str()); ++failures; continue; + } + cpu::NativeFmt ff; // the full expert's geometry, for strides into the GGUF + if (!cpu::native_fmt((int) t[0]->type, (int) t[2]->type, H, FF_FULL, ff, err)) { + std::printf("layer %d: %s\n", l, err.c_str()); ++failures; continue; + } + const size_t dsz = f.bytes - f.down_off, dsz_full = ff.bytes - ff.down_off; + const size_t drow = dsz / H, drow_full = dsz_full / H; + std::vector blobs((size_t) G * f.bytes); + for (int g = 0; g < G; ++g) { + const size_t E = (size_t) ((g * 37 + 5) % 256); + uint8_t* b = blobs.data() + (size_t) g * f.bytes; + std::memcpy(b, gguf.tensor_data(*t[0]) + E * ff.up_off, f.up_off); + std::memcpy(b + f.up_off, gguf.tensor_data(*t[1]) + E * ff.up_off, f.up_off); + for (int64_t r = 0; r < H; ++r) + std::memcpy(b + f.down_off + r * drow, gguf.tensor_data(*t[2]) + E * dsz_full + r * drow_full, drow); + } + std::mt19937 rng(17 + l); + std::normal_distribution nd(0.f, 1.f); + std::vector x((size_t) NTOK * H); + for (auto& v : x) v = nd(rng); + const int NE = G * T; + std::vector ptr(G); + std::vector start(G + 1), dst(NE), tok(NE); + const auto L = K::native_expert_layout(f.gu_type, f.d_type, H, FF); + uint8_t* dblob; + void *dx, *dxq, *dscr; + float* dout; + unsigned long long* dptr; + int32_t *dstart, *dn, *ddst, *dtok; + cudaMalloc((void**) &dblob, blobs.size()); + cudaMalloc(&dx, x.size() * 4); + cudaMalloc(&dxq, (size_t) NTOK * H / 32 * 36); + cudaMalloc(&dscr, K::native_expert_scratch_bytes(NE, FF)); + cudaMalloc((void**) &dout, (size_t) NE * H * 4); + cudaMalloc((void**) &dptr, (size_t) G * 8); + cudaMalloc((void**) &dstart, (size_t) (G + 1) * 4); + cudaMalloc((void**) &dn, 4); + cudaMalloc((void**) &ddst, (size_t) NE * 4); + cudaMalloc((void**) &dtok, (size_t) NE * 4); + for (int g = 0; g < G; ++g) { + ptr[g] = (unsigned long long) (dblob + (size_t) g * f.bytes); + start[g] = g * T; + for (int j = 0; j < T; ++j) { + dst[g * T + j] = g * T + j; + tok[g * T + j] = (g * 3 + j) % NTOK; + } + } + start[G] = NE; + cudaMemcpy(dblob, blobs.data(), blobs.size(), cudaMemcpyHostToDevice); + cudaMemcpy(dx, x.data(), x.size() * 4, cudaMemcpyHostToDevice); + cudaMemcpy(dptr, ptr.data(), (size_t) G * 8, cudaMemcpyHostToDevice); + cudaMemcpy(dstart, start.data(), (size_t) (G + 1) * 4, cudaMemcpyHostToDevice); + cudaMemcpy(dn, &G, 4, cudaMemcpyHostToDevice); + cudaMemcpy(ddst, dst.data(), (size_t) NE * 4, cudaMemcpyHostToDevice); + cudaMemcpy(dtok, tok.data(), (size_t) NE * 4, cudaMemcpyHostToDevice); + K::quantize_q8_1_rows((const float*) dx, NTOK, H, dxq, s); + auto run = [&](int m, int phase) { + K::native_expert_set_mode(m, phase); + K::native_expert_grouped(L, dptr, dstart, dn, ddst, dtok, G, NE, dxq, dscr, dout, s); + }; + std::vector ref((size_t) NE * H), got((size_t) NE * H); + cudaMemsetAsync(dout, 0xff, got.size() * 4, s); + run(mref, 0); + cudaMemcpyAsync(ref.data(), dout, ref.size() * 4, cudaMemcpyDeviceToHost, s); + cudaMemsetAsync(dout, 0xff, got.size() * 4, s); + run(mode, 0); + cudaMemcpyAsync(got.data(), dout, got.size() * 4, cudaMemcpyDeviceToHost, s); + if (cudaStreamSynchronize(s) != cudaSuccess) { std::printf("layer %d: CUDA error\n", l); return 1; } + size_t ndiff = 0; + double worst = 0; + for (size_t i = 0; i < ref.size(); ++i) + if (std::memcmp(&ref[i], &got[i], 4) != 0) { + ++ndiff; + worst = std::fmax(worst, std::fabs((double) ref[i] - got[i]) / (std::fabs((double) ref[i]) + 1e-6)); + } + auto time = [&](int m, int phase) { + for (int i = 0; i < 10; ++i) run(m, phase); + cudaEventRecord(e0, s); + for (int i = 0; i < iters; ++i) run(m, phase); + cudaEventRecord(e1, s); + cudaEventSynchronize(e1); + float ms = 0; + cudaEventElapsedTime(&ms, e0, e1); + return 1e3 * ms / iters; + }; + const double gu_b = (double) G * 2 * f.up_off, d_b = (double) G * dsz; + double us[2][3] = {{1e30, 1e30, 1e30}, {1e30, 1e30, 1e30}}; + for (int round = 0; round < 3; ++round) // interleaved, the minimum: the clocks wander between runs + for (int k = 0; k < 2; ++k) + for (int p = 0; p < 3; ++p) us[k][p] = std::fmin(us[k][p], time(k ? mode : mref, p)); + std::printf("layer %2d %-7s/%-6s G %d T %d | mode %d: all %7.1f us gu %7.1f us (%3.0f GB/s) down %6.1f us (%3.0f GB/s)" + " | mode %d: all %7.1f us gu %7.1f us (%3.0f GB/s) down %6.1f us (%3.0f GB/s) | %s\n", + l, ggml_type_name((ggml_type) f.gu_type), ggml_type_name((ggml_type) f.d_type), G, T, + mref, us[0][0], us[0][1], gu_b / us[0][1] / 1e3, us[0][2], d_b / us[0][2] / 1e3, + mode, us[1][0], us[1][1], gu_b / us[1][1] / 1e3, us[1][2], d_b / us[1][2] / 1e3, + ndiff ? "DIFFERS" : "bitwise equal"); + if (ndiff) { + std::printf(" %zu of %zu outputs differ, worst rel %.2e\n", ndiff, ref.size(), worst); + ++failures; + } + K::native_expert_set_mode(-1, 0); + cudaFree(dblob); cudaFree(dx); cudaFree(dxq); cudaFree(dscr); cudaFree(dout); cudaFree(dptr); + cudaFree(dstart); cudaFree(dn); cudaFree(ddst); cudaFree(dtok); + } + std::printf("native_expert_bench: %d differing\n", failures); + return failures ? 1 : 0; +} diff --git a/src/kernels/native_expert_parity.cpp b/src/kernels/native_expert_parity.cpp index 0183f01b48..01133fad54 100644 --- a/src/kernels/native_expert_parity.cpp +++ b/src/kernels/native_expert_parity.cpp @@ -147,7 +147,7 @@ int check_blob(const cpu::NativeFmt& f, const std::vector& blob, int se }; // guarded: the binary runs on AVX-2 CPUs too, and IQ4_XS has no AVX-512 kernel (an empty switch) if (cpu::cpu_avx512_ok() && cpu::iq512_supported(f.gu_type)) check("avx512", true); - if (cpu::iq256_supported(f.gu_type)) check("avx2", false); + if (cpu::cpu_avx2_ok() && cpu::iq256_supported(f.gu_type)) check("avx2", false); // no AVX2: ggml-cpu only } for (int k = 0; k < NT; ++k) { cpu::native_quant_h(f, ff[k].data(), hq[k].data()); @@ -180,7 +180,7 @@ int check_blob(const cpu::NativeFmt& f, const std::vector& blob, int se // BIT FOR BIT, for 1..8 tokens; then the time of 4 tokens (a verify window) both ways for (int role = 0; role < 2; ++role) { const int type = role == 0 ? f.gu_type : f.d_type; - if (!cpu::kq256_supported(type) || (role == 0 && type != 12)) continue; + if (!cpu::cpu_avx2_ok() || !cpu::kq256_supported(type) || (role == 0 && type != 12)) continue; const int n = role == 0 ? (int) H : (int) FF, rows = role == 0 ? (int) FF : (int) H; const size_t rb = role == 0 ? f.gu_row : f.d_row; const uint8_t* w = blob.data() + (role == 0 ? 0 : f.down_off); @@ -223,7 +223,7 @@ int check_blob(const cpu::NativeFmt& f, const std::vector& blob, int se role == 0 ? "gate" : "down", differ, us_k, us_g, us_g / us_k); if (differ) ++failures; } - if (f.d_type == 42) { + if (cpu::q2_native_kernels(f.d_type)) { // (b2) the GGUF-layout Q2_0 kernel the pool uses for Q2_0 down projections - the AVX-512 one // where the CPU has it, the AVX-2 one (q2_avx2.cpp) where it does not. Calling the AVX-512 // kernel unconditionally faults on a Zen 2/3 CPU. @@ -240,7 +240,7 @@ int check_blob(const cpu::NativeFmt& f, const std::vector& blob, int se std::printf(" q2_0 %s down vs ggml down: rel %.2e\n", cpu::cpu_avx512_ok() ? "AVX-512" : "AVX-2", rel(alt, got_c)); } - if (f.d_type == 20) { + if (f.d_type == 20 && cpu::cpu_avx2_ok()) { // (b3) the IQ4_NL multi-token AVX-2 kernel the pool now uses for IQ4_NL down projections, // against ggml-cpu's single-token vec_dot on the SAME Q8_0 activations (h), plus timing. std::vector alt((size_t) NT * H), refd((size_t) NT * H); diff --git a/src/kernels/ngram.cpp b/src/kernels/ngram.cpp index a533dbf510..7292dd963b 100644 --- a/src/kernels/ngram.cpp +++ b/src/kernels/ngram.cpp @@ -150,6 +150,11 @@ struct PleTable::Impl { bool locked = false; uint32_t rows[PLE_N_HEADS] = {}; uint8_t raw[PLE_N_HEADS * PLE_ROW_BYTES_MAX] = {}; + static constexpr size_t kMaxPrefetch = 16; + size_t n_prefetch = 0; + strata::ngram::PleReader::Ticket prefetch_tickets[kMaxPrefetch] = {}; + uint32_t prefetch_keys[kMaxPrefetch][PLE_N_HEADS] = {}; + uint8_t prefetch_raw[kMaxPrefetch][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 @@ -275,7 +280,40 @@ bool PleTable::open(const std::string& gguf_path, std::string& err, const PleIoO return true; } +void PleTable::wait_prefetches() { + if (impl_->n_prefetch == 0) return; + std::string dummy; + for (size_t i = 0; i < impl_->n_prefetch; ++i) (void) impl_->reader.collect(impl_->prefetch_tickets[i], dummy); + impl_->n_prefetch = 0; +} + +void PleTable::prefetch_rows(const uint32_t* rows16) { + if (impl_->mode == PleIo::Direct && impl_->reader.is_open()) { + for (size_t i = 0; i < impl_->n_prefetch; ++i) + if (std::memcmp(impl_->prefetch_keys[i], rows16, sizeof(uint32_t) * PLE_N_HEADS) == 0) return; + if (impl_->n_prefetch >= Impl::kMaxPrefetch) wait_prefetches(); + const size_t slot = impl_->n_prefetch++; + std::memcpy(impl_->prefetch_keys[slot], rows16, sizeof(uint32_t) * PLE_N_HEADS); + impl_->prefetch_tickets[slot] = impl_->reader.issue(impl_->prefetch_keys[slot], PLE_N_HEADS, impl_->prefetch_raw[slot]); + return; + } +#if defined(_WIN32) + if (impl_->data != nullptr && g_ple_prefetch) { + WIN32_MEMORY_RANGE_ENTRY ranges[PLE_N_HEADS]; + 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] * impl_->rb); + ranges[n].NumberOfBytes = impl_->rb; + ++n; + } + if (n > 0) (void) PrefetchVirtualMemory(GetCurrentProcess(), n, ranges, 0); + } +#endif +} + void PleTable::close() { + wait_prefetches(); impl_->reader.close(); impl_->pending = false; impl_->locked = false; // the unmap below releases the lock @@ -318,6 +356,7 @@ void PleTable::read_row(uint32_t row, float* out160) const { } bool PleTable::issue(const uint32_t* rows16) { + wait_prefetches(); std::memcpy(impl_->rows, rows16, sizeof impl_->rows); if (impl_->mode == PleIo::Direct) { if (impl_->pending) return false; // one token in flight per table @@ -360,6 +399,35 @@ 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) { + // Fast path: if all requested tokens match our in-flight/completed prefetch slots, collect them directly + // without issuing a second PleReader ticket or copying through RowCache. + if (impl_->n_prefetch >= n_tokens && n_tokens <= Impl::kMaxPrefetch) { + bool exact_match = true; + for (size_t t = 0; t < n_tokens; ++t) { + if (std::memcmp(impl_->prefetch_keys[t], rows + t * PLE_N_HEADS, sizeof(uint32_t) * PLE_N_HEADS) != 0) { + exact_match = false; + break; + } + } + if (exact_match) { + for (size_t i = 0; i < impl_->n_prefetch; ++i) { + if (!impl_->reader.collect(impl_->prefetch_tickets[i], err)) { + impl_->n_prefetch = 0; + return false; + } + } + impl_->n_prefetch = 0; + for (size_t t = 0; t < n_tokens; ++t) { + for (int h = 0; h < PLE_N_HEADS; ++h) { + impl_->decode(impl_->prefetch_raw[t] + (size_t) h * impl_->rb, + out + (t * PLE_N_HEADS + (size_t) h) * PLE_HEAD_DIM); + } + } + impl_->bytes_read += (uint64_t) n * impl_->rb; + return true; + } + } + wait_prefetches(); std::vector raw(n * impl_->rb); const auto ticket = impl_->reader.issue(rows, n, raw.data()); if (!impl_->reader.collect(ticket, err)) return false; @@ -367,6 +435,7 @@ bool PleTable::gather_batch(const uint32_t* rows, size_t n_tokens, float* out, s impl_->bytes_read += (uint64_t) n * impl_->rb; return true; } + wait_prefetches(); for (size_t i = 0; i < n; ++i) read_row(rows[i], out + i * PLE_HEAD_DIM); return true; } diff --git a/src/kernels/qsa_prompt_attn_parity.cpp b/src/kernels/qsa_prompt_attn_parity.cpp index d7dfda2700..4a0921db65 100644 --- a/src/kernels/qsa_prompt_attn_parity.cpp +++ b/src/kernels/qsa_prompt_attn_parity.cpp @@ -41,6 +41,18 @@ float h2f(uint16_t b) { __half h; *reinterpret_cast(&h) = b; return _ uint16_t f2h(float f) { __half h = __float2half(f); return *reinterpret_cast(&h); } int run(int fmt, int64_t ctx, int64_t nq, int reps) { // fmt 1 int8, 0 fp16, 2 q4_0 +#if !defined(__HIPCC__) && !defined(STRATA_USE_HIP) + if (fmt == 2) { // mode 4 (Q4_0 KV) runs on sm_80 and newer only: below that the dispatcher keeps the old kernel + int dev = 0, major = 0; + ck(cudaGetDevice(&dev), "device"); + ck(cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev), "cc"); + if (major < 8) { + std::printf("SKIP q4_0 ctx %lld: the tensor-core kernel takes Q4_0 KV on sm_80+ only (this device: sm_%d)\n", + (long long) ctx, major); + return 0; + } + } +#endif const k::QsaShapes s = k::qsa_real_shapes(); const int64_t HD = s.head_dim, NKV = s.n_head_kv, NH = s.n_head, PS = s.page_size; const int64_t pages = (ctx + PS - 1) / PS, rows = pages * NKV * PS; diff --git a/src/kernels/qsa_topk_parity.cpp b/src/kernels/qsa_topk_parity.cpp new file mode 100644 index 0000000000..d14af3e132 --- /dev/null +++ b/src/kernels/qsa_topk_parity.cpp @@ -0,0 +1,139 @@ +// src/kernels/qsa_topk_parity.cpp - the QSA top-k the engine dispatches (qsa_block_topk) against the original kernel +// (qsa_block_topk_ref): the selected cells must be identical (GPU, synthetic, no model). +// +// A case is a context under a capacity (the engine's --max-context; max_blocks = capacity / 4 + 2, and without an +// active-block count the dispatch follows it), with consecutive queries at the context's end so that the tail block +// holds 0 to 3 cells. Three score sets per case: continuous, 64 levels (many ties at the threshold) and all equal +// (the rule's ties go to the lowest index). The dispatch is called as a decode window calls it (no block count) and as +// the prompt path does (with the call's largest block count). The reference and the dispatched kernel are also timed. +// +// Usage: qsa_topk_parity --selftest +// qsa_topk_parity CONTEXT [QUERIES=256] [REPS=10] [CAPACITY=CONTEXT] [COUNT=1] (COUNT=0: no block count) +#include "strata/kernels/qsa.hpp" +#include "strata/kernels/qsa_select.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace k = strata::kernels; + +namespace { +void ck(cudaError_t e, const char* w) { + if (e != cudaSuccess) { std::fprintf(stderr, "%s: %s\n", w, cudaGetErrorString(e)); std::exit(2); } +} + +bool run_case(int64_t ctx, int64_t nq, int reps, int64_t capacity, bool counted) { + const k::QsaShapes s = k::qsa_real_shapes(); + const int64_t max_blocks = capacity / s.idx_block + 2, cap = k::qsa_selection_width(k::kTopkMaxCells, s); + std::vector steps((size_t) (nq * k::kStepCount)); + for (int64_t i = 0; i < nq; ++i) { + int32_t* st = steps.data() + i * k::kStepCount; + const int64_t pos = ctx - nq + i; + st[k::kStepPos] = (int32_t) pos; + st[k::kStepNKv] = (int32_t) (pos + 1); + st[k::kStepNBid] = (int32_t) ((pos + 1) / s.idx_block); + st[k::kStepWidth] = (int32_t) k::qsa_selection_width(pos + 1, s); + } + std::mt19937 rng((uint32_t) (ctx * 31 + capacity)); + std::normal_distribution nd(0.f, 1.f); + std::vector scores((size_t) (nq * max_blocks)), tied(scores.size()), equal(scores.size(), 0.25f); + for (int64_t i = 0; i < nq; ++i) { + const int32_t* st = steps.data() + i * k::kStepCount; + float* row = scores.data() + i * max_blocks; + for (int64_t b = 0; b < max_blocks; ++b) row[b] = nd(rng); + // the tail block as the scorers leave it: far above the others when it holds cells + if (st[k::kStepNKv] % s.idx_block != 0) row[st[k::kStepNBid]] += 1e9f; + for (int64_t b = 0; b < max_blocks; ++b) + tied[(size_t) (i * max_blocks + b)] = row[b] > 1e8f ? row[b] : std::floor(row[b] * 8.0f) / 8.0f; // ~64 levels + } + int32_t *d_steps = nullptr, *ids_ref = nullptr, *ids_new = nullptr; + float* d_scores = nullptr; + ck(cudaMalloc(&d_steps, steps.size() * 4), "malloc"); + ck(cudaMalloc(&d_scores, scores.size() * 4), "malloc"); + ck(cudaMalloc(&ids_ref, (size_t) (nq * cap) * 4), "malloc"); + ck(cudaMalloc(&ids_new, (size_t) (nq * cap) * 4), "malloc"); + ck(cudaMemcpy(d_steps, steps.data(), steps.size() * 4, cudaMemcpyHostToDevice), "upload"); + const int64_t active = counted ? steps[(size_t) ((nq - 1) * k::kStepCount + k::kStepNBid)] + 1 : -1; + std::vector a((size_t) (nq * cap)), b(a.size()); + auto same = [&](const std::vector& sc) -> int64_t { + ck(cudaMemcpy(d_scores, sc.data(), sc.size() * 4, cudaMemcpyHostToDevice), "upload"); + ck(cudaMemset(ids_ref, 0xff, a.size() * 4), "memset"); + ck(cudaMemset(ids_new, 0xff, a.size() * 4), "memset"); + k::qsa_block_topk_ref(d_scores, d_steps, nq, max_blocks, cap, s, ids_ref, nullptr); + k::qsa_block_topk(d_scores, d_steps, nq, max_blocks, cap, s, ids_new, nullptr, active); + ck(cudaDeviceSynchronize(), "sync"); + ck(cudaMemcpy(a.data(), ids_ref, a.size() * 4, cudaMemcpyDeviceToHost), "down"); + ck(cudaMemcpy(b.data(), ids_new, b.size() * 4, cudaMemcpyDeviceToHost), "down"); + int64_t n = 0; + for (int64_t i = 0; i < nq; ++i) { + const int64_t w = steps[(size_t) (i * k::kStepCount + k::kStepWidth)]; + n += std::equal(a.begin() + i * cap, a.begin() + i * cap + w, b.begin() + i * cap); + } + return n; + }; + const int64_t same_tied = same(tied), same_equal = same(equal), same_cont = same(scores); // d_scores: continuous + cudaEvent_t e0, e1; + ck(cudaEventCreate(&e0), "event"); + ck(cudaEventCreate(&e1), "event"); + auto timed = [&](auto f) { + ck(cudaEventRecord(e0), "record"); + for (int r = 0; r < reps; ++r) f(); + ck(cudaEventRecord(e1), "record"); + ck(cudaEventSynchronize(e1), "time"); + float ms = 0; + ck(cudaEventElapsedTime(&ms, e0, e1), "elapsed"); + return ms / (float) reps; + }; + const float t_ref = timed([&] { k::qsa_block_topk_ref(d_scores, d_steps, nq, max_blocks, cap, s, ids_ref, nullptr); }); + const float t_new = timed([&] { k::qsa_block_topk(d_scores, d_steps, nq, max_blocks, cap, s, ids_new, nullptr, active); }); + cudaEventDestroy(e0); cudaEventDestroy(e1); + cudaFree(d_steps); cudaFree(d_scores); cudaFree(ids_ref); cudaFree(ids_new); + const bool ok = same_cont == nq && same_tied == nq && same_equal == nq; + std::printf("%s context %lld under a capacity of %lld (%lld blocks), %lld queries %s: reference %.3f ms, " + "dispatched %.3f ms (%.1fx); identical ids: continuous %lld/%lld, 64 levels %lld/%lld, equal scores " + "%lld/%lld\n", ok ? "PASS" : "FAIL", (long long) ctx, (long long) capacity, (long long) max_blocks, + (long long) nq, counted ? "with their block count" : "without a block count", t_ref, t_new, t_ref / t_new, + (long long) same_cont, (long long) nq, (long long) same_tied, + (long long) nq, (long long) same_equal, (long long) nq); + return ok; +} +} // namespace + +int main(int argc, char** argv) { + if (argc > 1 && std::strcmp(argv[1], "--selftest") == 0) { + // {context, capacity}: both sides of the register kernel's reach (33,792 blocks = 135,168 cells), short + // contexts under a long capacity (fewer blocks than the kernel's threads; a prompt batch below the dispatch's + // 4,608 blocks), and the long contexts themselves + const int64_t cases[][2] = {{9000, 9000}, {135160, 135160}, {135176, 135176}, {2100, 262144}, + {9000, 262144}, {32768, 262144}, {262144, 262144}, {300001, 524288}, + {524288, 524288}}; + bool ok = true; + for (const auto& c : cases) { + ok = run_case(c[0], 8, 3, c[1], false) && ok; // a decode window's call + ok = run_case(c[0], 64, 3, c[1], true) && ok; // the prompt path's + } + std::printf("%s\n", ok ? "qsa_topk_parity: PASS" : "qsa_topk_parity: FAIL"); + return ok ? 0 : 1; + } + if (argc < 2) { + std::fprintf(stderr, "usage: qsa_topk_parity --selftest | CONTEXT [QUERIES=256] [REPS=10] [CAPACITY=CONTEXT] " + "[COUNT=1]\n"); + return 2; + } + const int64_t ctx = std::atoll(argv[1]); + const int64_t nq = argc > 2 ? std::atoll(argv[2]) : 256; + const int reps = argc > 3 ? std::atoi(argv[3]) : 10; + const int64_t capacity = argc > 4 ? std::atoll(argv[4]) : ctx; + if (ctx < nq || nq <= 0 || reps <= 0 || capacity < ctx) { + std::fprintf(stderr, "qsa_topk_parity: CONTEXT >= QUERIES > 0, REPS > 0, CAPACITY >= CONTEXT\n"); + return 2; + } + return run_case(ctx, nq, reps, capacity, argc <= 5 || std::atoi(argv[5]) != 0) ? 0 : 1; +} diff --git a/src/kernels/route_window_parity.cpp b/src/kernels/route_window_parity.cpp new file mode 100644 index 0000000000..bb0d6b9a42 --- /dev/null +++ b/src/kernels/route_window_parity.cpp @@ -0,0 +1,75 @@ +// route_window_parity - the verify window's batched routing against the per-token calls, bit for bit. +// +// `moe_route_window` replaces n per-token (projection + top-k) pairs by one multi-column BF16 projection and one +// n-token top-k. Speculative decoding is exact only if a token's route does not depend on how many tokens share +// its window, so both batched kernels must equal their one-token forms exactly - this checks that on random data +// shaped like the router (2560 -> 256, top-10) and like the shared-expert gate (2560 -> 1). +#include "strata/kernels/bf16_gemv.hpp" +#include "strata/kernels/router_top10.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace { +void ck(cudaError_t e, const char* what) { + if (e != cudaSuccess) { std::fprintf(stderr, "%s: %s\n", what, cudaGetErrorString(e)); std::exit(2); } +} +uint16_t bf16(float f) { uint32_t i; std::memcpy(&i, &f, 4); return (uint16_t) ((i + ((i >> 16) & 1u) + 0x7FFFu) >> 16); } +} // namespace + +int main() { + const int N = 2560, E = 256, K = 10; + int bad = 0; + std::mt19937 rng(7); + std::normal_distribution nd(0.0f, 1.0f); + for (int n_out : {E, 1}) { + for (int T : {2, 3, 4, 6}) { + std::vector w((size_t) n_out * N); + std::vector x((size_t) T * N); + for (auto& v : w) v = bf16(nd(rng) * 0.05f); + for (auto& v : x) v = nd(rng); + uint16_t* dw; float *dx, *dy1, *dyn; + ck(cudaMalloc(&dw, w.size() * 2), "w"); ck(cudaMalloc(&dx, x.size() * 4), "x"); + ck(cudaMalloc(&dy1, (size_t) T * n_out * 4), "y1"); ck(cudaMalloc(&dyn, (size_t) T * n_out * 4), "yn"); + ck(cudaMemcpy(dw, w.data(), w.size() * 2, cudaMemcpyHostToDevice), "up w"); + ck(cudaMemcpy(dx, x.data(), x.size() * 4, cudaMemcpyHostToDevice), "up x"); + for (int t = 0; t < T; ++t) + strata::kernels::bf16_gemv_fp32_mmvf(dx + (size_t) t * N, dw, dy1 + (size_t) t * n_out, N, n_out, nullptr); + strata::kernels::bf16_gemv_fp32_mmvf_cols(dx, dw, dyn, N, n_out, T, nullptr); + ck(cudaDeviceSynchronize(), "sync"); + std::vector y1((size_t) T * n_out), yn((size_t) T * n_out); + ck(cudaMemcpy(y1.data(), dy1, y1.size() * 4, cudaMemcpyDeviceToHost), "down 1"); + ck(cudaMemcpy(yn.data(), dyn, yn.size() * 4, cudaMemcpyDeviceToHost), "down n"); + const bool same = std::memcmp(y1.data(), yn.data(), y1.size() * 4) == 0; + std::printf(" mmvf cols n_out %3d T %d %s\n", n_out, T, same ? "bit-identical" : "*** DIFFERS ***"); + if (!same) ++bad; + if (n_out == E) { + int *i1, *in_; float *w1, *wn; + ck(cudaMalloc(&i1, (size_t) T * K * 4), "i1"); ck(cudaMalloc(&in_, (size_t) T * K * 4), "in"); + ck(cudaMalloc(&w1, (size_t) T * K * 4), "w1"); ck(cudaMalloc(&wn, (size_t) T * K * 4), "wn"); + for (int t = 0; t < T; ++t) + strata::kernels::router_top10(dy1 + (size_t) t * E, 1, E, K, i1 + t * K, w1 + t * K, nullptr); + strata::kernels::router_top10(dy1, T, E, K, in_, wn, nullptr); + ck(cudaDeviceSynchronize(), "sync top"); + std::vector a((size_t) T * K), b((size_t) T * K); + std::vector fa((size_t) T * K), fb((size_t) T * K); + ck(cudaMemcpy(a.data(), i1, a.size() * 4, cudaMemcpyDeviceToHost), "ids1"); + ck(cudaMemcpy(b.data(), in_, b.size() * 4, cudaMemcpyDeviceToHost), "idsn"); + ck(cudaMemcpy(fa.data(), w1, fa.size() * 4, cudaMemcpyDeviceToHost), "w1"); + ck(cudaMemcpy(fb.data(), wn, fb.size() * 4, cudaMemcpyDeviceToHost), "wn"); + const bool s2 = a == b && std::memcmp(fa.data(), fb.data(), fa.size() * 4) == 0; + std::printf(" top10 n T %d %s\n", T, s2 ? "bit-identical" : "*** DIFFERS ***"); + if (!s2) ++bad; + cudaFree(i1); cudaFree(in_); cudaFree(w1); cudaFree(wn); + } + cudaFree(dw); cudaFree(dx); cudaFree(dy1); cudaFree(dyn); + } + } + std::printf(bad ? "route_window_parity: %d FAILURES\n" : "route_window_parity OK\n", bad); + return bad ? 1 : 0; +} diff --git a/src/platform/memory_test.cpp b/src/platform/memory_test.cpp index e5b4cde73d..7346189d66 100644 --- a/src/platform/memory_test.cpp +++ b/src/platform/memory_test.cpp @@ -13,7 +13,44 @@ #pragma comment(lib, "psapi.lib") #endif +namespace { +uint64_t gib(double g) { return (uint64_t) (g * 1073741824.0); } + +// #577: the file tier's unbuffered choice (true = the file cache keeps its reads, so they stay buffered) +int file_cache_keeps_cases() { + using strata::platform::file_cache_keeps; + struct Case { const char* what; double avail, arena, read; bool keeps; }; + const double experts = 71.7; // UD-Q4_K_XL's routed experts + const Case cases[] = { + // the reporter's 96 GB PC (83.0 GiB available at start): 0.1.38 passed the budget asked for and every + // shard's bytes (103.7 GiB) and went unbuffered + {"96 GB, 0.1.38's inputs (budget 72, all shards)", 83.0, 72.0, 103.7, false}, + {"96 GB, budget 72, before the copy (arena = all experts)", 83.0, experts, 0.0, true}, + {"96 GB, budget 72, copy built (48.94 resident)", 83.0 - 48.94, 0.0, experts - 48.94, true}, + {"96 GB, budget 40, before the copy", 83.0, 40.0, experts - 40.0, true}, + {"96 GB, budget 40, copy built", 83.0 - 40.0, 0.0, experts - 40.0, true}, + // #357/#362's low-RAM PCs stay unbuffered + {"64 GB, budget 40, before the copy", 55.0, 40.0, experts - 40.0, false}, + {"64 GB, budget 40, copy built", 15.0, 0.0, experts - 40.0, false}, + {"32 GB, budget 16, before the copy", 26.0, 16.0, experts - 16.0, false}, + {"32 GB, budget 16, copy built", 10.0, 0.0, experts - 16.0, false}, + {"32 GB, Q2_0 pack (34 GiB experts), budget 8", 26.0, 8.0, 34.0 - 8.0, false}, + {"nothing available", 0.0, 0.0, 1.0, false}, + {"arena larger than the RAM", 10.0, 20.0, 0.0, true}, + }; + int fail = 0; + for (const Case& c : cases) { + const bool got = file_cache_keeps(gib(c.avail), gib(c.arena), gib(c.read)); + std::printf("file_cache_keeps %-58s -> %s%s\n", c.what, got ? "buffered" : "unbuffered", + got == c.keeps ? "" : " <-- WRONG"); + fail |= got != c.keeps; + } + return fail; +} +} // namespace + int main() { + int fail_keeps = file_cache_keeps_cases(); const uint64_t bytes = 256ull << 20; void* p = std::malloc(bytes); if (p == nullptr) return 2; @@ -21,7 +58,7 @@ int main() { const strata::platform::LockResult r = strata::platform::lock_resident(p, bytes); std::printf("lock_resident: ok=%d locked=%llu MiB (%s)\n", (int) r.ok, (unsigned long long) (r.locked_bytes >> 20), r.note.c_str()); - int fail = !(r.ok && r.locked_bytes == bytes); + int fail = !(r.ok && r.locked_bytes == bytes) | fail_keeps; #if defined(_WIN32) PROCESS_MEMORY_COUNTERS pmc{}; GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof pmc); diff --git a/src/prefill/gemm.cu b/src/prefill/gemm.cu index bb6ef045b8..a35daeb2a3 100644 --- a/src/prefill/gemm.cu +++ b/src/prefill/gemm.cu @@ -3,8 +3,12 @@ #include "strata/kernels/dequant_bf16.hpp" #include +#include #include +#include +#include + #if defined(__HIPCC__) && defined(STRATA_HIPBLASLT_AVAILABLE) // The HIP compatibility shim maps CUDA shuffle spellings to Strata helpers. // hipBLASLt's public headers declare native HIP shuffle functions, so keep @@ -298,6 +302,8 @@ Gemm::~Gemm() { delete static_cast(hipblaslt_state_); #endif if (handle_) cublasDestroy((cublasHandle_t) handle_); + if (tc_w_) cudaFree(tc_w_); + if (tc_x_) cudaFree(tc_x_); if (!external_) { if (scratch_) cudaFree(scratch_); if (workspace_) cudaFree(workspace_); @@ -372,6 +378,89 @@ bool Gemm::init(void* stream, int64_t scratch_elems, std::string& err) { return true; } +#if !defined(__HIPCC__) +// ---- below sm_80: BF16 GEMMs without BF16 tensor cores (PR #655, #540, #395) ---------------------------------------- +// Turing and Volta have no BF16 tensor cores: cublasGemmEx on CUDA_R_16BF inputs falls back to a SIMT fp32 kernel +// (magma_sgemmEx), a fifth of a 4K prompt on an RTX 2080 Ti (#655) and ~10% of a V100's prompt (#540). BF16 -> FP16 +// is exact for every value inside FP16's normal range (the 7-bit mantissa fits in 10 bits); weights and normalized +// activations sit there. With the FP16 path, the weight is converted once per call and the activations in row +// slices, into the instance's own buffers, and the product runs as the FP16 GEMM (fp32 accumulate) on the tensor +// cores. Finite values beyond FP16's range are clamped to +-65504 (#540) instead of becoming Inf. A beta = 1 +// product (STRATA_PREFILL_BF16X2's remainder, ~2^-9 of the original, deep in FP16's subnormal band) keeps cuBLAS. +// Default: on for compute capability 7.0 - 7.4 (Volta: only the experimental STRATA_EXPERIMENTAL_SM60 build runs +// there), OFF for 7.5 (RTX 20 in the ready-made engine: the sums round differently, so it is opt-in until it has +// been gated). STRATA_BF16_TC=1 / =0 turns it on / off on any 7.x card (=2, a test mode: on any card). +// Pascal (6.x) has no tensor cores and cuBLAS has no BF16 GEMM for it (#395, measured NOT_SUPPORTED on a P40): both +// operands are widened to fp32 by an exact shift and the product is cublasSgemm with the same fp32 accumulator. +namespace { +__global__ void bf16_to_f16_kernel(const uint16_t* __restrict__ in, __half* __restrict__ out, int64_t n) { + const int64_t i = (int64_t) blockIdx.x * blockDim.x + threadIdx.x; + if (i < n) { + float f = __uint_as_float((uint32_t) in[i] << 16); + if (f > 65504.0f && !isinf(f)) f = 65504.0f; + else if (f < -65504.0f && !isinf(f)) f = -65504.0f; + out[i] = __float2half_rn(f); + } +} +void bf16_to_f16(const uint16_t* in, uint16_t* out, int64_t n, cudaStream_t st) { + if (n <= 0) return; + bf16_to_f16_kernel<<<(unsigned) ((n + 255) / 256), 256, 0, st>>>(in, reinterpret_cast<__half*>(out), n); +} +#if defined(STRATA_EXPERIMENTAL_SM60) // Pascal runs only the experimental build +__global__ void bf16_to_f32_kernel(const uint16_t* __restrict__ in, float* __restrict__ out, int64_t n) { + const int64_t i = (int64_t) blockIdx.x * blockDim.x + threadIdx.x; + if (i < n) out[i] = __uint_as_float((uint32_t) in[i] << 16); +} +void bf16_to_f32(const uint16_t* in, float* out, int64_t n, cudaStream_t st) { + if (n <= 0) return; + bf16_to_f32_kernel<<<(unsigned) ((n + 255) / 256), 256, 0, st>>>(in, out, n); +} +#endif +// The current device's compute capability as 10 * major + minor, per device (a layer split can mix cards); 0: not +// known, read as "not an old card" so a failed query keeps the cuBLAS BF16 call. +int current_cc() { + static std::atomic cc[64] = {}; + int dev = 0; + if (cudaGetDevice(&dev) != cudaSuccess || dev < 0 || dev >= 64) { cudaGetLastError(); return 0; } + int v = cc[dev].load(std::memory_order_relaxed); + if (v == 0) { + int major = 0, minor = 0; + if (cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev) != cudaSuccess || + cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, dev) != cudaSuccess) { + cudaGetLastError(); + return 0; + } + v = 10 * major + minor; + cc[dev].store(v, std::memory_order_relaxed); + } + return v; +} +// 0: cuBLAS's BF16 GEMM, 1: through FP16 (tensor cores), 2: through FP32 (Pascal) +int bf16_path() { + static const int forced = [] { + const char* v = std::getenv("STRATA_BF16_TC"); + return v != nullptr && v[0] != '\0' ? std::atoi(v) : -1; + }(); + const int cc = current_cc(); + if (forced == 2 && cc > 0) return 1; // a test mode: the FP16 path on any card (gemm_bf16_parity on sm_80+) + if (cc <= 0 || cc >= 80) return 0; + if (cc < 70) return 2; + if (forced >= 0) return forced != 0 ? 1 : 0; + return cc < 75 ? 1 : 0; +} +bool grow(uint16_t*& p, int64_t& have, int64_t want) { // `have`, `want`: 2-byte elements + if (have >= want) return true; + if (p) cudaFree(p); + p = nullptr; + have = 0; + if (cudaMalloc((void**) &p, (size_t) want * 2) != cudaSuccess) { cudaGetLastError(); p = nullptr; return false; } + have = want; + return true; +} +constexpr int64_t kXSliceElems = 16ll << 20; // 32 MiB of FP16 activations per slice (64 MiB as fp32) +} // namespace +#endif + void Gemm::bf16(const uint16_t* X, const uint16_t* W, float* Y, int64_t T, int64_t N, int64_t K, int64_t ldy, float beta) { if (T <= 0 || N <= 0) return; @@ -383,6 +472,41 @@ void Gemm::bf16(const uint16_t* X, const uint16_t* W, float* Y, int64_t T, int64 STRATA_ABSORB_HIPBLAS_STICKY("hipBLASLt bf16"); return; } +#endif +#if !defined(__HIPCC__) + if (const int path = K > 0 ? bf16_path() : 0; path == 1 && N > 1 && beta == 0.0f) { + // a single output row stays cuBLAS's GEMV, faster than the conversions; beta = 1: see above + const int64_t x_rows = std::max(1, std::min(T, kXSliceElems / K)); + if (grow(tc_w_, tc_w_elems_, N * K) && grow(tc_x_, tc_x_elems_, x_rows * K)) { + bf16_to_f16(W, tc_w_, N * K, (cudaStream_t) stream_); + for (int64_t t0 = 0; t0 < T; t0 += x_rows) { + const int64_t n = std::min(x_rows, T - t0); + bf16_to_f16(X + t0 * K, tc_x_, n * K, (cudaStream_t) stream_); + f16(tc_x_, tc_w_, Y + t0 * ldy, n, N, K, ldy, beta); + } + return; + } + } +#if defined(STRATA_EXPERIMENTAL_SM60) + else if (path == 2) { + // Pascal: fp32 copies (2 elements of the 2-byte buffers each). Every tile is a disjoint block of Y, so each + // gets the caller's beta. + const int64_t x_rows = std::max(1, std::min(T, kXSliceElems / K)); + if (grow(tc_w_, tc_w_elems_, 2 * N * K) && grow(tc_x_, tc_x_elems_, 2 * x_rows * K)) { + float* const wf = reinterpret_cast(tc_w_); + float* const xf = reinterpret_cast(tc_x_); + bf16_to_f32(W, wf, N * K, (cudaStream_t) stream_); + for (int64_t t0 = 0; t0 < T; t0 += x_rows) { + const int64_t n = std::min(x_rows, T - t0); + bf16_to_f32(X + t0 * K, xf, n * K, (cudaStream_t) stream_); + ck(cublasSgemm((cublasHandle_t) handle_, CUBLAS_OP_T, CUBLAS_OP_N, (int) N, (int) n, (int) K, &alpha, + wf, (int) K, xf, (int) K, &beta, Y + t0 * ldy, (int) ldy), + "cublasSgemm (bf16 on Pascal)"); + } + return; + } + } +#endif #endif // Column-major view: Y^T[N, T] = W[N, K] (stored K x N col-major, transposed) . X^T[K, T]. ck(cublasGemmEx((cublasHandle_t) handle_, CUBLAS_OP_T, CUBLAS_OP_N, (int) N, (int) T, (int) K, &alpha, W, diff --git a/src/prefill/gemm_bf16_parity.cu b/src/prefill/gemm_bf16_parity.cu new file mode 100644 index 0000000000..a94700517f --- /dev/null +++ b/src/prefill/gemm_bf16_parity.cu @@ -0,0 +1,155 @@ +// src/prefill/gemm_bf16_parity.cu - Gemm::bf16 against cuBLAS's own BF16 product (GPU, synthetic, no model). +// +// Below sm_80 (no BF16 tensor cores) Gemm::bf16 converts the weight and the activations to FP16 and runs the FP16 +// tensor-core GEMM (Volta by default, Turing with STRATA_BF16_TC=1) or widens them to fp32 (Pascal); everywhere else +// it is the cuBLAS BF16 call itself. (On Pascal the cuBLAS BF16 reference itself may be refused: the test then fails +// at the reference, which says so.) This +// checks the result against cublasGemmEx on the same BF16 inputs, within fp32-accumulation rounding, over the prompt +// path's shapes: the hyper-connection down / up projections, the router and indexer rows, the PLE value matrix, a T +// large enough to slice the activations, beta = 1 accumulation (the bf16x2 low parts) and an output row stride wider +// than N. --bench adds the time of each against the cuBLAS BF16 product. +#include "strata/prefill/gemm.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +uint16_t to_bf16(float f) { // round to nearest even + uint32_t u; + std::memcpy(&u, &f, 4); + u += 0x7fffu + ((u >> 16) & 1u); + return (uint16_t) (u >> 16); +} + +bool ok_or(cudaError_t e, const char* what) { + if (e != cudaSuccess) std::printf("FAIL: %s: %s\n", what, cudaGetErrorString(e)); + return e == cudaSuccess; +} + +struct Shape { + int64_t T, N, K, ldy; + const char* what; +}; + +} // namespace + +int main(int argc, char** argv) { + const bool bench = argc > 1 && std::string(argv[1]) == "--bench"; + int dev = 0, maj = 0, min = 0; + cudaGetDevice(&dev); + cudaDeviceGetAttribute(&maj, cudaDevAttrComputeCapabilityMajor, dev); + cudaDeviceGetAttribute(&min, cudaDevAttrComputeCapabilityMinor, dev); + std::printf("gemm_bf16_parity: compute capability %d.%d\n", maj, min); + + cudaStream_t st = nullptr; + if (!ok_or(cudaStreamCreate(&st), "stream")) return 1; + strata::prefill::Gemm gemm; + std::string err; + if (!gemm.init(st, 0, err)) { std::printf("FAIL: %s\n", err.c_str()); return 1; } + cublasHandle_t h = nullptr; + if (cublasCreate(&h) != CUBLAS_STATUS_SUCCESS) { std::printf("FAIL: cublasCreate\n"); return 1; } + cublasSetStream(h, st); + + const Shape shapes[] = { + {4096, 320, 10240, 0, "hc down (activations sliced)"}, + {4096, 10240, 320, 0, "hc up"}, + {777, 2560, 2560, 0, "PLE value"}, + {512, 512, 2560, 0, "router / indexer q"}, + {300, 4, 10240, 0, "hc inject"}, + {2048, 1, 2560, 0, "single row"}, + {333, 128, 2560, 136, "row stride wider than N"}, + }; + std::mt19937 rng(1234); + std::normal_distribution nd(0.0f, 1.0f); + int failures = 0; + for (const Shape& s : shapes) { + const int64_t ldy = s.ldy > 0 ? s.ldy : s.N; + std::vector x((size_t) (s.T * s.K)), xlo(x.size()), w((size_t) (s.N * s.K)); + for (size_t i = 0; i < x.size(); ++i) { + const float v = 3.0f * nd(rng); // activations: normalized, a few units + x[i] = to_bf16(v); + xlo[i] = to_bf16(1e-3f * nd(rng)); // a bf16x2 low part + } + for (auto& v : w) v = to_bf16(0.02f * nd(rng)); + uint16_t *dx = nullptr, *dxlo = nullptr, *dw = nullptr; + float *dy = nullptr, *dr = nullptr; + const size_t ybytes = (size_t) (s.T * ldy) * 4; + bool ok = ok_or(cudaMalloc((void**) &dx, x.size() * 2), "x") && ok_or(cudaMalloc((void**) &dxlo, x.size() * 2), "xlo") && + ok_or(cudaMalloc((void**) &dw, w.size() * 2), "w") && ok_or(cudaMalloc((void**) &dy, ybytes), "y") && + ok_or(cudaMalloc((void**) &dr, ybytes), "ref"); + if (!ok) return 1; + cudaMemcpy(dx, x.data(), x.size() * 2, cudaMemcpyHostToDevice); + cudaMemcpy(dxlo, xlo.data(), xlo.size() * 2, cudaMemcpyHostToDevice); + cudaMemcpy(dw, w.data(), w.size() * 2, cudaMemcpyHostToDevice); + cudaMemset(dy, 0, ybytes); + cudaMemset(dr, 0, ybytes); + + // the product under test: X . W^T, then the low part added with beta = 1 + gemm.bf16(dx, dw, dy, s.T, s.N, s.K, ldy); + gemm.bf16(dxlo, dw, dy, s.T, s.N, s.K, ldy, 1.0f); + // the reference: cuBLAS on the BF16 inputs, the same two calls + const float one = 1.0f, zero = 0.0f; + auto ref = [&](const uint16_t* X, float beta) { + return cublasGemmEx(h, CUBLAS_OP_T, CUBLAS_OP_N, (int) s.N, (int) s.T, (int) s.K, &one, dw, CUDA_R_16BF, + (int) s.K, X, CUDA_R_16BF, (int) s.K, &beta, dr, CUDA_R_32F, (int) ldy, + CUBLAS_COMPUTE_32F, CUBLAS_GEMM_DEFAULT); + }; + if (ref(dx, zero) != CUBLAS_STATUS_SUCCESS || ref(dxlo, one) != CUBLAS_STATUS_SUCCESS) { + std::printf("FAIL: reference cublasGemmEx\n"); + return 1; + } + if (!ok_or(cudaStreamSynchronize(st), "sync")) return 1; + std::vector y((size_t) (s.T * ldy)), r(y.size()); + cudaMemcpy(y.data(), dy, ybytes, cudaMemcpyDeviceToHost); + cudaMemcpy(r.data(), dr, ybytes, cudaMemcpyDeviceToHost); + double worst = 0.0, mag = 1e-30; + int bad = 0; + for (int64_t t = 0; t < s.T; ++t) + for (int64_t c = 0; c < s.N; ++c) { + const float a = y[(size_t) (t * ldy + c)], b = r[(size_t) (t * ldy + c)]; + if (!std::isfinite(a)) ++bad; + worst = std::max(worst, (double) std::fabs(a - b)); + mag = std::max(mag, (double) std::fabs(b)); + } + // the untouched columns of a wider row stride stay as they were + for (int64_t t = 0; t < s.T && ldy > s.N; ++t) + for (int64_t c = s.N; c < ldy; ++c) + if (y[(size_t) (t * ldy + c)] != 0.0f) ++bad; + const bool pass = bad == 0 && worst <= 1e-4 * mag; + if (!pass) ++failures; + std::printf(" %-32s T %5lld N %5lld K %5lld ldy %5lld: worst |diff| %.3e of max |ref| %.3e (rel %.2e) %s\n", + s.what, (long long) s.T, (long long) s.N, (long long) s.K, (long long) ldy, worst, mag, worst / mag, + pass ? "pass" : "FAIL"); + + if (bench) { + auto time = [&](auto&& f) { + for (int i = 0; i < 3; ++i) f(); + cudaStreamSynchronize(st); + const auto t0 = std::chrono::steady_clock::now(); + for (int i = 0; i < 20; ++i) f(); + cudaStreamSynchronize(st); + return std::chrono::duration(std::chrono::steady_clock::now() - t0).count() / 20; + }; + const double us_gemm = time([&] { gemm.bf16(dx, dw, dy, s.T, s.N, s.K, ldy); }); + const double us_ref = time([&] { ref(dx, zero); }); + std::printf(" bench: Gemm::bf16 %9.1f us cuBLAS BF16 %9.1f us (%.2fx)\n", us_gemm, us_ref, + us_ref / us_gemm); + } + cudaFree(dx); cudaFree(dxlo); cudaFree(dw); cudaFree(dy); cudaFree(dr); + } + cublasDestroy(h); + cudaStreamDestroy(st); + std::printf("gemm_bf16_parity: %d failures\n", failures); + return failures == 0 ? 0 : 1; +} diff --git a/src/prefill/kernels.cu b/src/prefill/kernels.cu index ed78013dfa..4ad67b1ad0 100644 --- a/src/prefill/kernels.cu +++ b/src/prefill/kernels.cu @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -956,6 +957,19 @@ __global__ void copy_i32_kernel(int32_t* __restrict__ dst, const int32_t* __rest dst[i] = src[i]; } } // namespace +namespace { +__global__ void copy_f4_kernel(float4* __restrict__ dst, const float4* __restrict__ src, int64_t n4) { + for (int64_t i = (int64_t) blockIdx.x * blockDim.x + threadIdx.x; i < n4; i += (int64_t) gridDim.x * blockDim.x) + dst[i] = src[i]; +} +} // namespace +void copy_f32_wide(float* dst, const float* src, int64_t n, void* stream) { + if (n <= 0) return; + const int64_t n4 = n / 4, b = std::min((n4 + 255) / 256, 4096); + if (n4 > 0) copy_f4_kernel<<<(unsigned) b, 256, 0, (cudaStream_t) stream>>>((float4*) dst, (const float4*) src, n4); + if (n % 4) copy_i32((int32_t*) dst + n4 * 4, (const int32_t*) src + n4 * 4, n % 4, stream); + check("copy_f32_wide"); +} void copy_i32(int32_t* dst, const int32_t* src, int64_t n, void* stream) { if (n <= 0) return; const int64_t b = (n + 255) / 256; diff --git a/src/prefill/moe_fused.cu b/src/prefill/moe_fused.cu index b5f9a1c80b..19d1c949c0 100644 --- a/src/prefill/moe_fused.cu +++ b/src/prefill/moe_fused.cu @@ -364,6 +364,11 @@ const DevInfo& dev_info() { DevInfo& d = g_dev[dev & 31]; if (d.done) return d; d.done = true; +#if defined(STRATA_HIP_GFX906) + // gfx906 reports compute capability 9.0 through HIP, but the mma.sync bodies above are CUDA sm_80+ only and + // empty in a hipcc build: never available here (the prompt path keeps its own expert GEMMs) + return d; +#else int major = 0, minor = 0; cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev); cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, dev); @@ -385,6 +390,7 @@ const DevInfo& dev_info() { cudaOccupancyMaxActiveBlocksPerMultiprocessor(&d.occ_d, expert_kernel, THREADS, smem_bytes(false)); if (d.occ_gu < 1 || d.occ_d < 1) d.cc = 0; // does not fit this card: the MMQ path cudaGetLastError(); +#endif return d; } diff --git a/src/prefill/moe_fused_iq.cu b/src/prefill/moe_fused_iq.cu index 52476c87aa..b2d990869e 100644 --- a/src/prefill/moe_fused_iq.cu +++ b/src/prefill/moe_fused_iq.cu @@ -516,6 +516,7 @@ struct DevInfo { std::mutex g_mu; DevInfo g_dev[32]; +#if !defined(STRATA_HIP_GFX906) template bool setup_ww(int& occ) { cudaFuncAttributes fa{}; if (cudaFuncGetAttributes(&fa, native_kernel) != cudaSuccess || fa.ptxVersion < 80) return false; @@ -530,6 +531,7 @@ template bool setup_ww(int& occ) { return true; } template bool setup_one(int& occ) { return setup_ww(occ) && setup_ww(occ); } +#endif const DevInfo& dev_info() { int dev = 0; @@ -538,6 +540,11 @@ const DevInfo& dev_info() { DevInfo& d = g_dev[dev & 31]; if (d.done) return d; d.done = true; +#if defined(STRATA_HIP_GFX906) + // gfx906 reports compute capability 9.0 through HIP, but the mma.sync bodies above are CUDA sm_80+ only and + // empty in a hipcc build: never available here (the prompt path keeps its own expert GEMMs) + return d; +#else int major = 0; cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev); cudaDeviceGetAttribute(&d.sms, cudaDevAttrMultiProcessorCount, dev); @@ -548,6 +555,7 @@ const DevInfo& dev_info() { setup_one(occ) && setup_one(occ); d.occ = d.ok ? occ : 1; cudaGetLastError(); +#endif return d; } diff --git a/src/prefill/prefill.cpp b/src/prefill/prefill.cpp index 5d46e4bfb9..e76fb6ddc1 100644 --- a/src/prefill/prefill.cpp +++ b/src/prefill/prefill.cpp @@ -92,6 +92,7 @@ constexpr int64_t C = 10240, ZV = 6144, HV = 48; // plan v0.3 P6: staging holds the largest blob of the pack (a native pack's blobs differ per layer) inline int64_t MAXBLOB() { return (int64_t) strata::kernels::cpu::expert_layout().max_blob; } constexpr int STAGE = 8; // host->device expert staging ring (chunks below stream_all_min()) +constexpr int kSplitHelpRing = 48; // layer split help: the helper stage's ring slots it streams through (at most) // Step 3: from this chunk size on, every non-resident expert of every layer streams in a fixed order through a // ring_slots()-slot ring (nearly all 512 are routed at such a chunk), so the copy engine keeps working through the // attention halves instead of waiting for each layer's routing. @@ -109,7 +110,20 @@ double g_pinned_share = 1.0; // 8192-token chunks: 96 slots 1153 tok/s, 384 1294 (the next layer's experts arrive during its attention half) - // and 96 when a large share goes through host copies (IQ3_S on 64 GB, a third unpinned: 96 slots 1216, 256 1070 - // the host copies are the limit and the bigger ring only takes cache slots). STRATA_PREFILL_RING overrides. +// +// Those are slots on the pack they were measured on, and a slot is one WHOLE expert blob - a Q2_0 blob is +// 1,382,400 B, so 384 slots is 506 MiB. On a pack with bigger blobs the same slot count is a different amount of +// memory: 1,912 MiB on Q8_0 (5,222,400 B), which is more than half of an 8 GB card's expert cache and what held +// that card at a 1,024-token chunk when its own buffers would have fitted 6,144. Measured on the 4-way rig, one +// env var and nothing else: chunk 1,024 -> 6,144, prefill 87 -> 402 tok/s. So the budget is kept in BYTES and the +// slot count is derived from the pack (`ring_budget_slots`); Q2_0 still resolves to exactly 1024 (fused) and 384, +// so the pack all of this was tuned on does not move. int g_ring_override = 0; // #340: set by a layer split (Prefill::set_ring_override); 0 = the rule below +// #583 (0.1.39b): the auto chunk scan's byte-budget ring (Prefill::set_ring_budget), for requests whose chunk is above +// g_ring_small_max - the chunk 0.1.39's rule would have picked. A prompt that fits that chunk keeps 0.1.39's ring: +// on one chunk the smaller ring only slowed it (RTX 5070, 4K prompts: Coder -15%, IQ3_S -5%, IQ3_XXS -2%). +int g_ring_budget = 0; +int64_t g_ring_small_max = 0; // #136: the fused experts (STRATA_PF_FUSED=1) launch on a batch of a layer's streamed experts at once, so the ring // should hold a whole layer's (~460 of 512 on Q2_0): with 384 slots a layer's last batch waits for slots its own // first batch frees. Measured on the 5070, Q2_0, the 4K / 32K code-agent prompts (one run each): fused at 384 slots @@ -137,6 +151,30 @@ inline bool fused_ring() { // the largest ring: 512 slots; 1024 with the Q2_0 pack's fused experts (P3's smaller buffers, measured there) - the // native packs' fused layers were measured at 512 inline int ring_cap() { return fused_ring() && !strata::kernels::cpu::expert_layout().native ? RING_MAX : 512; } +// The ring's budget in bytes: the measured slot counts above, at the blob size they were measured with. It is +// bytes and not slots because a slot is one whole blob and the blob is the pack's - see the note above. +inline constexpr uint64_t Q2_0_BLOB = 1382400ull; // the blob of the pack the ring was tuned on +inline uint64_t ring_bytes() { + const uint64_t slots = fused_ring() ? 1024ull : 384ull; // #136: a fused ring holds two layers' experts + return (g_pinned_share >= 0.9 ? slots : slots / 4) * Q2_0_BLOB; +} +// ...and what that buys on THIS pack, never past ring_cap(): the slot count `init` lays out, and what the auto +// chunk scan treats as a full ring. A pack whose blobs are larger than Q2_0's gets fewer slots for the same +// bytes, which is the point - the ring competes with the expert cache for the same VRAM. +// 0.1.39b: on by default (#583); STRATA_RING_BYTES=0 restores 0.1.39's ring, loan and auto chunk list. It moves the +// prompt path's loan on a native pack (fewer ring slots, the rest kept as cache slots, a larger auto chunk), so a long +// prompt's experts are read through a different mix of resident and streamed groups and its bits differ from 0.1.39's +// (RTX 5070, IQ3_XXS, 32K prompt: +14% to +26%; teacher-forced against the FP16 prompt path in the same band). +inline bool ring_bytes_on() { + static const bool on = [] { const char* v = std::getenv("STRATA_RING_BYTES"); return v == nullptr || v[0] != '0'; }(); + return on; +} +inline int ring_budget_slots() { + const int64_t per = MAXBLOB(); + const int64_t n = per > 0 ? (int64_t) (ring_bytes() / (uint64_t) per) : 0; + const int cap = ring_cap(); + return (int) (n <= 0 ? 0 : (n > cap ? cap : n)); +} inline int ring_slots(size_t T) { const char* v = std::getenv("STRATA_PREFILL_RING"); #if defined(STRATA_USE_HIP) @@ -148,8 +186,14 @@ inline int ring_slots(size_t T) { }(); if (!v && g_ring_override <= 0 && wmma) return (int64_t) T >= stream_all_min() ? 96 : STAGE; #endif - const int pinned_ring = fused_ring() ? 1024 : 384; - const int r = v ? std::atoi(v) : g_ring_override > 0 ? g_ring_override : (g_pinned_share >= 0.9 ? pinned_ring : 96); + // the byte budget as this pack's slots: 1024 fused / 384 not on Q2_0, fewer on a pack with bigger blobs. The + // unpinned arm stays a slot count (96): it was measured where the host copies are the limit, and there the ring + // is not what is competing for VRAM. + // 0.1.39's ring (1024 fused / 384 pinned, 96 when a large share goes through host copies), and the #583 byte + // budget the auto scan chose for chunks past the size 0.1.39's rule would have picked (set_ring_budget) + const int pinned_ring = g_pinned_share >= 0.9 ? (fused_ring() ? 1024 : 384) : 96; + const bool budget = ring_bytes_on() && g_ring_budget > 0 && (int64_t) T > g_ring_small_max; + const int r = v ? std::atoi(v) : g_ring_override > 0 ? g_ring_override : budget ? g_ring_budget : pinned_ring; if (v && r == STAGE) return STAGE; // Explicit opt-in to routed-only staging, including large chunks. const int big = r < 16 ? 16 : r > ring_cap() ? ring_cap() : r; return (int64_t) T >= stream_all_min() ? big : STAGE; @@ -386,6 +430,21 @@ struct PeerPrefill { int32_t *src = nullptr, *bounds = nullptr, *ident = nullptr; uint8_t *grp_gu = nullptr, *grp_d = nullptr; std::unique_ptr ctx; + mmq::Context* run_ctx = nullptr; // the context its products run in: ctx, or a layer split helper stage's + // Without P2P (a layer split's helper) the activations and the result rows go through MAPPED pinned host buffers: the device that needs them reads them with a copy kernel. A copy-engine + // upload would queue behind the expert blobs that device's streams already hold (up to a ring of them, ~70 us + // each - the grouping tables' reason for mapped memory too), and cudaMemcpyPeerAsync would be staged by the + // driver. `back_at` / `back_rows`: the rows this layer sends back. + bool p2p = true; + // The row table and the group bounds go the same way: the primary's are pinned for its own device only (or + // pageable), and a copy from them on the peer's stream would hold up the host until that stream is idle. + float* host_x = nullptr; // T x N activations (mapped, portable) + float* host_rows = nullptr; // the peer's result rows (mapped, portable) + int32_t* host_src = nullptr; // the peer's rows' tokens (mapped, portable) + int32_t* host_bounds = nullptr; // its group bounds (mapped, portable) + size_t host_x_cap = 0, host_rows_cap = 0, host_src_cap = 0; // elements + static constexpr size_t kHostBounds = 2 * (NE + NE / 16 + 2) + 64; + int64_t back_at = 0, back_rows = 0; std::vector bounds_host; std::vector owned; int64_t layers = 0, experts = 0, rows = 0, over_cap = 0; // stats @@ -409,6 +468,10 @@ struct PeerPrefill { if (s) cudaStreamDestroy(s); cudaSetDevice(prev); if (ev_in) cudaEventDestroy(ev_in); + if (host_x) cudaFreeHost(host_x); + if (host_rows) cudaFreeHost(host_rows); + if (host_src) cudaFreeHost(host_src); + if (host_bounds) cudaFreeHost(host_bounds); } }; @@ -496,6 +559,9 @@ struct Prefill::Impl { int32_t* tok_dev = nullptr; std::vector tok_host; std::unique_ptr pp; // multi-GPU: the peer GPU's expert share (set_peer) + // layer split: the next stage's GPU as a stream-only peer (set_stage_helper); its buffers are that stage's own + // prompt buffers, bound per prompt, and it takes the place of `pp` for a run it helps + std::unique_ptr help_pp; }; namespace { @@ -1051,6 +1117,137 @@ bool Prefill::draft_kv(core::MtpDrafter& mtp, const float* R_rows, const int32_t (long long) B, ms_idle, ms_since(tl0), ms_since(t0)); return true; } +namespace { +// Layer split: STRATA_PREFILL_HELP=1 lends the idle stage's GPU (opt-in: the helped rows are computed in another MMQ +// grouping and round differently from the default, like STRATA_SPLIT_OWN). Unset or 0: every stage's prompt experts on +// its own GPU, as before. +bool split_help_env() { + static const bool v = [] { const char* e = std::getenv("STRATA_PREFILL_HELP"); return e != nullptr && std::atoi(e) != 0; }(); + return v; +} +// The share of a stage's streamed experts the idle stage's GPU takes for a one-chunk prompt of T tokens (0: none). +// The streamed bytes are the same at any T; the hand-over (the activations there, the rows back) grows with T and is +// paid per layer whatever the share. Measured on 2x RTX 3090 (UD-Q4_K_XL, PCIe 4.0 x16 each, no P2P), fixed shares +// 0.2 / 0.3 / 0.4 against none: 1.5K tokens 749 / 820 / 908 (none 675), 3K 1335 / 1437 / 1328 (1262), 4K 1622 / 1620 / +// 1440 (1609), 6K 1811 / 1660 / 1471 (2038), 8K 1895 / 1804 / 1663 (2230) - the best share falls with T and from ~4K +// none pays, which this rule follows (0.41 at 1.5K, 0.32 at 3K, off from ~3.3K). +// STRATA_PREFILL_HELP_FRAC sets the share for every T. +double split_help_frac(int64_t T) { + static const double fixed = [] { + const char* e = std::getenv("STRATA_PREFILL_HELP_FRAC"); + return e ? std::clamp(std::atof(e), 0.0, 1.0) : -1.0; + }(); + if (fixed >= 0.0) return fixed; + const double f = std::min(0.45, 0.5 - (double) T / 16384.0); + return f >= 0.3 ? f : 0.0; +} +} // namespace + +bool Prefill::set_stage_helper(Prefill* helper, std::string& err) { + Impl& m = *impl_; + m.help_pp.reset(); + if (helper == nullptr || helper == this || !split_help_env()) return true; + const Impl& h = *helper->impl_; + if (m.device < 0 || h.device < 0 || h.device == m.device || !mmq_plan().any) return true; + auto pp = std::make_unique(); + pp->peer = nullptr; // stream-only: it holds no experts, it streams a share of this stage's + pp->compact = true; + pp->p2p = false; // the mapped host route, whether or not the cards could reach each other (measured there) + pp->out_pipe = true; + bool ok; + { + const core::OnDevice on(m.device); + ok = cudaEventCreateWithFlags(&pp->ev_in, cudaEventDisableTiming) == cudaSuccess; + } + int prev = 0; + cudaGetDevice(&prev); + pp->dev = h.device; + cudaSetDevice(pp->dev); + ok = ok && cudaStreamCreateWithFlags(&pp->s, cudaStreamNonBlocking) == cudaSuccess && + cudaStreamCreateWithFlags(&pp->s_out, cudaStreamNonBlocking) == cudaSuccess && + cudaStreamCreateWithFlags(&pp->s_cp, cudaStreamNonBlocking) == cudaSuccess && + cudaEventCreateWithFlags(&pp->ev_done, cudaEventDisableTiming) == cudaSuccess; + for (int i = 0; ok && i < PeerPrefill::kGrpEv; ++i) + ok = cudaEventCreateWithFlags(&pp->ev_grp[i], cudaEventDisableTiming) == cudaSuccess; + for (int b = 0; ok && b < 2; ++b) ok = cudaEventCreateWithFlags(&pp->ev_dm[b], cudaEventDisableTiming) == cudaSuccess; + // the ring's events; its slots are the helper stage's own ring, bound per prompt (bind_stage_helper) + pp->pcopied.assign((size_t) kSplitHelpRing, nullptr); + pp->pused.assign((size_t) kSplitHelpRing, nullptr); + for (int i = 0; ok && i < kSplitHelpRing; ++i) + ok = cudaEventCreateWithFlags(&pp->pcopied[(size_t) i], cudaEventDisableTiming) == cudaSuccess && + cudaEventCreateWithFlags(&pp->pused[(size_t) i], cudaEventDisableTiming) == cudaSuccess; + cudaSetDevice(prev); + if (!ok) { err = "prefill: the layer split's prompt help (streams and events)"; return false; } + helper_ = helper; + m.help_pp = std::move(pp); + return true; +} + +// A one-chunk prompt of T tokens on a layer split: binds the stream-only peer to the helper stage's prompt buffers +// (that stage idles until this one hands its rows over). False when it cannot help this prompt. +bool Prefill::bind_stage_helper(int64_t T) { + Impl& m = *impl_; + if (!m.help_pp || helper_ == nullptr || m.pp) return false; + const double frac = split_help_frac(T); + if (frac <= 0.0 || T < stream_all_min() || m.src == nullptr || m.ring <= STAGE || fused_ring()) return false; + const strata::kernels::cpu::ExpertLayout& lay = strata::kernels::cpu::expert_layout(); + for (int64_t l = stage_lb_; l < stage_le_; ++l) { + if (!(mmq_plan().any && mmq_plan().layer[(size_t) l])) return false; // the peer computes on MMQ only + if (lay.native && fused::native_supported(lay.fmt[(size_t) l].gu_type, lay.fmt[(size_t) l].d_type)) return false; + } + Impl& h = *helper_->impl_; + // the helper's buffers must be laid out for this chunk (the serve path lends every stage's at once) + if (h.T < T || h.mixed == nullptr || h.Xq == nullptr || h.Hq == nullptr || h.GU == nullptr || h.H == nullptr || + h.Dm == nullptr || h.src_dev == nullptr || h.ids_identity == nullptr || h.bounds_dev == nullptr || + h.grp_gu == nullptr || h.grp_d == nullptr || !h.mmq_ctx || h.ring <= STAGE) + return false; + PeerPrefill& P = *m.help_pp; + P.T_max = T; + P.cap_rows = T * K; + P.G = T; // one expert never has more rows than the chunk has tokens + P.mixed = h.mixed; + P.Xq_g = h.Xq; // the helper's per-layer buffers hold T * K rows: a group's T fit + P.Hq_g = h.Hq; + P.GU_g = h.GU; + P.H_g = h.H; + P.Dm_b[0] = h.Dm; + P.Dm_b[1] = h.Dm + (size_t) T * N; + P.src = h.src_dev; + P.ident = h.ids_identity; + P.bounds = h.bounds_dev; + P.grp_gu = h.grp_gu; + P.grp_d = h.grp_d; + P.run_ctx = h.mmq_ctx.get(); + P.RP = std::min(h.ring, kSplitHelpRing); + P.pstage.assign(h.stage_dev, h.stage_dev + P.RP); + P.plive.assign((size_t) P.RP, 0); + P.dm_live[0] = P.dm_live[1] = false; + P.out_pending = false; + P.ps_frac = frac; + if (!P.p2p) { // grown once to the largest chunk it helped + auto grow = [](auto*& p, size_t& cap, size_t n) { + if (cap >= n) return true; + if (p) cudaFreeHost(p); + p = nullptr; + cap = 0; + if (cudaHostAlloc((void**) &p, n * sizeof(*p), cudaHostAllocPortable | cudaHostAllocMapped) != cudaSuccess) { + cudaGetLastError(); + p = nullptr; + return false; + } + cap = n; + return true; + }; + size_t bounds_cap = P.host_bounds ? PeerPrefill::kHostBounds : 0; + if (!grow(P.host_x, P.host_x_cap, (size_t) T * N) || !grow(P.host_rows, P.host_rows_cap, (size_t) T * K * N) || + !grow(P.host_src, P.host_src_cap, (size_t) T * K) || !grow(P.host_bounds, bounds_cap, PeerPrefill::kHostBounds)) + return false; + } + const core::OnDevice on(P.dev); + mmq::iota(P.ident, T * K, P.s); // the helper's row table: a refill may have overwritten its slots + return true; +} + bool Prefill::set_peer(core::PeerExperts* peer, int64_t cap_rows, std::string& err) { Impl& m = *impl_; if (peer == nullptr || !peer->valid()) { m.pp.reset(); return true; } @@ -1133,6 +1330,7 @@ bool Prefill::set_peer(core::PeerExperts* peer, int64_t cap_rows, std::string& e pp->grp_d = (uint8_t*) take(MMQ_GROUP * mp.d_max + MMQ_TAIL); if (ok) { pp->ctx = std::make_unique(); + pp->run_ctx = pp->ctx.get(); mmq::iota(pp->ident, pp->cap_rows, pp->s); ok = cudaStreamSynchronize(pp->s) == cudaSuccess; } @@ -1151,6 +1349,10 @@ bool Prefill::set_peer(core::PeerExperts* peer, int64_t cap_rows, std::string& e void Prefill::set_pinned_share(double share) { g_pinned_share = share; } void Prefill::set_ring_override(int slots) { g_ring_override = slots > 0 ? slots : 0; } +void Prefill::set_ring_budget(int slots, int64_t small_max) { + g_ring_budget = slots > 0 ? slots : 0; + g_ring_small_max = small_max > 0 ? small_max : 0; +} double Prefill::pinned_share() { return g_pinned_share; } uint64_t Prefill::bytes_needed(const core::ModelGeometry& g, const core::SessionState& ss, int64_t chunk) { @@ -1162,7 +1364,19 @@ uint64_t Prefill::bytes_needed(const core::ModelGeometry& g, const core::Session o.take((size_t) GEMM_SCRATCH, ok); o.take(GEMM_WS, ok); auto f = [&](size_t n) { o.take(n, ok); }; - f(T * N); f(T * D); f(T * D); o.take(T * D, ok); f(T * LR); o.take(T * LR, ok); + // `carve`'s order, buffer for buffer: emb, R, xn, grs, xn16, lo, lo16, gated, inj, mixed, mixed_bf, mixed_h, + // bo. This counted `xn` unconditionally (carve takes it only under STRATA_GR_UNFUSED) and never counted + // `grs`. Net over-count T*(D-HC)*4 bytes: 42 MB at a 1024-token chunk, 252 MB (48 Q8_0 slots) at 6144 - the + // prompt path was told it had less room than it did. Safe - the direction is over-estimating, and `take` + // still bounds-checks - but it under-sizes every loan, so every chunk the scan picks is one step smaller. + if (ring_bytes_on()) { + f(T * N); f(T * D); + if (gr_unfused()) f(T * D); + f(T * HC); + } else { // STRATA_RING_BYTES=0: 0.1.39's count + f(T * N); f(T * D); f(T * D); + } + o.take(T * D, ok); f(T * LR); o.take(T * LR, ok); f(T * D); f(T * HC); f(T * N); o.take(T * N, ok); o.take(T * N, ok); f(T * N); if (bf16x2_hc()) { o.take(T * D, ok); o.take(T * LR, ok); } if (bf16x2()) o.take(T * N, ok); @@ -1190,6 +1404,33 @@ uint64_t Prefill::bytes_needed(const core::ModelGeometry& g, const core::Session return o.used + (8u << 20); // alignment slack } +uint64_t Prefill::bytes_needed_no_ring(const core::ModelGeometry& g, const core::SessionState& ss, int64_t chunk) { + return bytes_needed(g, ss, chunk) - (uint64_t) ring_slots((size_t) chunk) * (uint64_t) MAXBLOB(); +} + +int64_t Prefill::ring_default_slots() { + const int r = g_pinned_share >= 0.9 ? (fused_ring() ? 1024 : 384) : 96; + return (int64_t) std::min(r, ring_cap()); +} + +int64_t Prefill::ring_cap_for(int64_t old_chunk) { + // 0.1.39b (#583, measured on the RTX 5070): giving up ring slots for a bigger chunk paid where 0.1.39's ring held + // the chunk at 4096 or less (IQ3_XXS 32K prompts +14% to +26%: 4096/384 -> 6656/56) and lost where 0.1.39 already + // read 6144-token chunks (the Coder: 6144/384 -> 7936/199, 32K -12%). From kKeepRingChunk on the ring keeps its + // 0.1.39 size and the scan only looks for a bigger chunk next to it (IQ3_XXS unpinned 6144/96 -> 6912/96: +9%). + constexpr int64_t kKeepRingChunk = 6144; + return old_chunk >= kKeepRingChunk ? ring_default_slots() : ring_max_slots(); +} + +// the unpinned arm keeps its measured 96 (the PR's rule; the byte budget would have been ~49 slots on IQ3_S) +int64_t Prefill::ring_max_slots() { + return g_pinned_share >= 0.9 ? (int64_t) ring_budget_slots() : (int64_t) std::min(96, ring_cap()); +} + +int64_t Prefill::ring_slots_for(int64_t chunk) { return ring_slots((size_t) chunk); } + +bool Prefill::ring_bytes_enabled() { return ring_bytes_on(); } + namespace { const core::WeightRef* need(const core::LayerView& v, const char* suffix, std::string& err) { @@ -1301,7 +1542,7 @@ struct PeTimer { }; } // namespace -bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& err) { +bool Prefill::run_impl(const int64_t* tokens, int64_t n, int64_t pos0, std::string& err) { err.clear(); Impl& m = *impl_; const core::OnDevice on_device(m.device); @@ -1309,11 +1550,8 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e core::SessionState& ss = *m.ss; const auto t_start = Clock::now(); const int64_t LB = stage_lb_, LE = stage_le_; - // the next stage reads chunk c on a thread while this one reads chunk c + 1 (declared first: an early return - // waits for it before anything it reads goes away) - std::string next_err; - std::future next_run; - int hand_buf = 0; + // The direct successor's future lives on the Prefill object. Intermediate + // stages therefore do not drain the complete remaining GPU chain here. double host_sync_ms = 0, host_chunk_ms = 0, host_setup_ms = 0; // STRATA_PREFILL_TIMING: the host's share strata::kernels::QsaShapes s = strata::kernels::qsa_real_shapes(); s.n_head = g.n_head; s.n_head_kv = g.n_head_kv; s.head_dim = g.head_dim; s.idx_n_head = g.idx_q_heads; @@ -1321,6 +1559,24 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e const uint64_t gdn_floats = (uint64_t) g.ssm_state_size * g.ssm_v_heads * g.ssm_state_size + (uint64_t) g.ssm_conv_channels * (g.ssm_d_conv - 1); int32_t prev[2] = {ss.ple_prev[0], ss.ple_prev[1]}; + // layer split: a prompt of one chunk runs the stages one after the other, so the next stage's GPU idles while + // this one reads; it streams and computes a share of this stage's experts then (set_stage_helper), in the + // place of a --peer-device peer, and gives the stage back its own buffers when the run ends + const bool single_chunk = hand_in_ == nullptr ? n <= m.T : single_chunk_; + const bool helped = single_chunk && bind_stage_helper(n); + if (helped) std::swap(m.pp, m.help_pp); + struct HelpScope { + Impl& m; + bool on; + ~HelpScope() { + if (!on) return; + const core::OnDevice d(m.pp->dev); // its work is done before the stage hands over; settle its streams + cudaStreamSynchronize(m.pp->s_cp); + cudaStreamSynchronize(m.pp->s_out); + cudaStreamSynchronize(m.pp->s); + std::swap(m.pp, m.help_pp); + } + } help_scope{m, helped}; PfTimer pt; PeTimer pe; if (m.pp) pe.dev = m.pp->dev; else pe.on = false; @@ -1515,7 +1771,7 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e if (ps_on) m.pp->pseq_start[(size_t) l] = m.pp->pseq.size(); for (int32_t e = 0; e < m.g->n_expert; ++e) { if (m.host_res && m.cache && m.host_res[(size_t) l * m.g->n_expert + e] >= 0) continue; - if (m.pp && m.pp->peer->has(l, e)) continue; // multi-GPU: computed on (or read from) the peer + if (m.pp && m.pp->peer && m.pp->peer->has(l, e)) continue; // multi-GPU: computed on (or read from) the peer int job = -1; const uint8_t* b = nullptr; if (m.src->transient(l, e)) { // CS-T: copied by the source into the stager's buffer @@ -1642,8 +1898,23 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e } host_setup_ms += ms_since(tsetup); bool normed = false; // F-2: the previous half's write already normed R for this half (grs, xn16) + // #579 #613 (opt-in diagnosis, STRATA_PF_STEP_SYNC=1): the compute and copy streams are waited for after each + // step named below, a step that took over 250 ms is logged, and a stall's report names the step it is in. + // Slower (a sync per step); the bytes are the same. + static const bool step_sync = [] { const char* e = std::getenv("STRATA_PF_STEP_SYNC"); return e && e[0] == '1'; }(); + auto pf_step = [&](const char* what, int64_t layer) { + if (!step_sync) return; + core::progress_at(what, layer, p0); + const auto ts = Clock::now(); + const cudaError_t a = cudaStreamSynchronize(m.cs), b = cudaStreamSynchronize(m.copy); + const double ms = ms_since(ts); + if (ms > 250.0 || a != cudaSuccess || b != cudaSuccess) + std::fprintf(stderr, "strata pf-step: chunk from token %lld, layer %lld: %s took %.0f ms (%s / %s)\n", + (long long) p0, (long long) layer, what, ms, cudaGetErrorString(a), cudaGetErrorString(b)); + }; for (int64_t l = LB; l < LE; ++l) { core::progress_beat(); // the serve watchdog: a prompt chunk of 8192 tokens is still moving + if (l > LB) pf_step("reading the prompt (batched, step sync): the experts and the rest of layer", l - 1); core::progress_at("reading the prompt (batched): layer", l, p0); // #251: a stall names layer and chunk const core::LayerView v(*m.wt, l); if (l == std::max(LB, 1) && !ple_land()) return false; // the PLE rows, read from layer 1 on @@ -1773,7 +2044,20 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e core::qsa_kv_format(st), (p0 + s.page_size - 1) / s.page_size, s, m.cs); pt.mark(kPfQsa, cs); + pf_step("reading the prompt (batched, step sync): the K/V staged from RAM at layer", l); } + // #579 #613 (HIP, opt-in A/B, STRATA_KV_HOST_DMA=1): the append writes the staging pool and the + // resident slots only, and one DMA copies the chunk's blocks from the staging pool to the host copy + // - no kernel writes host memory over PCIe. The bytes every reader sees are the same (the staged + // first block is complete; past the chunk's last cell nothing is read until a later append writes + // it). CUDA: never. +#if defined(STRATA_USE_HIP) + static const bool kv_host_dma = [] { const char* e = std::getenv("STRATA_KV_HOST_DMA"); return e && e[0] == '1'; }(); +#else + constexpr bool kv_host_dma = false; +#endif + const bool host_by_dma = staged && kv_host_dma; + const strata::kernels::KvHostPools* host_w = host_by_dma ? nullptr : &st.host; if (st.kv_hybrid) { // K8V4: K INT8 unrotated, V rotated Q4_0 (only V and the output rotate) strata::kernels::fwht256_inplace_cuda(m.Vc, T * 2, m.cs); kv_append(m.Kc, m.Kc, T, p0, st.page_table, s.page_size, nullptr, nullptr, @@ -1788,12 +2072,17 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e } if (st.kv_q4) strata::kernels::kv_append_q4(st.k_q4, st.v_q4, st.page_table, p0, T, m.Kc, m.Vc, s, m.cs, - &st.host, staged ? &m.stage : nullptr); + host_w, staged ? &m.stage : nullptr); else kv_append(m.Kc, m.Vc, T, p0, st.page_table, s.page_size, st.kv_int8 ? nullptr : st.k_pool, st.kv_int8 ? nullptr : st.v_pool, st.k_q, st.v_q, st.k_scale, st.v_scale, m.cs, - &st.host, staged ? &m.stage : nullptr); + host_w, staged ? &m.stage : nullptr); + if (host_by_dma) + strata::kernels::kv_unstage_to_host(pools_of(m.stage, m.ident_table), st.host, + core::qsa_kv_format(st), p0 / s.page_size, + (p0 + T + s.page_size - 1) / s.page_size, s, m.cs); } + if (staged) pf_step("reading the prompt (batched, step sync): the K/V append at layer", l); split_q(m.Qf, m.q, T, m.cs); rms_rows(m.q, (const float*) wqn->data, T * 24, 256, 256, EPS, m.cs); rope(m.q, T, 24, 256, 6144, p0, strata::kernels::rope_scaling(), m.cs); @@ -2049,7 +2338,12 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e int32_t* src_h = grp_mapped ? m.grp_host + 2 * m.grp_tk : m.src_host.data(); if (grp_mapped) copy_i32(m.grp_dev, m.ids, T * K, m.cs); else cudaMemcpyAsync(m.ids_host.data(), m.ids, (size_t) T * K * 4, cudaMemcpyDeviceToHost, m.cs); + // #579: a stall here is the GPU (this layer's attention and router, or the previous layer's + // work), not the host: the watchdog's report says so (only its text changes) + core::progress_at("reading the prompt (batched): waiting for the GPU (attention, router) at layer", + l, p0); cudaStreamSynchronize(m.cs); + core::progress_at("reading the prompt (batched): layer", l, p0); pt.fold(); if (pe.on) { // the peer's marks so far are done: the primary waited for its last rows int pd = 0; cudaGetDevice(&pd); cudaSetDevice(pe.dev); cudaStreamSynchronize(m.pp->s); pe.fold(); cudaSetDevice(pd); @@ -2074,7 +2368,7 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e continue; } if (c == 0 || (m.host_res && m.cache && m.host_res[(size_t) l * m.g->n_expert + e] >= 0) || - !m.pp->peer->has(l, e)) + !m.pp->peer || !m.pp->peer->has(l, e)) continue; if (rows_peer + c > m.pp->cap_rows) { ++m.pp->over_cap; continue; } on_peer[(size_t) e] = 1; @@ -2145,6 +2439,7 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e peer_now = use_mmq && !order_peer.empty(); if (peer_now) { PeerPrefill& P = *m.pp; + if (!P.p2p) cudaMemcpyAsync(P.host_x, m.mixed, (size_t) T * N * 4, cudaMemcpyDeviceToHost, m.cs); cudaEventRecord(P.ev_in, m.cs); int prevd = 0; cudaGetDevice(&prevd); @@ -2153,9 +2448,17 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e cudaStreamWaitEvent(ps, P.ev_in, 0); if (P.out_pending) { cudaStreamWaitEvent(ps, P.ev_done, 0); P.out_pending = false; } pe.mark(kPeMoeIn, ps); - cudaMemcpyPeerAsync(P.mixed, P.dev, m.mixed, prevd, (size_t) T * N * 4, ps); - cudaMemcpyAsync(P.src, src_h + rows_local, // multi-GPU: the mapped table when on - (size_t) rows_peer * 4, cudaMemcpyHostToDevice, ps); + if (P.p2p) cudaMemcpyPeerAsync(P.mixed, P.dev, m.mixed, prevd, (size_t) T * N * 4, ps); + else copy_f32_wide(P.mixed, P.host_x, T * N, ps); + P.back_at = rows_local; + P.back_rows = rows_peer; + if (P.p2p) { + cudaMemcpyAsync(P.src, src_h + rows_local, // multi-GPU: the mapped table when on + (size_t) rows_peer * 4, cudaMemcpyHostToDevice, ps); + } else { + std::memcpy(P.host_src, src_h + rows_local, (size_t) rows_peer * 4); + copy_i32(P.src, P.host_src, rows_peer, ps); + } const size_t n = order_peer.size(); if (P.compact) { // groups of up to MMQ_GROUP experts and at most G rows, each computed from its own rows @@ -2179,7 +2482,12 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e P.bounds_host[n + 1 + g2 * (MMQ_GROUP + 1) + i] = P.bounds_host[std::min(P.groups[g2].second, P.groups[g2].first + i)] - P.bounds_host[P.groups[g2].first]; - cudaMemcpyAsync(P.bounds, P.bounds_host.data(), P.bounds_host.size() * 4, cudaMemcpyHostToDevice, ps); + if (P.p2p || P.bounds_host.size() > PeerPrefill::kHostBounds) { + cudaMemcpyAsync(P.bounds, P.bounds_host.data(), P.bounds_host.size() * 4, cudaMemcpyHostToDevice, ps); + } else { + std::memcpy(P.host_bounds, P.bounds_host.data(), P.bounds_host.size() * 4); + copy_i32(P.bounds, P.host_bounds, (int64_t) P.bounds_host.size(), ps); + } pe.mark(kPeMoeGemm, ps); const auto& f = lay.fmt[(size_t) l]; for (size_t g2 = 0; g2 < ng; ++g2) { @@ -2227,7 +2535,7 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e gu.w = P.grp_gu; gu.type = mmq_gt; gu.w_rows = 1280; gu.w_cols = N; gu.expert_bytes = mmq_gub; gu.n = ngx; gu.xq = P.Xq_g; gu.bounds = rel; gu.ids = P.ident; gu.total_rows = nr; gu.max_rows = maxr; gu.dst = P.GU_g; gu.ld_dst = 1280; - P.ctx->run(gu, ps); + P.run_ctx->run(gu, ps); mmq::swiglu(P.GU_g, P.H_g, nr, 640, !lay.native, ps); mmq::quantize(P.H_g, nullptr, P.Hq_g, mmq_dt, 640, 640, nr, ps); const int b = (int) (g2 & 1); @@ -2237,11 +2545,15 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e dn.n = ngx; dn.xq = P.Hq_g; dn.bounds = rel; dn.ids = P.ident; dn.total_rows = nr; dn.max_rows = maxr; dn.dst = P.Dm_b[b]; dn.ld_dst = N; - P.ctx->run(dn, ps); + P.run_ctx->run(dn, ps); cudaEventRecord(P.ev_grp[g2 % PeerPrefill::kGrpEv], ps); cudaStreamWaitEvent(P.s_out, P.ev_grp[g2 % PeerPrefill::kGrpEv], 0); - cudaMemcpyPeerAsync(m.Dm + (size_t) (rows_local + r0) * N, prevd, P.Dm_b[b], P.dev, - (size_t) nr * N * 4, P.s_out); + if (P.p2p) + cudaMemcpyPeerAsync(m.Dm + (size_t) (rows_local + r0) * N, prevd, P.Dm_b[b], P.dev, + (size_t) nr * N * 4, P.s_out); + else // the primary reads them when it combines (below) + cudaMemcpyAsync(P.host_rows + (size_t) r0 * N, P.Dm_b[b], (size_t) nr * N * 4, + cudaMemcpyDeviceToHost, P.s_out); cudaEventRecord(P.ev_dm[b], P.s_out); P.dm_live[b] = true; } @@ -2254,7 +2566,12 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e for (size_t i = 0; i <= MMQ_GROUP; ++i) P.bounds_host[n + 1 + g2 * (MMQ_GROUP + 1) + i] = P.bounds_host[std::min(n, g2 * MMQ_GROUP + i)] - P.bounds_host[g2 * MMQ_GROUP]; - cudaMemcpyAsync(P.bounds, P.bounds_host.data(), P.bounds_host.size() * 4, cudaMemcpyHostToDevice, ps); + if (P.p2p || P.bounds_host.size() > PeerPrefill::kHostBounds) { + cudaMemcpyAsync(P.bounds, P.bounds_host.data(), P.bounds_host.size() * 4, cudaMemcpyHostToDevice, ps); + } else { + std::memcpy(P.host_bounds, P.bounds_host.data(), P.bounds_host.size() * 4); + copy_i32(P.bounds, P.host_bounds, (int64_t) P.bounds_host.size(), ps); + } mmq::quantize(P.mixed, P.src, P.Xq, mmq_gt, N, N, rows_peer, ps); pe.mark(kPeMoeGemm, ps); const auto& f = lay.fmt[(size_t) l]; @@ -2279,7 +2596,7 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e gu.w = P.grp_gu; gu.type = mmq_gt; gu.w_rows = 1280; gu.w_cols = N; gu.expert_bytes = mmq_gub; gu.n = ngx; gu.xq = P.Xq; gu.bounds = P.bounds + j0; gu.ids = P.ident; gu.total_rows = rows_peer; gu.max_rows = maxr; gu.dst = P.GU; gu.ld_dst = 1280; - P.ctx->run(gu, ps); + P.run_ctx->run(gu, ps); mmq::swiglu(P.GU + r0 * 1280, P.H + r0 * 640, nr, 640, !lay.native, ps); mmq::quantize(P.H + r0 * 640, nullptr, P.Hq, mmq_dt, 640, 640, nr, ps); mmq::Product dn; @@ -2287,7 +2604,7 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e dn.n = ngx; dn.xq = P.Hq; dn.bounds = P.bounds + n + 1 + g2 * (MMQ_GROUP + 1); dn.ids = P.ident; dn.total_rows = nr; dn.max_rows = maxr; dn.dst = P.Dm + r0 * N; dn.ld_dst = N; - P.ctx->run(dn, ps); + P.run_ctx->run(dn, ps); if (P.out_pipe && nr > 0) { // this group's rows go back while the next group computes cudaEventRecord(P.ev_grp[g2], ps); cudaStreamWaitEvent(P.s_out, P.ev_grp[g2], 0); @@ -2527,7 +2844,7 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e } else { const bool r0 = m.host_res && m.cache && m.host_res[(size_t) l * m.g->n_expert + e] >= 0; const uint8_t* bp = r0 ? m.cache->device_slot(m.host_res[(size_t) l * m.g->n_expert + e]) - : (m.pp ? m.pp->peer->slot_ptr(l, e) : nullptr); // over the cap: P2P + : (m.pp && m.pp->peer ? m.pp->peer->slot_ptr(l, e) : nullptr); // over the cap: P2P if (bp == nullptr) { err = "prefill: an expert is neither resident, streamed nor on the peer"; return false; } ++stats_.experts_resident; if (!compute(j, bp, -1)) return false; @@ -2538,7 +2855,11 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e } } pt.mark(kPfCombine, cs); - if (peer_now) cudaStreamWaitEvent(m.cs, m.pp->ev_done, 0); // multi-GPU: the peer's rows are in Dm + if (peer_now) { // multi-GPU: the peer's rows are in Dm (or, without P2P, in host memory) + cudaStreamWaitEvent(m.cs, m.pp->ev_done, 0); + if (!m.pp->p2p) + copy_f32_wide(m.Dm + (size_t) m.pp->back_at * N, m.pp->host_rows, m.pp->back_rows * N, m.cs); + } moe_combine(m.Dm, m.slot_dev, m.w, m.shared, m.sg, m.bo, T, m.cs); // debug: STRATA_DBG_NAN=1 reports the first layer of a chunk whose MoE produced non-finite values if (static const bool dbg = std::getenv("STRATA_DBG_NAN") != nullptr; dbg) { @@ -2598,21 +2919,24 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e core::progress_at("reading the prompt (batched): finishing the chunk from token", p0); pt.mark(kPfStart, cs); if (next_ != nullptr) { - // the rows to the host buffer the next stage read two chunks ago (it has finished: waited below) - float* h = m.hand[hand_buf]; + // The current hand-off slot was used two chunks ago. + float* h = m.hand[hand_buf_]; if (cudaMemcpyAsync(h, m.R, (size_t) T * D * 4, cudaMemcpyDeviceToHost, m.cs) != cudaSuccess || cudaStreamSynchronize(m.cs) != cudaSuccess) { err = std::string("prefill: the layer split's hand-off: ") + cudaGetErrorString(cudaGetLastError()); return false; } - // this stage's state is at the chunk's end now (synced) and moves on with the next chunk below + // Wait only for the DIRECT successor's previous chunk. That successor + // may already have forwarded its older chunk to later GPUs. if (on_stage_chunk && !on_stage_chunk(p0 + T, err)) return false; - if (next_run.valid() && !next_run.get()) { err = next_err; return false; } + if (next_run_.valid() && !next_run_.get()) { err = next_err_; return false; } + next_err_.clear(); next_->hand_in_ = h; - next_run = std::async(std::launch::async, [this, tokens, c0, T, p0, &next_err] { - return next_->run(tokens + c0, T, p0, next_err); + next_->single_chunk_ = single_chunk; + next_run_ = std::async(std::launch::async, [this, tokens, c0, T, p0] { + return next_->run_impl(tokens + c0, T, p0, next_err_); }); - hand_buf ^= 1; + hand_buf_ ^= 1; continue; // the last stage reports the chunk (on_chunk) } if (const char* dump = std::getenv("STRATA_PREFILL_DUMP_R")) { // debug: the final residuals, every 64th @@ -2641,7 +2965,8 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e host_chunk_ms += ms_since(toc2); } } - if (next_run.valid() && !next_run.get()) { err = next_err; return false; } + // Do not drain the successor here. This is the overlap: an intermediate + // stage can return while later GPUs are still processing the previous chunk. ss.ple_prev[0] = prev[0]; ss.ple_prev[1] = prev[1]; if (std::getenv("STRATA_DBG_NAN") != nullptr) { // debug: the state the prompt leaves for the token path @@ -2721,4 +3046,39 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e return true; } +bool Prefill::drain_pipeline(std::string& err) { + bool ok = true; + + if (next_run_.valid()) { + if (!next_run_.get()) { + err = next_err_; + ok = false; + } + } + + if (next_ != nullptr) { + std::string tail_err; + if (!next_->drain_pipeline(tail_err)) { + if (ok) err = tail_err; + ok = false; + } + } + + return ok; +} + +bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& err) { + const bool body_ok = run_impl(tokens, n, pos0, err); + + std::string drain_err; + const bool drain_ok = drain_pipeline(drain_err); + + if (!body_ok) return false; + if (!drain_ok) { + err = drain_err; + return false; + } + return true; +} + } // namespace strata::prefill diff --git a/src/program/generate.cpp b/src/program/generate.cpp index d40deb1b7e..03b5010453 100644 --- a/src/program/generate.cpp +++ b/src/program/generate.cpp @@ -15,6 +15,7 @@ // (`phase-2-correct-engine.md:5-9`). What it is FOR is the honest tok/s figure and the logit dump. #include "strata/core/device.hpp" +#include "strata/core/remote_expert_opt.hpp" #include "strata/core/expert_cache.hpp" #include "strata/core/conversation_snapshot.hpp" #include "strata/core/conversation_memory.hpp" @@ -70,6 +71,8 @@ #else #include #include +#include +#include #endif #include @@ -113,7 +116,85 @@ bool under_wddm() { #endif } +// #620 #486: " (N MiB of M MiB VRAM free on this GPU)" for an allocation's failure message (clears the error first) +std::string vram_free_note() { + (void) cudaGetLastError(); + size_t free_b = 0, total_b = 0; + if (cudaMemGetInfo(&free_b, &total_b) != cudaSuccess) { + (void) cudaGetLastError(); + return {}; + } + char buf[96]; + std::snprintf(buf, sizeof buf, " (%llu MiB of %llu MiB VRAM free on this GPU)", (unsigned long long) (free_b >> 20), + (unsigned long long) (total_b >> 20)); + return buf; +} + // perf-review D-4: the lent slots are refilled with queued copies and one wait; STRATA_REFILL_BLOCKING=1 waits on each +// A file-backed arena on AMD (STRATA_ARENA_MMAP; the engine raises ROCclr's pin-in-place threshold, see main): an +// adaptive swap copies one expert straight from the mapped file. Through ROCclr's staging that copy blocks the host +// (decode windows of 150+ ms); instead the batch's pages are locked here for the async copies and unlocked when they +// have landed (unpin_blobs), so the release that follows can hand them back. Neighbouring experts share a page and +// a copy from a range only PART of which is registered fails ("invalid argument"), so the page ranges are merged +// first and each merged span is registered once. A span that does not register is copied through the staging +// path. Portable: a layer split copies to either card. +// Whether pin_blobs locks anything (AMD on Linux with STRATA_ARENA_MMAP=1): the callers only gather the swaps' spans +// then - `blob()` is not free (it counts reads, and the file tier may assemble the blob), so the default runs skip it. +bool pin_blobs_on() { +#if (defined(STRATA_USE_HIP) || defined(STRATA_HIP_GFX906)) && !defined(_WIN32) + static const bool on = std::getenv("STRATA_ARENA_MMAP") && std::getenv("STRATA_ARENA_MMAP")[0] == '1'; + return on; +#else + return false; +#endif +} +int pin_blobs(std::vector> r, std::vector& live) { + int failed = 0; // ranges left pageable (a copy from them still works, through the driver's staging) +#if (defined(STRATA_USE_HIP) || defined(STRATA_HIP_GFX906)) && !defined(_WIN32) + if (!pin_blobs_on() || r.empty()) return 0; + for (auto& [a, e] : r) { a &= ~(uintptr_t) 4095; e = (e + 4095) & ~(uintptr_t) 4095; } + std::sort(r.begin(), r.end()); + size_t k = 0; + for (size_t i = 1; i < r.size(); ++i) { + if (r[i].first <= r[k].second) r[k].second = std::max(r[k].second, r[i].second); + else r[++k] = r[i]; + } + r.resize(k + 1); + for (const auto& [a, e] : r) { + if (cudaHostRegister((void*) a, (size_t) (e - a), cudaHostRegisterReadOnly | cudaHostRegisterPortable) == + cudaSuccess) + live.push_back((void*) a); + else { + (void) cudaGetLastError(); + ++failed; + } + } +#else + (void) r; (void) live; +#endif + return failed; +} +void unpin_blobs(std::vector& v) { + for (void* p : v) (void) cudaHostUnregister(p); + (void) cudaGetLastError(); + v.clear(); +} + +// STRATA_RSS_TRACE=1: the process's mapped file pages (the arena) at a startup step - who reads the arena back +void rss_probe(const char* where) { + static const bool on = std::getenv("STRATA_RSS_TRACE") != nullptr; + if (!on) return; + FILE* f = std::fopen("/proc/self/status", "r"); + if (!f) return; + char line[256]; + long long kb = -1; + while (std::fgets(line, sizeof line, f)) + if (std::strncmp(line, "RssFile:", 8) == 0) { kb = std::atoll(line + 8); break; } + std::fclose(f); + std::fprintf(stderr, "strata rss: %s: RssFile %.2f GiB\n", where, (double) kb / (1024.0 * 1024.0)); + std::fflush(stderr); +} + bool refill_blocking() { static const bool v = std::getenv("STRATA_REFILL_BLOCKING") != nullptr; return v; @@ -290,6 +371,8 @@ struct Options { /// measurement found nothing - see `expert_cache_per_layer`. int expert_cache = 0; std::array expert_cache_remote{}; ///< CUDA1..3 slots; CUDA0 keeps dense/state/MTP + std::array expert_cache_remote_auto{}; + bool remote_expert_opt = false; ///< optional optimization of the existing remote-expert decode path std::string expert_cache_remote_placement = "stripe"; ///< stripe experts or assign complete layers to CUDA1..3 bool expert_cache_cpu_order = false; /// **R4.2g. ROUND 328 MEASURED THAT THE GLOBAL ADMISSION POLICY CANNOT WORK, AND THIS IS THE FIX.** @@ -341,6 +424,10 @@ struct Options { /// and the KV state, minus this reserve for the graphs, the hit scratch and the head. int vram_reserve_mib = 700; bool vram_reserve_given = false; ///< --vram-reserve-mib on the command line (#496: no smaller automatic reserve) + /// #533 (opt-in): the expert cache in physical segments (CUDA virtual memory management), so the serve loop's + /// `VRAM ` command can give part of it back to other programs and take it again. Off: one cudaMalloc. + bool vram_elastic = false; + int64_t vram_segment_mib = 512; /// Plan v0.3 P5: batched prompt processing in chunks of this many tokens (0 = the token path). int64_t prefill_chunk = 0; /// `--prefill auto`: the largest chunk (up to 8192) whose buffers the expert cache can lend. Every expert a chunk @@ -364,6 +451,16 @@ struct Options { /// spec-1 drafts); 0 = plain decode. `spec_oracle` drafts from a token file (the expected continuation, for /// the exactness test); `spec_corrupt` N > 0 replaces every Nth draft with a wrong token. int spec = 0; + /// --serve runs up to this many sequences at once, one token each per batch window (0 = off). A request + /// sent as `BGEN ...` reads its prompt and its first token through the usual path, then + /// continues in slot of the batch windows (`BT ` lines, then `BDONE ...`). + int batch = 0; + /// With an explicit --layer-split, every GPU loads only its own layers' dense weights instead of the whole + /// model's: the VRAM they took goes back to the expert cache (opt-in) + bool trim_stage_weights = false; + /// The --batch slots in this many groups pipelined through the stages of a layer split (stage k + /// runs one group while stage k+1 runs another). 1 = every slot in one window, stage after stage. + int batch_groups = 1; std::string spec_oracle; int spec_corrupt = 0; /// Plan v0.3 P6: the MTP draft layer's runtime directory (tools/mtp_rt.py); drafts come from it. @@ -414,6 +511,11 @@ struct Options { /// --serve: the token that opens a chat turn (<|im_start|>). The last one in a prompt is where the chat's /// history ends and the new assistant turn begins, which is the checkpoint the next request can reuse. int64_t turn_token = 248045; + /// --serve (#458, opt-in): the role token of a short turn the server puts right before the new assistant turn + /// (the "system" of a trailing reasoning-effort turn). When the turn before the last <|im_start|> opens with it, + /// the checkpoint goes in front of that turn instead, so it holds the conversation and not the effort text. + /// -1 = off (the checkpoint at the last <|im_start|>, as before). + int64_t tail_role_token = -1; /// --serve: a text part of the prompt of at most N tokens (a chat message, the assistant header, a short tool /// result) goes through the verify windows, S tokens at a time, instead of the batched prompt path (0 = always /// the batched path) @@ -516,8 +618,17 @@ void usage() { " --conversation-cache-mib N --serve: RAM budget for parked conversations (default 0 = off)\n" " --conversation-cache-slots N --serve: at most N parked conversations (default 4)\n" " --conversation-cache-min-free-mib N --serve: physical RAM floor when parking (default 2560)\n" + " --vram-elastic --serve (#533, opt-in, NVIDIA): the expert cache in segments (--vram-segment-mib,\n" + " default 512), so the command `VRAM ` (the server's POST /v1/vram) can\n" + " give VRAM back to other programs between requests and take it back later\n" + " --batch N / --slots N --serve (opt-in): up to N requests decode together in batch slots (2..8),\n" + " each slot with its own session (VRAM like the main one); docs/BATCHING.md\n" + " --batch-groups G --batch with a layer split: the slots in G groups pipelined through the GPUs\n" + " --trim-stage-weights an explicit --layer-split: each GPU loads only its own layers' dense weights\n" " --prompt-cache-every N --serve: also checkpoint every N fresh prompt tokens (default 16384, 0 = off)\n" " --turn-token ID --serve: the token that opens a chat turn (default 248045, <|im_start|>)\n" + " --tail-role-token ID --serve: a turn of this role right before the last turn is left out of\n" + " the conversation checkpoint (a trailing effort turn; default -1 = off)\n" " --short-read N --serve: read at most N fresh text tokens through the decode windows instead\n" " of the batched prompt path (default 64, 0 = off)\n" " --suffix-draft N prompt lookup: draft from an earlier repeat of the last N+ tokens of context\n" @@ -563,9 +674,10 @@ void usage() { " GPU via `moe_hit_grouped_s2`. DEFAULT 0. Measured at 4096 slots\n" " with --expert-cache-per-layer: 54.4%% hits, CPU pool drain 19.1 -> 10.3\n" " ms/token, -2.7 ms/token end to end.\n" - " --expert-cache-device1 N pre-fill N experts on CUDA1 (experimental)\n" - " --expert-cache-device2 N pre-fill N more experts on CUDA2\n" - " --expert-cache-device3 N pre-fill N more experts on CUDA3\n" + " --expert-cache-device1 N|auto fill CUDA1 with N experts or as many as fit\n" + " --expert-cache-device2 N|auto fill CUDA2 with more experts\n" + " --expert-cache-device3 N|auto fill CUDA3 with more experts\n" + " --remote-expert-opt enable remote-expert decode optimizations (serve mode)\n" " --expert-cache-remote-placement stripe|layer distribute expert ranks or whole\n" " layers across CUDA1..3 (default: stripe)\n" " --peer-device N a second GPU as an adaptive expert-cache tier (rows over P2P; it also\n" @@ -598,8 +710,7 @@ void usage() { " 34 GB, and measured 71.97 vs 34.78 ms/token cold vs warm.\n" " --shared-expert-arena FILE Linux: back the resident arena with one MAP_SHARED file.\n" " Put this file on /dev/shm, not ordinary SSD storage.\n" - " A small header binds an existing backing file to the same pack.\n" - " --shared-expert-arena-follower attach to a fully populated shared arena and skip the source\n" + " A small header binds an existing backing file to the same pack.\n" " --shared-expert-arena-follower attach to a fully populated shared arena and skip the source\n" " expert load; refused unless the shared header has published readiness.\n" " --resident-cpu-experts with mmap and a static profile, keep the experts the GPU cache does not\n" " hold resident in ordinary RAM (and the prompt path's lend region as far as\n" @@ -794,6 +905,55 @@ std::string stage_text() { return s; } +#if !defined(_WIN32) +// #605: whether `path` is on a rotational disk (1), not (0) or unknown (-1), from sysfs; `dev` is the disk's name +int on_rotational_disk(const std::string& path, std::string& dev) { + struct stat st{}; + if (::stat(path.c_str(), &st) != 0) return -1; + char sys[64]; + std::snprintf(sys, sizeof sys, "/sys/dev/block/%u:%u", (unsigned) major(st.st_dev), (unsigned) minor(st.st_dev)); + std::error_code ec; + std::filesystem::path p = std::filesystem::canonical(sys, ec); + for (int up = 0; !ec && up < 2; ++up, p = p.parent_path()) { // a partition has its disk's queue + std::ifstream q(p / "queue" / "rotational"); + int r = -1; + if (q >> r) { + dev = p.filename().string(); + return r; + } + } + return -1; +} + +// #605: the engine's threads in uninterruptible sleep (state D - almost always waiting for the disk), and where +// (wchan): 16 threads in blk_io_schedule is a disk that cannot keep up, not a deadlock in the engine +void disk_wait_threads(std::FILE* f) { + std::map where; + int waiting = 0, threads = 0; + std::error_code ec; + for (const auto& e : std::filesystem::directory_iterator("/proc/self/task", ec)) { + ++threads; + std::ifstream st(e.path() / "stat"); + std::string line; + std::getline(st, line); + const size_t rp = line.rfind(')'); + if (rp == std::string::npos || rp + 2 >= line.size() || line[rp + 2] != 'D') continue; + ++waiting; + std::ifstream w(e.path() / "wchan"); + std::string wc; + std::getline(w, wc); + ++where[wc.empty() || wc == "0" ? "?" : wc]; + } + if (threads == 0) return; + std::string list; + for (const auto& [name, n] : where) list += (list.empty() ? " (" : ", ") + name + " x" + std::to_string(n); + if (!list.empty()) list += ")"; + std::fprintf(f, " threads waiting on the disk (state D): %d of %d%s%s\n", waiting, threads, list.c_str(), + waiting > 0 ? " - the engine is waiting for the drive: a rotational or failing disk with --ple-io " + "direct (#605: --ple-io ram), or a drive too slow for the reads asked of it" : ""); +} +#endif + void stall_report(std::FILE* f, uint64_t layers_during) { strata::core::Progress& p = strata::core::progress(); std::fprintf(f, "strata serve: stall report (engine %s): stage \"%s\" for %lld s; %llu layers served since the " @@ -807,6 +967,9 @@ void stall_report(std::FILE* f, uint64_t layers_during) { } if (auto fn = strata::core::diag_pool_fn().load()) fn(f); if (auto fn = strata::core::diag_verify_fn().load()) fn(f); +#if !defined(_WIN32) + disk_wait_threads(f); +#endif const MemSample m = mem_sample(); std::fprintf(f, " memory: %llu MiB resident, %llu MiB %s, %llu MiB RAM available; %llu %s\n", m.rss_mib, m.commit_mib, @@ -1069,6 +1232,15 @@ int main(int argc, char** argv) { // pipe or a file is block-buffered, so a program that dies loses every line it had already printed - which // turns "it crashed at step 7" into "it crashed somewhere", and the difference is a debugging session. std::setvbuf(stdout, nullptr, _IONBF, 0); +#if (defined(STRATA_USE_HIP) || defined(STRATA_HIP_GFX906)) && !defined(_WIN32) + // AMD, a file-backed arena (STRATA_ARENA_MMAP): ROCclr copies a pageable source of 1 MiB or more by locking its + // pages in place (a GPU userptr), and keeps them - so every expert the VRAM fill copied from the mapped + // experts.bin stayed resident, the release of the VRAM-held ones was undone by the driver at once, and a + // 32 GB PC ran with ~0.3 GB free: the kernel swapped, and a turn's checkpoint took 25-80 s. Above this size + // ROCclr stages through its own pinned buffers instead; decode is unchanged (49.06 vs 49.16 ms/window). + if (std::getenv("STRATA_ARENA_MMAP") && std::getenv("STRATA_ARENA_MMAP")[0] == '1') + setenv("GPU_PINNED_MIN_XFER_SIZE", "1000000", 0); // MiB; an explicit setting wins +#endif // Load every CUDA kernel when the context is created, before the expert cache takes the free VRAM. With the // default lazy loading, a kernel first used mid-prompt (MMQ for IQ3_XXS at 64K+ on a 12 GB card) found no VRAM // left for its code and the engine ended ("out of memory: cudaFuncSetAttribute"). Costs ~30 MB of VRAM. @@ -1196,9 +1368,13 @@ int main(int argc, char** argv) { const std::string v = next("--expert-cache"); o.expert_cache = (v == "auto") ? -1 : std::atoi(v.c_str()); } - else if (a == "--expert-cache-device1") o.expert_cache_remote[0] = std::atoi(next("--expert-cache-device1")); - else if (a == "--expert-cache-device2") o.expert_cache_remote[1] = std::atoi(next("--expert-cache-device2")); - else if (a == "--expert-cache-device3") o.expert_cache_remote[2] = std::atoi(next("--expert-cache-device3")); + else if (a == "--expert-cache-device1" || a == "--expert-cache-device2" || a == "--expert-cache-device3") { + const size_t r = (size_t) (a.back() - '1'); + const std::string v = next(a.c_str()); + o.expert_cache_remote_auto[r] = v == "auto"; + o.expert_cache_remote[r] = v == "auto" ? std::numeric_limits::max() : std::atoi(v.c_str()); + } + else if (a == "--remote-expert-opt") o.remote_expert_opt = true; else if (a == "--expert-cache-remote-placement") o.expert_cache_remote_placement = next("--expert-cache-remote-placement"); else if (a == "--vram-reserve-mib") { o.vram_reserve_mib = std::atoi(next("--vram-reserve-mib")); o.vram_reserve_given = true; } @@ -1214,9 +1390,15 @@ int main(int argc, char** argv) { } else if (a == "--no-split-rows") o.no_split_rows = true; else if (a == "--no-prefill-borrow") o.no_prefill_borrow = true; + else if (a == "--vram-elastic") o.vram_elastic = true; + else if (a == "--vram-segment-mib") o.vram_segment_mib = std::atoll(next("--vram-segment-mib")); else if (a == "--prefill-until") o.prefill_until = std::atoll(next("--prefill-until")); else if (a == "--dump-final-r") o.dump_final_r = next("--dump-final-r"); else if (a == "--spec") o.spec = std::atoi(next("--spec")); + else if (a == "--batch") o.batch = std::atoi(next("--batch")); + else if (a == "--slots") o.batch = std::atoi(next("--slots")); // the same as --batch + else if (a == "--trim-stage-weights") o.trim_stage_weights = true; + else if (a == "--batch-groups") o.batch_groups = std::atoi(next("--batch-groups")); else if (a == "--spec-oracle") o.spec_oracle = next("--spec-oracle"); else if (a == "--spec-corrupt") o.spec_corrupt = std::atoi(next("--spec-corrupt")); else if (a == "--mtp") o.mtp = next("--mtp"); @@ -1251,6 +1433,7 @@ int main(int argc, char** argv) { else if (a == "--prompt-cache-every") o.prompt_cache_every = std::max(0LL, std::atoll(next("--prompt-cache-every"))); else if (a == "--prompt-cache-root") o.prompt_cache_root = std::max(0LL, std::atoll(next("--prompt-cache-root"))); else if (a == "--turn-token") o.turn_token = std::atoll(next("--turn-token")); + else if (a == "--tail-role-token") o.tail_role_token = std::atoll(next("--tail-role-token")); else if (a == "--short-read") o.short_read = std::max(0LL, std::atoll(next("--short-read"))); else if (a == "--suffix-draft") o.suffix_draft = std::max(0, std::atoi(next("--suffix-draft"))); else if (a == "--mtp-max-t") o.mtp_max_t = std::max(0, std::atoi(next("--mtp-max-t"))); @@ -1354,14 +1537,12 @@ int main(int argc, char** argv) { if (o.serve && o.conversation_cache_mib > 0 && (o.prompt_cache == 0 || o.conversation_cache_slots == 0)) std::fprintf(stderr, "strata serve: warning: conversation caching is disabled by %s\n", o.prompt_cache == 0 ? "--prompt-cache 0" : "--conversation-cache-slots 0"); - if (o.conversation_cache_mib > 0 && o.conversation_cache_slots > 0 && o.prompt_cache > 0 && !o.layer_split.empty()) { - std::fprintf(stderr, "strata serve: conversation parking does not yet support --layer-split; disable parking with --conversation-cache-mib 0\n"); - return 2; - } + // parking with --layer-split saves every stage (SavedConversation::stage_images) // Layer split (multi-GPU): the later stages run layers [K_i, K_i+1) on their own GPUs (--split-device, default - // the next visible ones); "auto" places the K from each GPU's free VRAM once the weights are in (below). Across - // GPUs, not yet: KV streaming, images, control vectors, the helper caches (--expert-cache-remote), and lending - // cache slots to the prompt path (each stage's prompt path has its own buffers). + // the next visible ones); "auto" places the K from each GPU's free VRAM once the weights are in (below). KV + // streaming and images (the server's GPU encoder) run across a split, also with parking and --batch. Across + // GPUs, not yet: control vectors, the helper caches (--expert-cache-remote), and lending cache slots to the + // prompt path (each stage's prompt path has its own buffers). // --pcie-frac given: that one share is every stage's (the stages' own link probes are skipped), so a split over // a fast and a slow link cannot set the two apart from the command line (#485) const bool pcie_given = o.pcie_frac >= 0.0; @@ -1392,6 +1573,11 @@ int main(int argc, char** argv) { for (int d = 1; d < n_dev && (split_auto || split_devs.size() < split_at.size()); ++d) split_devs.push_back(d); if (ok && !split_auto && split_devs.empty() && split_at.size() == 1) split_devs.push_back(0); // one GPU split_same = ok && split_devs.size() == 1 && split_devs[0] == 0 && !split_auto; + if (split_same && o.conversation_cache_mib > 0 && o.conversation_cache_slots > 0 && o.prompt_cache > 0) { + std::fprintf(stderr, "strata serve: conversation parking does not support a layer split on one GPU " + "(--split-device 0); disable parking with --conversation-cache-mib 0\n"); + return 2; + } if (ok && split_auto && split_devs.empty()) { std::fprintf(stderr, "strata generate: --layer-split auto: one GPU visible, so no split\n"); o.layer_split.clear(); @@ -1406,7 +1592,9 @@ int main(int argc, char** argv) { } if (!ok) { std::fprintf(stderr, "strata generate: --layer-split K[,K2..]|auto needs --serve, rising K from 2, and one " - "distinct GPU per K in --split-device (1..%d; or 0 with one K: the same GPU)\n", n_dev - 1); + "distinct GPU per K in --split-device (1..%d; or 0 with one K: the same GPU). K is the " + "FIRST LAYER of each later GPU, not a count of layers per card: \"24,36,42\" for 4 " + "GPUs, not \"24,12,6,6\" (got \"%s\")\n", n_dev - 1, o.layer_split.c_str()); return 2; } } @@ -1721,6 +1909,10 @@ int main(int argc, char** argv) { std::fprintf(stderr, "strata generate: --expert-cache-remote-placement must be stripe or layer\n"); return 2; } + if (o.remote_expert_opt && !o.serve) { + std::fprintf(stderr, "strata generate: --remote-expert-opt requires --serve\n"); + return 2; + } // the peer tier is the second card's only user: a layer split or a remote expert cache would put a second engine // part (and a second copy of the same experts) on it if (o.peer_device >= 1 && (!o.layer_split.empty() || o.expert_cache_remote[0] > 0)) { @@ -1771,12 +1963,33 @@ int main(int argc, char** argv) { // ... and nothing runs on a CPU without AVX2: every CPU expert kernel is AVX2 at least (the AVX-512 ones are // chosen above it), and so is ggml-cpu in the release build, which the native pack's layout load initializes // next. Refused here, by name, rather than an illegal instruction in the first expert. - if (!strata::kernels::cpu::cpu_avx2_ok()) { - std::fprintf(stderr, "strata generate: this CPU (%s) does not support AVX2 with FMA and F16C, which every CPU " - "expert kernel needs; Strata runs on Intel Haswell (2013), AMD Zen (2017) or newer\n", + // The experimental older-CPU build (STRATA_ISA_FLOOR=avx|none, compiled on that PC; #394 #595 #623) has ggml-cpu + // for that floor, so a native pack's experts run there on ggml-cpu (every AVX2 kernel is behind cpu_avx2_ok). + const char* isa_floor = strata::kernels::cpu::isa_floor_build(); + if (!strata::kernels::cpu::cpu_avx2_ok() && isa_floor[0] == '\0') { + std::fprintf(stderr, "strata generate: this CPU (%s) does not support AVX2 with FMA and F16C, which this engine's " + "CPU expert kernels need; Strata runs on Intel Haswell (2013), AMD Zen (2017) or newer. " + "Older CPUs are EXPERIMENTAL and slow: setup compiles an engine for them on this PC " + "(STRATA_ISA_FLOOR, see \"Older CPUs\" in docs/INSTALL.md)\n", strata::kernels::cpu::cpu_name().c_str()); return 2; } + if (isa_floor[0] != '\0') { + const bool avx_floor = std::strcmp(isa_floor, "avx") == 0; + if (avx_floor ? !strata::kernels::cpu::cpu_avx1_ok() : !strata::kernels::cpu::cpu_sse42_ok()) { + std::fprintf(stderr, "strata generate: this engine is the older-CPU build for %s, which this CPU (%s) does " + "not have%s\n", avx_floor ? "AVX" : "SSE4.2 with POPCNT", + strata::kernels::cpu::cpu_name().c_str(), + avx_floor ? "; compile it with STRATA_ISA_FLOOR=none" : ""); + return 2; + } + if (!strata::kernels::cpu::cpu_avx2_ok()) + std::fprintf(stderr, "strata generate: EXPERIMENTAL older-CPU build (ggml-cpu for %s): this CPU has no AVX2, " + "so the CPU's experts run on ggml-cpu's kernels - expect it to be slow\n", isa_floor); + else + std::fprintf(stderr, "strata generate: this is the older-CPU build (ggml-cpu for %s); this CPU has AVX2, " + "and the normal build is faster on it\n", isa_floor); + } std::string err; if (!o.native_head_gguf.empty() && !o.stream_token) { @@ -1849,7 +2062,8 @@ int main(int argc, char** argv) { else if (!strata::kernels::cpu::cpu_avx512_ok()) std::fprintf(stderr, "strata generate: this CPU has no AVX-512: the expert kernels run on %s " "(multi-token for the i-quant gate/up rows)\n", - std::getenv("STRATA_NO_IQ256") == nullptr ? "AVX-2" : "ggml-cpu vec_dot (STRATA_NO_IQ256 set)"); + !strata::kernels::cpu::cpu_avx2_ok() ? "ggml-cpu vec_dot (no AVX2: the older-CPU build)" + : std::getenv("STRATA_NO_IQ256") == nullptr ? "AVX-2" : "ggml-cpu vec_dot (STRATA_NO_IQ256 set)"); strata::core::ModelGeometry g; // canonical defaults; the model file overrides the MoE shape below int64_t K = 10; // THE ROPE CONFIG RESOLVES HERE, BEFORE ANY WEIGHT MOVES - the CLI and the model file have both spoken, @@ -1992,6 +2206,41 @@ int main(int argc, char** argv) { } if (native_pack) skip.insert("token_embd.weight"); } + // Layer split with explicit split points: every GPU holds only the dense weights of ITS layers (the PLE tensors + // stay everywhere). Without this each card keeps a full copy (~3.4 GB for the Coder) that its stage never + // reads - VRAM the expert cache wants. Opt-in, STRATA_STAGE_TRIM=1, on HIP and CUDA alike: measured on 2x MI50 + // (PR #639), and the default waits for its author's re-run of the release branch on his cards. + const std::set skip_base = skip; + // --trim-stage-weights (PR #559) is the same switch as STRATA_STAGE_TRIM=1 (PR #639) + const bool stage_trim = multi_gpu && !split_auto && !split_at.empty() && (o.trim_stage_weights || [] { + const char* v = std::getenv("STRATA_STAGE_TRIM"); + return v != nullptr && v[0] != 0 && std::string(v) != "0"; + }()); + if (o.trim_stage_weights && !stage_trim) + std::fprintf(stderr, "strata generate: --trim-stage-weights needs an explicit --layer-split (ignored)\n"); + // keep_routers: CUDA0 with --mmap-experts keeps every layer's router (ffn_gate_inp, ~2.5 MiB a layer) - the file + // tier's routing-aware prefetch (RouterLookahead, below) copies all of them from CUDA0's arena, and would + // otherwise turn itself off + auto add_foreign = [&](int64_t lb, int64_t le, std::set& out, bool keep_routers) { + std::FILE* f = std::fopen((o.pack + "/index.txt").c_str(), "rb"); + if (!f) return; + char line[1024], name[256]; + while (std::fgets(line, sizeof line, f)) { + if (line[0] == '#' || std::sscanf(line, "%255s", name) != 1) continue; + const std::string n = name; + if (n.rfind("blk.", 0) != 0 || n.find("ple") != std::string::npos) continue; + if (keep_routers && n.ends_with(".ffn_gate_inp.weight")) continue; + const int64_t l = std::atoll(name + 4); + if (l < lb || l >= le) out.insert(n); + } + std::fclose(f); + }; + if (stage_trim) { + add_foreign(0, split_at[0], skip, o.mmap_experts); + strata::core::NativeDense::set_layer_range(0, (int) split_at[0]); + std::fprintf(stderr, "strata generate: layer split: CUDA0 loads the dense weights of layers 0-%lld only\n", + (long long) split_at[0] - 1); + } uint64_t pool_bytes = 0; if (!strata::core::WeightTable::pool_bytes(o.pack, pool_bytes, err, skip.empty() ? nullptr : &skip)) { std::fprintf(stderr, "strata generate: %s\n", err.c_str()); @@ -2164,6 +2413,18 @@ int main(int argc, char** argv) { std::fprintf(stderr, "strata generate: %s\n", err.c_str()); return 1; } +#if !defined(_WIN32) + if (pio.mode == strata::kernels::PleIo::Direct) { + // #605: --ple-io direct's random reads (up to --ple-inflight at once) are for SSDs; a rotational disk + // can take longer than the stall watchdog's 60 s for one prompt chunk's rows. Recommended, not forced. + std::string dev; + if (!under_wddm() && on_rotational_disk(o.ple_gguf, dev) == 1) // (WSL's virtual disk says rotational) + std::fprintf(stderr, "strata generate: WARNING: the n-gram table (%s) is on a rotational disk (%s): " + "--ple-io direct reads it at random and can stall a prompt for minutes (#605). " + "--ple-io ram (it then needs RAM for the table) or the model on an SSD is " + "recommended\n", o.ple_gguf.c_str(), dev.c_str()); + } +#endif if (pio.lock) std::fprintf(stderr, "strata generate: PLE table %s (--ple-io ram) in %.1f s\n", ple_table.locked() ? "locked in RAM" : "loaded (not locked)", @@ -2311,16 +2572,31 @@ int main(int argc, char** argv) { return 1; } const strata::core::OnDevice on(st.dev); + std::set skip_s = skip_base; + uint64_t pool_s = pool_bytes; + if (stage_trim) { + const int64_t lb = split_at[i], le = i + 1 < split_at.size() ? split_at[i + 1] : g.n_layers; + add_foreign(lb, le, skip_s, false); + strata::core::NativeDense::set_layer_range((int) lb, (int) le); + if (!strata::core::WeightTable::pool_bytes(o.pack, pool_s, err, &skip_s)) { + std::fprintf(stderr, "strata generate: %s\n", err.c_str()); + return 1; + } + std::fprintf(stderr, "strata generate: layer split: CUDA%d loads the dense weights of layers %lld-%lld only\n", + st.dev, (long long) lb, (long long) le - 1); + } else { + skip_s = skip; + } void* arena_s = nullptr; - if (cudaMalloc(&arena_s, pool_bytes) != cudaSuccess || - !st.wt.load(o.pack, arena_s, pool_bytes, err, skip.empty() ? nullptr : &skip)) { + if (cudaMalloc(&arena_s, pool_s) != cudaSuccess || + !st.wt.load(o.pack, arena_s, pool_s, err, skip_s.empty() ? nullptr : &skip_s)) { cudaGetLastError(); size_t free_b = 0, total_b = 0; // #486: what that card had free cudaMemGetInfo(&free_b, &total_b); std::fprintf(stderr, "strata generate: layer split, CUDA%d weights: %s (%llu MiB needed, %llu MiB of %llu " "MiB free on that card)\n", st.dev, err.empty() ? "the weight arena does not fit" : err.c_str(), - (unsigned long long) (pool_bytes >> 20), (unsigned long long) (free_b >> 20), + (unsigned long long) (pool_s >> 20), (unsigned long long) (free_b >> 20), (unsigned long long) (total_b >> 20)); return 1; } @@ -2558,7 +2834,7 @@ int main(int argc, char** argv) { for (int i = 0; i + 1 < ns; ++i) { acc += 1.0 / layer_ms[(size_t) i]; at[(size_t) i] = std::clamp((int64_t) std::llround(acc / total * (double) L), - i == 0 ? 2 : at[(size_t) i - 1] + 1, L - (ns - 1 - i)); + i == 0 ? 2 : at[(size_t) i - 1] + 1, L - (ns - 1 - i)); } consider(); } @@ -2645,8 +2921,9 @@ int main(int argc, char** argv) { const strata::core::WeightRef* wo_s = st.wt.find("output.weight"); if (wo_s == nullptr || (last && !o.native_head_gguf.empty() && !st.head.load(o.native_head_shards, g.n_embd, wo_s->ne1, err))) { - std::fprintf(stderr, "strata generate: layer split, CUDA%d head: %s\n", st.dev, - wo_s == nullptr ? "output.weight is missing" : err.c_str()); + std::fprintf(stderr, "strata generate: layer split, CUDA%d head: %s%s\n", st.dev, + wo_s == nullptr ? "output.weight is missing" : err.c_str(), + wo_s == nullptr ? "" : vram_free_note().c_str()); return 1; } size_t fb = 0, tb = 0; @@ -2656,6 +2933,79 @@ int main(int argc, char** argv) { last ? " and the head" : "", (double) fb / 1073741824.0); } + // --batch: every stage carves o.batch more sessions like its own (same layer range and context), one per + // slot of the batch windows. Before the expert cache is sized, so `--expert-cache auto` leaves them room. + // Recommend, never force: a count the engine cannot run is said and adjusted (or batching left off) - the + // engine still starts and serves one request at a time. + std::vector>> bslot_ss; + if (o.batch != 0) { + const int cap = strata::kernels::kVerifyMaxT; + const char* off = !o.serve ? "it needs --serve" : o.batch < 2 ? "it needs 2 or more slots" + : split_same ? "it needs each stage of a layer split on its own GPU" : nullptr; + if (off != nullptr) { + std::fprintf(stderr, "strata generate: WARNING: --batch %d is off: %s\n", o.batch, off); + o.batch = 0; + } else if (o.batch > cap) { + std::fprintf(stderr, "strata generate: WARNING: --batch %d: a batch window holds at most %d rows, so %d " + "slots\n", o.batch, cap, cap); + o.batch = cap; + } + } + if (o.batch > 0 && o.batch_groups > 1 && (stages.empty() || o.batch % o.batch_groups != 0)) { + std::fprintf(stderr, "strata generate: WARNING: --batch-groups %d needs a layer split and to divide --batch %d; " + "one group\n", o.batch_groups, o.batch); + o.batch_groups = 1; + } + if (o.batch > 0) { + bslot_ss.resize(1 + stages.size()); + int fit = o.batch; // the slots every stage could carve + uint64_t bytes0 = 0; + for (size_t k = 0; k < bslot_ss.size(); ++k) { + const int dev = k == 0 ? 0 : stages[k - 1]->dev; + const int64_t lo = k == 0 ? 0 : stages[k - 1]->lb; + const int64_t hi = k == 0 ? (multi_gpu ? split_at[0] : -1) : stages[k - 1]->le; + const strata::core::OnDevice on_k(dev); + const uint64_t bytes = strata::core::session_bytes(g, o.max_context, K, lo, hi); + if (k == 0) bytes0 = bytes; + for (int b = 0; b < fit; ++b) { + auto u = std::make_unique(); + void* buf = nullptr; + if (cudaMalloc(&buf, bytes) != cudaSuccess || strata::core::session_init(g, o.max_context, K, buf, *u, lo, hi) == 0) { + cudaGetLastError(); + if (buf != nullptr) cudaFree(buf); + size_t fb = 0, tb = 0; + cudaMemGetInfo(&fb, &tb); + std::fprintf(stderr, "strata generate: WARNING: --batch %d: slot %d's session does not fit on CUDA%d " + "(%.2f GiB each, %.2f GiB free); %d slot%s\n", o.batch, b, dev, + (double) bytes / 1073741824.0, (double) fb / 1073741824.0, b, b == 1 ? "" : "s"); + fit = b; + break; + } + strata::core::session_zero(*u, g, nullptr, nullptr); + bslot_ss[k].push_back(std::move(u)); + } + cudaDeviceSynchronize(); + size_t fb = 0, tb = 0; + cudaMemGetInfo(&fb, &tb); + std::fprintf(stderr, "strata generate: --batch: %d slot sessions on CUDA%d (%.2f GiB each); %.2f GiB free\n", + (int) bslot_ss[k].size(), dev, (double) bytes / 1073741824.0, (double) fb / 1073741824.0); + } + for (auto& v : bslot_ss) if ((int) v.size() > fit) v.resize((size_t) fit); // the same count on every stage + if (fit < 2) { + std::fprintf(stderr, "strata generate: WARNING: --batch is off: not two slot sessions fit (a smaller " + "--max-context or KV streaming, --kv-resident, makes them smaller)\n"); + bslot_ss.clear(); + o.batch = 0; + } else { + o.batch = fit; + if (o.batch_groups > 1 && o.batch % o.batch_groups != 0) o.batch_groups = 1; + // the sessions' VRAM is the expert cache's: say what it costs (docs/BATCHING.md has the measured trade) + std::fprintf(stderr, "strata generate: --batch %d: the slot sessions take %.2f GiB of VRAM on CUDA0 that the " + "expert cache would otherwise hold\n", o.batch, + (double) bytes0 * o.batch / 1073741824.0); + } + } + // Secure MTP's CUDA0 allocations before the large host arena is registered with both CUDA contexts. // In particular WDDM can refuse the draft weights after mapping tens of GiB of host pages. strata::core::MtpDrafter mtp; @@ -2670,7 +3020,33 @@ int main(int argc, char** argv) { static const strata::core::ModelGeometry draft_geometry{}; // with a layer split across GPUs the drafter reads the last stage's residual: it lives on that device const strata::core::OnDevice on_mtp(last_st ? last_st->dev : -1); - if (!o.mtp.empty() && !mtp.load(o.mtp, draft_geometry, last_st ? last_st->ss : ss, o.spec, err, o.mtp_window)) { std::fprintf(stderr, "strata generate: %s\n", err.c_str()); return 1; } + if (!o.mtp.empty() && !mtp.load(o.mtp, draft_geometry, last_st ? last_st->ss : ss, o.spec, err, o.mtp_window)) { std::fprintf(stderr, "strata generate: %s%s\n", err.c_str(), vram_free_note().c_str()); return 1; } + mtp.set_ple_session(&ss); + } + // THE HEAD BEFORE THE CACHE, AND BEFORE THE ARENA. The expert cache takes what is free minus the reserve, so + // everything allocated after it comes out of the reserve. The native head (~0.5 GB with IQ3_S) was loaded after + // it and ate most of the 700 MiB: 128K IQ3_S ended with 30 MiB free, the driver paged, and a request stalled for + // good at its first verify window. Loaded first, the cache is sized around it. #620: and before the expert + // arena registers tens of GiB of host pages, like the drafter above - WDDM then refused the head's cudaMalloc on + // a 16 GB card with the desktop on the iGPU ("native head upload: out of memory" with GiBs free). + const strata::core::WeightRef* wo = wt.find("output.weight"); + if (wo == nullptr) { std::fprintf(stderr, "strata generate: output.weight is missing\n"); return 1; } + const int64_t n_vocab = wo->ne1; + strata::core::NativeHead native_head; + if (!o.native_head_gguf.empty() && !multi_gpu) { // a layer split's head is on its last stage + if (!native_head.load(o.native_head_shards, g.n_embd, n_vocab, err)) { + std::fprintf(stderr, "strata generate: %s%s\n", err.c_str(), vram_free_note().c_str()); + return 1; + } + std::fprintf(stderr, "strata generate: experimental native Q5_K head, %llu bytes\n", + (unsigned long long) native_head.weight_bytes()); + } + std::vector logits((size_t) n_vocab); + float* d_logits = nullptr; + if (cudaMalloc(&d_logits, (size_t) n_vocab * 4) != cudaSuccess) { + (void) cudaGetLastError(); + std::fprintf(stderr, "strata generate: the logits buffer failed%s\n", vram_free_note().c_str()); + return 1; } // Create the additional contexts after MTP has secured CUDA0 memory, but // before the host arena maps its expert pages into their address spaces. @@ -2726,6 +3102,7 @@ int main(int argc, char** argv) { std::fprintf(stderr, "strata generate: GPU %d: %s, compute capability %d.%d%s\n", dev, name, strata::cc_major_of(p.major), strata::cc_minor_of(p.minor), strata::emulated_cc() ? " (STRATA_EMULATE_CC: a test mode, the card is emulated)" : ""); +#if !defined(STRATA_HIP_GFX906) // a CUDA toolkit mismatch; the gfx906 HIP build has no CUDART_VERSION { // #542: a build whose libcudart is older than its headers (a CUDA 13 kit with a dangling libcudart.so that // CMake resolved to the system's CUDA 12 one) reads cudaDeviceProp shifted - silently, and slowly int rt = 0; @@ -2736,6 +3113,7 @@ int main(int argc, char** argv) { "toolkit>, with its libcudart.so present) (#542)\n", CUDART_VERSION / 1000, CUDART_VERSION % 1000 / 10, rt / 1000, rt % 1000 / 10); } +#endif #endif const std::string e = strata::core::device_code_error(); if (!e.empty()) { @@ -2793,6 +3171,8 @@ int main(int argc, char** argv) { std::fprintf(stderr, "strata generate: %s\n", err.c_str()); return 1; } + if (!arena_src.ram_warning().empty()) // #633: said before the load's numbers, which it explains + std::fprintf(stderr, "strata generate: WARNING: %s\n", arena_src.ram_warning().c_str()); std::fprintf(stderr, "strata generate: expert arena: %s\n", arena_src.note().c_str()); if (arena_src.reused_shared_population()) { std::fprintf(stderr, "strata generate: shared expert population already ready; source load skipped\n"); @@ -2831,28 +3211,7 @@ int main(int argc, char** argv) { // numbers if the slots do not fit, instead of handing back a cache smaller than it was asked for. mem_mark("the weights, the session and the drafter"); strata::core::ExpertCache xcache; - // THE HEAD BEFORE THE CACHE. The expert cache takes what is free minus the reserve, so everything allocated - // after it comes out of the reserve. The native head (~0.5 GB with IQ3_S) was loaded after it and ate most of - // the 700 MiB: 128K IQ3_S ended with 30 MiB free, the driver paged, and a request stalled for good at its first - // verify window. Loaded first, the cache is sized around it. - const strata::core::WeightRef* wo = wt.find("output.weight"); - if (wo == nullptr) { std::fprintf(stderr, "strata generate: output.weight is missing\n"); return 1; } - const int64_t n_vocab = wo->ne1; - strata::core::NativeHead native_head; - if (!o.native_head_gguf.empty() && !multi_gpu) { // a layer split's head is on its last stage - if (!native_head.load(o.native_head_shards, g.n_embd, n_vocab, err)) { - std::fprintf(stderr, "strata generate: %s\n", err.c_str()); - return 1; - } - std::fprintf(stderr, "strata generate: experimental native Q5_K head, %llu bytes\n", - (unsigned long long) native_head.weight_bytes()); - } - std::vector logits((size_t) n_vocab); - float* d_logits = nullptr; - if (cudaMalloc(&d_logits, (size_t) n_vocab * 4) != cudaSuccess) { - std::fprintf(stderr, "strata generate: the logits buffer failed\n"); - return 1; - } + // THE HEAD BEFORE THE CACHE: the native head and the logits are allocated above, before the expert arena (#620) const bool auto_cache = o.expert_cache < 0; bool reserve_adapted = false; // #496: the auto sizing lowered the reserve so a small card's cache fits if (o.expert_cache < 0) { @@ -2964,6 +3323,24 @@ int main(int argc, char** argv) { } o.expert_cache = (int) sized_slots.size(); } + // #533: --vram-elastic: the cache in physical segments (the VRAM command resizes it between requests). One GPU, + // no helper caches, serve mode: anything else keeps the one cudaMalloc, said once. + if (o.vram_elastic) { + const char* why = !o.serve ? "it works between requests of --serve" + : multi_gpu ? "a layer split has a cache per GPU" + : std::any_of(o.expert_cache_remote.begin(), o.expert_cache_remote.end(), + [](int n) { return n > 0; }) ? "the helper caches on other GPUs" + : o.vram_segment_mib < 64 ? "--vram-segment-mib is below 64" : nullptr; +#if defined(STRATA_USE_HIP) + if (why == nullptr) why = "it is NVIDIA-only for now"; +#endif + if (why != nullptr) { + std::fprintf(stderr, "strata generate: --vram-elastic is off: %s\n", why); + o.vram_elastic = false; + } else { + xcache.set_segment_bytes(o.vram_segment_mib << 20); + } + } if (o.expert_cache > 0) { // keep the first `keep_bytes` of the cache (the profile's hottest experts first); false when nothing is left auto shrink_to = [&](int64_t keep_bytes) -> bool { @@ -3136,6 +3513,12 @@ int main(int argc, char** argv) { // to hold back is the windows - and `free_b` has already lost the drafter. const int64_t room = stage_room(st.dev, true, false); const strata::core::OnDevice on(st.dev); + { + size_t fb = 0, tb = 0; + cudaMemGetInfo(&fb, &tb); + std::fprintf(stderr, "strata generate: layer split, CUDA%d: %.2f GiB free of %.2f, room for experts %.2f GiB\n", + st.dev, (double) fb / 1073741824.0, (double) tb / 1073741824.0, (double) room / 1073741824.0); + } std::vector sized; int64_t used = 0; for (const auto& pr : st.profile) { @@ -3180,6 +3563,8 @@ int main(int argc, char** argv) { std::fprintf(stderr, "strata generate: layer split: CUDA0 runs layers 0-%lld\n", (long long) (split_at[0] - 1)); std::array remote_experts; + std::unique_ptr remote_opt; + if (o.remote_expert_opt && o.expert_cache_remote[0] > 0) remote_opt = std::make_unique(); const bool multi_remote = o.expert_cache_remote[1] > 0 || o.expert_cache_remote[2] > 0; if (o.expert_cache_remote[0] > 0) { if (o.expert_cache <= 0 || profile.empty() || o.no_pool) { @@ -3193,7 +3578,8 @@ int main(int argc, char** argv) { for (const auto& pr : st->profile) if (st->cache.slot_of(pr.first, pr.second) < 0) ranked.push_back(pr); } - if (multi_remote) { + if (multi_remote || std::any_of(o.expert_cache_remote_auto.begin(), o.expert_cache_remote_auto.end(), + [](bool automatic) { return automatic; })) { // The shipped frequency profile names only 8000 of 24576 experts. Once exhausted, // fill remaining VRAM from unranked pairs in expert-then-layer order: this spreads // the tail across all layers instead of concentrating it on layer zero. @@ -3251,8 +3637,10 @@ int main(int argc, char** argv) { if (st->cache.slot_of(pr.first, pr.second) >= 0) claimed[(size_t) pr.first * (size_t) g.n_expert + (size_t) pr.second] = 1; for (int r = 0; r < 3; ++r) if (o.expert_cache_remote[(size_t) r] > 0) { + if (remote_opt) remote_opt->attach(remote_experts[(size_t) r]); if (!remote_experts[(size_t) r].open(remote_dev[r], o.expert_cache_remote[(size_t) r], - g.n_layers, g.n_expert, by_device[(size_t) r], xcache, *srcp, claimed, err)) { + g.n_layers, g.n_expert, by_device[(size_t) r], xcache, *srcp, claimed, err, + o.expert_cache_remote_auto[(size_t) r])) { std::fprintf(stderr, "strata generate: %s\n", err.c_str()); return 1; } @@ -3281,6 +3669,7 @@ int main(int argc, char** argv) { (long long) (peer.resident() + xcache.slots()), (long long) (g.n_layers * g.n_expert)); } + if (remote_opt && !remote_opt->init(err)) { std::fprintf(stderr, "strata generate: %s\n", err.c_str()); return 1; } Drive drive; for (int r = 0; r < 3; ++r) if (o.expert_cache_remote[(size_t) r] > 0) drive.d.remote[drive.d.remote_count++] = &remote_experts[(size_t) r]; @@ -3629,7 +4018,12 @@ int main(int argc, char** argv) { strata::core::doorbell_reset(db); double mix = 0, ffn = 0, post = 0; if (!strata::core::session_replay_stages(g, 0, 0, ss, gr, main_cs, mix, ffn, post, err)) { - std::fprintf(stderr, "strata generate: %s\n", err.c_str()); + // #610: a native (IQ) pack captures no per-layer graphs; the serve path times its window's stages + std::fprintf(stderr, "strata generate: %s%s\n", err.c_str(), + native_pack ? " - a native (IQ) pack has no per-layer graphs to replay. Its decode window's GPU " + "stages are printed per request by the serve path instead: STRATA_VERIFY_PROFILE=1 " + "STRATA_DECODE_TIMING=1 with --serve (docs/DETAILS.md, \"Where a decode window's " + "time goes\")" : ""); return 1; } const int reps = 20; @@ -3900,6 +4294,31 @@ int main(int argc, char** argv) { } thits.d_res = d_res; thits.n_expert = g.n_expert; + // a file-backed arena (STRATA_ARENA_MMAP): the experts no GPU holds are the ones the CPU pool and the + // prompt path will read - start reading them now instead of faulting them in 4 KB at a time mid-request + // ... and the ones a GPU holds are handed back first (STRATA_ARENA_RELEASE=0 keeps them): the fill read the + // whole arena, and left mapped and referenced it crowds every other allocation into swap + { + static const bool keep = std::getenv("STRATA_ARENA_RELEASE") && std::getenv("STRATA_ARENA_RELEASE")[0] == '0'; + rss_probe("before the release"); + uint64_t released = 0; + if (!keep) + for (size_t i = 0; i < host_res.size(); ++i) + if (host_res[i] != strata::core::kNotResident) + released += srcp->release((int64_t) i / g.n_expert, (int64_t) i % g.n_expert); + if (released > 0) + std::fprintf(stderr, "strata generate: expert arena: %.2f GiB of VRAM-held experts handed back to the OS\n", + (double) released / (1024.0 * 1024.0 * 1024.0)); + rss_probe("after the release"); + int64_t pf = 0; + for (size_t i = 0; i < host_res.size(); ++i) + if (host_res[i] == strata::core::kNotResident) { + srcp->prefetch((int64_t) i / g.n_expert, (int64_t) i % g.n_expert); + ++pf; + } + (void) pf; + rss_probe("after the prefetch hints"); + } for (auto& st : stages) { // layer split across GPUs: the same table on every device const strata::core::OnDevice on(st->dev); if (cudaMalloc((void**) &st->d_res, host_res.size() * sizeof(int32_t)) != cudaSuccess || @@ -3979,8 +4398,9 @@ int main(int argc, char** argv) { } strata::prefill::Prefill::set_pinned_share(total ? (double) pinned / (double) total : 1.0); } - auto lend_slots = [&](int64_t c) -> int64_t { - const uint64_t need = strata::prefill::Prefill::bytes_needed(g, ss, c); + // bytes -> slots for CUDA0's cache: exact when it knows its per-slot offsets (a native pack's blobs differ + // per layer), otherwise max_blob each. + auto slots_from_bytes = [&](uint64_t need) -> int64_t { const int64_t blob = (int64_t) strata::kernels::cpu::expert_layout().max_blob; int64_t k = (int64_t) ((need + (uint64_t) blob - 1) / (uint64_t) blob); if (xcache.slot_offsets() != nullptr) { // sized slots: take slots from the end until they hold `need` @@ -3990,6 +4410,18 @@ int main(int argc, char** argv) { } return k; }; + // ...and its inverse, for the ring's room: the bytes the cache's last `k` slots hold. k can arrive <= 0: the + // caller's lend budget is `min(slots - 128, pct * slots / 100)` and goes negative on a cache smaller than 128 + // slots, where `slots - k` would index slot_offsets() past its end. Such a cache lends nothing, so say so. + auto bytes_from_slots = [&](int64_t k) -> uint64_t { + if (k <= 0) return 0; + if (xcache.slot_offsets() != nullptr) + return (uint64_t) (xcache.bytes() - (int64_t) xcache.slot_offsets()[xcache.slots() - k]); + return (uint64_t) k * (uint64_t) strata::kernels::cpu::expert_layout().max_blob; + }; + auto lend_slots = [&](int64_t c) -> int64_t { + return slots_from_bytes(strata::prefill::Prefill::bytes_needed(g, ss, c)); + }; // `lend_bytes` went with the single-cache serve loan: a participant's loan is priced by `part_bytes` from its // OWN cache, and the only other user of the old helper was the serve path's own relayout. auto request_chunk = [](int64_t tokens, int64_t max_chunk) -> int64_t { @@ -4000,7 +4432,7 @@ int main(int argc, char** argv) { return std::min(max_chunk, rounded); }; // The prompt path's chunk and the slots it borrows for its buffers: the requested chunk halved until it fits, - // or with --prefill auto the largest of kAutoChunks whose buffers take at most kAutoLendPct % of the slots (a + // or with --prefill auto the largest chunk whose buffers take at most kAutoLendPct % of the slots (a // lent slot's expert is streamed during the prompt and refilled after it; measured on a 12 GB card, 32K Q2_0 // prompt: 4096 791 tok/s, 6144 878, 8192 973 with 69% of the slots lent). A request lends only what its own // prompt needs (Prefill::relayout), so a big chunk costs short prompts nothing. 0 = none fits. @@ -4013,20 +4445,95 @@ int main(int argc, char** argv) { return v ? (int64_t) std::atoi(v) : (int64_t) (strata::prefill::Prefill::pinned_share() >= 0.9 ? 90 : 85); }(); + // The largest chunk --prefill auto may pick: the operator's ceiling (--prefill auto:N) and never past the + // context, but a bare `auto` always reaches 8192. 32768 and 16384 stay opt-in (#282): a 32K prompt with + // IQ2_XS (RTX 5090, 64K context) read at 5,624 tok/s in 8192-token chunks and 6,465 in one 32768 chunk (40K + // -> 18K experts streamed; an NVFP4 pack at 262K: 3,535 -> 5,201). + const int64_t auto_ceiling = std::max(8192, std::min(o.prefill_auto_max, o.max_context)); + // The auto scan used to walk a fixed list of sizes - 32768, 16384, 8192, 6144, ... - and take the first that + // fit. That list is coarse exactly where a rig needs it: this one affords ~8,700 tokens and was handed 8192, + // and 8704 is not on it. `bytes_needed` is a sum of (T x positive constant) terms plus a max of such sums, + // so it rises monotonically with T, and so does every test the scan applies - which makes the largest size + // that fits a bisection on the 256-token grid the prompt path already works on (`request_chunk` rounds up to + // it). log2(32768/256) = 7 probes, against the list's 10, at one `bytes_needed` per probe. + auto biggest_chunk = [](int64_t ceiling, auto&& ok) -> int64_t { + if (ceiling < 256) return 0; + int64_t lo = 1, hi = ceiling / 256, best = 0; // n = T / 256, and ok() is monotone in n + while (lo <= hi) { + const int64_t mid = lo + (hi - lo) / 2; + if (ok(mid * 256)) { best = mid * 256; lo = mid + 1; } else hi = mid - 1; + } + return best; + }; auto plan_lend = [&](int64_t& chunk) -> int64_t { - // 32768 and 16384 (#282, opt-in: --prefill auto:32768): a 32K prompt with IQ2_XS (RTX 5090, 64K context) - // read at 5,624 tok/s in 8192-token chunks and 6,465 in one 32768 chunk (40K -> 18K experts streamed; an - // NVFP4 pack at 262K: 3,535 -> 5,201) - static constexpr int64_t kAutoChunks[] = {32768, 16384, 8192, 6144, 4096, 3072, 2048, 1024, 512, 256}; auto slots_for = lend_slots; - if (o.prefill_auto) { + // 0.1.39's list with its ring (also the size up to which a prompt keeps that ring under #583) + auto old_rule = [&]() -> int64_t { + strata::prefill::Prefill::set_ring_budget(0, 0); + static constexpr int64_t kAutoChunks[] = {32768, 16384, 8192, 6144, 4096, 3072, 2048, 1024, 512, 256}; for (const int64_t c : kAutoChunks) { // above 8192: only when asked for, and only when a prompt of the context can use it if (c > 8192 && (c > o.prefill_auto_max || c > o.max_context)) continue; const int64_t k = slots_for(c); - if (k + 128 <= xcache.slots() && k * 100 <= kAutoLendPct * xcache.slots()) { chunk = c; return k; } + if (k + 128 <= xcache.slots() && k * 100 <= kAutoLendPct * xcache.slots()) return c; } return 0; + }; + if (o.prefill_auto && !strata::prefill::Prefill::ring_bytes_enabled()) { // STRATA_RING_BYTES=0: 0.1.39's list + const int64_t c = old_rule(); + if (c > 0) { chunk = c; return slots_for(c); } + return 0; + } + if (o.prefill_auto) { // the default since 0.1.39b (#583) + // The chunk and the ring are one budget, and the ring is the better buy. Measured on this 4-way + // IQ3_S rig (2x RTX 3060 + 2x RTX 5060, CUDA3 lending at its 90% cap) on a 120K prompt: 8960 tokens + // with the 17-slot ring that leaves reads at 963 tok/s, 8192/130 at 1,008, 7168 with the ring full at + // its 199-slot byte budget at 1,000, and 5632/199 at 915. A ring slot is worth ~0.53 tok/s there and + // a chunk token ~0.05, so the 69 slots between a full ring and 8192's 130 are worth more than the + // 512 chunk tokens they cost - and once the ring IS full, shrinking the chunk further buys nothing. + // So: the largest chunk that still leaves the ring full. Only a rig where no chunk at all can afford + // one falls back to the old rule, which takes the largest chunk whose ring clears kRingMin (the + // value at or below which ring_slots() returns STAGE and streaming is off). + constexpr int64_t kRingMin = 16; + const int64_t small = old_rule(); // 0.1.39's chunk (and its ring for the prompts that fit it) + const int64_t ring_max = strata::prefill::Prefill::ring_cap_for(small); + const int64_t budget = std::min(xcache.slots() - 128, kAutoLendPct * xcache.slots() / 100); + // The room is a BYTE budget. A ring slot is max_blob bytes (`carve` lays out one whole blob each), + // while these cache slots hold their own layer's blob, which is smaller than max_blob unless the cache + // happens to hold the pack's biggest layer - on the 4-way IQ3_S rig that is 2.15 MiB a slot against a + // 2.54 MiB max_blob. Counting the room in slots priced the ring at 0.85x what it costs, `fits` then + // rejected the whole chunk instead of shrinking the ring, and the scan stopped a step short: measured + // against chunk 6144/ring 199, 8192 was refused where the same bytes afford a ~87-slot ring. + const int64_t blob = (int64_t) strata::kernels::cpu::expert_layout().max_blob; + const uint64_t avail = bytes_from_slots(budget); + auto room_of = [&](int64_t t) -> int64_t { + const uint64_t nonring = strata::prefill::Prefill::bytes_needed_no_ring(g, ss, t); + return std::min((int64_t) ((avail - std::min(avail, nonring)) / (uint64_t) blob), ring_max); + }; + // `room_of` is capped at ring_max, so "the ring is full" is exactly room == ring_max, and both that + // test and the ones below it only get harder as t grows - the bisection stays valid. + auto scan = [&](int64_t floor) -> int64_t { + return biggest_chunk(auto_ceiling, [&](int64_t t) -> bool { + const int64_t room = room_of(t); + if (room < floor) return false; + strata::prefill::Prefill::set_ring_budget((int) room, 0); + // ring_slots() now returns `room` - unless STRATA_PREFILL_RING overrides it, in which case the + // chunk has to fit THAT ring, which is the pre-fix rule and the A/B arm + const int64_t k = slots_for(t); + return k + 128 <= xcache.slots() && k * 100 <= kAutoLendPct * xcache.slots(); + }); + }; + int64_t c = scan(ring_max); + if (c == 0 && ring_max < strata::prefill::Prefill::ring_default_slots()) c = scan(kRingMin); + // a prompt that fits 0.1.39's chunk keeps 0.1.39's ring (one chunk: the smaller ring only slowed it) + if (small >= c) { // the scan bought nothing: 0.1.39's choice + strata::prefill::Prefill::set_ring_budget(0, 0); + if (small > 0) { chunk = small; return slots_for(small); } + return 0; + } + strata::prefill::Prefill::set_ring_budget((int) room_of(c), small); + chunk = c; + return slots_for(c); } for (int64_t c = chunk; c >= 256; c /= 2) { const int64_t k = slots_for(c); @@ -4094,6 +4601,19 @@ int main(int argc, char** argv) { std::fprintf(stderr, "strata generate: CPU expert residency: %s\n", err.c_str()); return 1; } + // #577: the unbuffered choice above was made before the RAM copy existed, from the budget asked for; now the + // copy is built, decide again from the RAM it really holds and the expert bytes outside it (on a 96 GB PC + // the file cache keeps those, and every refill after a prompt read the drive instead). Windows only: the + // unbuffered reads exist there alone +#if defined(_WIN32) + if (o.mmap_experts && o.resident_budget > 0) { + std::string why; + const bool was = src.unbuffered(); + const bool ub = src.recheck_unbuffered(why); + std::fprintf(stderr, "strata generate: the file tier reads %s%s (%s)\n", + ub ? "unbuffered" : "through the file cache", ub == was ? "" : " (changed)", why.c_str()); + } +#endif } if (o.serve) { if (o.spec < 2 || o.mtp.empty() || o.prefill_chunk <= 0 || @@ -4127,9 +4647,9 @@ int main(int argc, char** argv) { int64_t lent_chunk = 0; std::vector> lent; }; - auto part_slots = [&](const PfPart& p, int64_t c) -> int64_t { - const uint64_t need = strata::prefill::Prefill::bytes_needed(g, *p.ses, c); - strata::core::ExpertCache& xc = *p.cache; + // bytes -> slots for one cache: exact when it knows its per-slot offsets (a native pack's blobs differ + // per layer), otherwise max_blob each. + auto cache_slots_for = [&](const strata::core::ExpertCache& xc, uint64_t need) -> int64_t { if (xc.slot_offsets() != nullptr) { // sized slots: from the end until they hold `need` int64_t k = 0; while (k < xc.slots() && @@ -4139,6 +4659,9 @@ int main(int argc, char** argv) { const int64_t blob = (int64_t) strata::kernels::cpu::expert_layout().max_blob; return (int64_t) ((need + (uint64_t) blob - 1) / (uint64_t) blob); }; + auto part_slots = [&](const PfPart& p, int64_t c) -> int64_t { + return cache_slots_for(*p.cache, strata::prefill::Prefill::bytes_needed(g, *p.ses, c)); + }; auto part_bytes = [&](const PfPart& p, int32_t first) -> uint64_t { strata::core::ExpertCache& xc = *p.cache; return xc.slot_offsets() ? (uint64_t) (xc.bytes() - (int64_t) xc.slot_offsets()[first]) @@ -4185,54 +4708,121 @@ int main(int argc, char** argv) { if (!fits_one(p, c, cap)) return false; return true; }; - static constexpr int64_t kAutoChunks[] = {32768, 16384, 8192, 6144, 4096, 3072, 2048, 1024, 512, 256}; - auto pick = [&](const PfPart* only) -> int64_t { - if (o.prefill_auto) { - for (const int64_t c : kAutoChunks) { - if (c > 8192 && (c > o.prefill_auto_max || c > o.max_context)) continue; // #282, as plan_lend - if (fits(c, true, only)) return c; - } - } else { + // The chunk and the ring are one budget. The ring used to be a fixed 384 slots taken off the top + // before the chunk was considered: 506 MiB on a Q2_0 pack, where it was tuned, but 1,912 MiB on + // Q8_0, more than half an 8 GB card's cache - which is what kept that card at a 1024-token chunk. + // Size the chunk first and hand the ring what the smallest participant leaves over, up to the ring's + // byte budget - but hand it ALL of it, as in plan_lend: the ring is the better buy per byte (the + // measurements are there), so the chunk to take is the largest one that still leaves the ring full, + // not the largest one that leaves it anything at all. kRingMin is the fallback floor for a rig where + // no chunk can afford a full ring; ring_slots() returns STAGE at or below it, which turns streaming + // off. + constexpr int64_t kRingMin = 16; + // The room is a BYTE budget, as in plan_lend: a ring slot is max_blob bytes, but a cache slot holds + // its own layer's blob, which on this rig averages 0.85 of it. In slots the ring looked 18% cheaper + // than it is, and a chunk whose ring only fitted after that discount was refused outright - the scan + // stopped one step short while the chunk it did take had room to spare for the cap-sized ring. + const int64_t kBlob = (int64_t) strata::kernels::cpu::expert_layout().max_blob; + auto ring_room = [&](int64_t c, const PfPart* only, int64_t cap) -> int64_t { + int64_t room = cap; + for (const PfPart& p : pf_parts) { + if (only != nullptr && &p != only) continue; + // clamped at 0: a cache under 128 slots has a negative lend budget, and `slots - budget` is + // then `slots`, which is off_[slots] - the end of the prefix sum, so that cache lends 0 bytes + // rather than reading one entry past its offsets + const int64_t budget = std::max(0, std::min(p.cache->slots() - 128, + kAutoLendPct * p.cache->slots() / 100)); + const uint64_t avail = part_bytes(p, (int32_t) (p.cache->slots() - budget)); + const uint64_t nonring = strata::prefill::Prefill::bytes_needed_no_ring(g, *p.ses, c); + room = std::min(room, (int64_t) ((avail - std::min(avail, nonring)) / (uint64_t) kBlob)); + } + return room; + }; + // The largest chunk on the 256-token grid that `only` - or, with null, every stage - can lend with the + // ring still at its full byte budget (ring_room is capped at ring_max, so that is room == ring_max), + // under the operator's ceiling (auto_ceiling keeps a bare `auto` at 8192, #282). Failing that, the + // largest chunk whose ring clears kRingMin. + // 0.1.39's list with its ring: the chunk it would pick, and the size up to which a prompt keeps that ring + auto old_pick = [&](const PfPart* only) -> int64_t { + strata::prefill::Prefill::set_ring_budget(0, 0); + static constexpr int64_t kAutoChunks[] = {32768, 16384, 8192, 6144, 4096, 3072, 2048, 1024, 512, 256}; + for (const int64_t c : kAutoChunks) { + if (c > 8192 && (c > o.prefill_auto_max || c > o.max_context)) continue; // #282, as plan_lend + if (fits(c, true, only)) return c; + } + return 0; + }; + auto scan = [&](const PfPart* only) -> int64_t { + if (!o.prefill_auto) { // an explicit --prefill is the operator's number, and a loan of it only has to fit for (int64_t c = o.prefill_chunk; c >= 256; c /= 2) if (fits(c, false, only)) return c; + return 0; } - return 0; + if (!strata::prefill::Prefill::ring_bytes_enabled()) return old_pick(only); // STRATA_RING_BYTES=0 + const int64_t small = old_pick(only); + const int64_t cap = strata::prefill::Prefill::ring_cap_for(small); + auto probe = [&](int64_t floor) -> int64_t { + return biggest_chunk(auto_ceiling, [&](int64_t t) -> bool { + const int64_t room = ring_room(t, only, cap); + if (room < floor) return false; + strata::prefill::Prefill::set_ring_budget((int) room, 0); + // ring_slots() now returns `room` - unless STRATA_PREFILL_RING overrides it, in which + // case the chunk has to fit THAT ring, which is the pre-fix rule and the A/B arm + return fits(t, true, only); + }); + }; + int64_t c = probe(cap); + if (c == 0 && cap < strata::prefill::Prefill::ring_default_slots()) c = probe(kRingMin); + return std::max(c, small); // never a smaller chunk than 0.1.39's }; - const int64_t chunk = pick(nullptr); + const int64_t chunk = scan(nullptr); // #448: a small card in a layer split caps every stage's chunk (an RTX 3080's 512-slot cache held a // 32 GB card's split to 512 tokens: prompts 6.2x slower, decode the same). Named when it bites, so the // regression is one log line: each stage that cannot fund the chunk CUDA0 alone would read in. - if (pf_parts.size() > 1) { - const int64_t alone = pick(&pf_parts[0]); - if (alone > chunk) { - for (size_t i = 1; i < pf_parts.size(); ++i) { - const PfPart& p = pf_parts[i]; - if (fits_one(p, alone, o.prefill_auto)) continue; - const int dev = p.dev < 0 ? 0 : p.dev; - cudaDeviceProp prop{}; - if (cudaGetDeviceProperties(&prop, dev) != cudaSuccess) { - (void) cudaGetLastError(); - prop.name[0] = 0; - } - const std::string pct = - o.prefill_auto ? ", and lend at most " + std::to_string(kAutoLendPct) + "%" : ""; - std::fprintf(stderr, "strata serve: WARNING: prompt chunk %lld tokens, not %lld: CUDA%d (%s) " - "has %lld expert-cache slots, and a %lld-token chunk borrows %lld of them " - "(it must keep 128%s) - prompts read slower than on CUDA0 alone (#448)\n", - (long long) chunk, (long long) alone, dev, prop.name, - (long long) p.cache->slots(), (long long) alone, - (long long) part_slots(p, alone), pct.c_str()); - // the helper tiers start at CUDA1 without a split (and are enabled in order) - std::fprintf(stderr, "strata serve: a card this small can serve as a helper expert cache " - "instead of a split stage: without --layer-split, with %s " - "(docs/SECOND_GPU.md)\n", - dev == 1 ? "--expert-cache-device1 N" : "--expert-cache-device1..3 N, in order"); + const int64_t alone = pf_parts.size() > 1 ? scan(&pf_parts[0]) : chunk; + // both scans left the ring override on their own last trial: put it back on the chunk that won, which + // is what `init` lays out and what the INFO line reports + if (chunk > 0 && o.prefill_auto && strata::prefill::Prefill::ring_bytes_enabled()) { + // a prompt that fits 0.1.39's chunk keeps 0.1.39's ring (one chunk: the smaller ring only slowed + // it); the byte-budget ring is for the chunks past it + const int64_t small = old_pick(nullptr); + strata::prefill::Prefill::set_ring_budget( + small >= chunk ? 0 : (int) ring_room(chunk, nullptr, strata::prefill::Prefill::ring_cap_for(small)), + small); + } + if (chunk > 0 && alone > chunk) { + for (size_t i = 1; i < pf_parts.size(); ++i) { + const PfPart& p = pf_parts[i]; + if (fits_one(p, alone, o.prefill_auto)) continue; + const int dev = p.dev < 0 ? 0 : p.dev; + cudaDeviceProp prop{}; + if (cudaGetDeviceProperties(&prop, dev) != cudaSuccess) { + (void) cudaGetLastError(); + prop.name[0] = 0; } + const std::string pct = + o.prefill_auto ? ", and lend at most " + std::to_string(kAutoLendPct) + "%" : ""; + std::fprintf(stderr, "strata serve: WARNING: prompt chunk %lld tokens, not %lld: CUDA%d (%s) " + "has %lld expert-cache slots, and a %lld-token chunk borrows %lld of them " + "(it must keep 128%s) - prompts read slower than on CUDA0 alone (#448)\n", + (long long) chunk, (long long) alone, dev, prop.name, + (long long) p.cache->slots(), (long long) alone, + (long long) part_slots(p, alone), pct.c_str()); + // the helper tiers start at CUDA1 without a split (and are enabled in order) + std::fprintf(stderr, "strata serve: a card this small can serve as a helper expert cache " + "instead of a split stage: without --layer-split, with %s " + "(docs/SECOND_GPU.md)\n", + dev == 1 ? "--expert-cache-device1 N" : "--expert-cache-device1..3 N, in order"); } } if (chunk > 0) { if (o.prefill_auto) - std::fprintf(stderr, "strata serve: prompt chunk auto: %lld tokens\n", (long long) chunk); + // the RESOLVED ring, not the room offered: ring_slots() clamps to [16, RING_MAX] and a chunk + // under stream_all_min() gets STAGE, and STRATA_PREFILL_RING overrides both. This is what + // `init` lays out, and what the INFO line reports to the Monitor tab. + std::fprintf(stderr, "strata serve: prompt chunk auto: %lld tokens, a %lld-slot ring\n", + (long long) chunk, + (long long) strata::prefill::Prefill::ring_slots_for(chunk)); else if (chunk != o.prefill_chunk) std::fprintf(stderr, "strata serve: prompt chunk %lld -> %lld tokens so its buffers fit in " "every expert cache\n", (long long) o.prefill_chunk, (long long) chunk); @@ -4301,6 +4891,7 @@ int main(int argc, char** argv) { } else { std::fprintf(stderr, "strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)\n"); } + rss_probe("the prompt path set up"); // layer split across GPUs: a prompt path per stage, each handing its chunk's rows to the next. // THE CHUNK STEPS DOWN INSTEAD OF EXITING. A split's stage caches are sized after the arena is registered, and // the prompt path's own buffers (no loan) are not priced into them: with the whole arena pinned (#253) a @@ -4413,6 +5004,19 @@ int main(int argc, char** argv) { err.clear(); } } + // a layer split: a one-chunk prompt runs the stages one after the other, so each stage's prompt path gets the + // next stage's GPU (the last one the first's) to stream and compute a share of its experts meanwhile + if (multi_gpu && !stages.empty()) { + std::vector> paths{{&sp, 0}}; + for (const auto& st : stages) paths.emplace_back(&st->sp, st->dev); + for (size_t i = 0; i < paths.size(); ++i) { + const strata::core::OnDevice on(paths[i].second); + if (!paths[i].first->set_stage_helper(paths[(i + 1) % paths.size()].first, err)) { + std::fprintf(stderr, "strata serve: %s - stage %zu reads its prompts alone\n", err.c_str(), i); + err.clear(); + } + } + } mem_mark("the head and the prompt path"); // #340: STRATA_SPLIT_SMALL_OWN=S (tokens): on a layer split, every stage that borrows keeps the slots for an // S-token chunk's buffers for the whole session (0.1.29's own buffers, carved from the tail of its cache): @@ -4467,6 +5071,7 @@ int main(int argc, char** argv) { strata::core::Verifier ver; strata::core::VerifyHits vh; vh.d_res = thits.d_res; + vh.h_res = host_res.empty() ? nullptr : host_res.data(); vh.cache_base = thits.cache_base; vh.blob = thits.blob; vh.slot_off = xcache.slot_offsets(); // E-6: the device plan's pointers @@ -4507,17 +5112,19 @@ int main(int argc, char** argv) { for (int st = 1; st < n_stages; ++st) { bool ok_s = false; if (split_same) { - ok_s = ver_same.init(wt, g, ss, vh, native_head.loaded() ? &native_head : nullptr, o.spec, err); + ok_s = ver_same.init(wt, g, ss, vh, native_head.loaded() ? &native_head : nullptr, std::max(o.spec, o.batch), err); } else { GpuStage& gs = *stages[(size_t) st - 1]; const strata::core::OnDevice on(gs.dev); strata::core::VerifyHits vs; vs.d_res = gs.d_res; + vs.h_res = host_res.empty() ? nullptr : host_res.data(); vs.cache_base = gs.cache.device_slot(0); vs.blob = thits.blob; vs.slot_off = gs.cache.slot_offsets(); vs.n_slots = gs.cache.slots(); - ok_s = gs.ver.init(gs.wt, g, gs.ss, vs, gs.head.loaded() ? &gs.head : nullptr, o.spec, err); + gs.ver.set_remote_expert_opt(remote_opt.get()); + ok_s = gs.ver.init(gs.wt, g, gs.ss, vs, gs.head.loaded() ? &gs.head : nullptr, std::max(o.spec, o.batch), err); split_drive.cache_base[st] = gs.cache.device_slot(0); split_drive.cache_slot_off[st] = gs.cache.slot_offsets(); split_drive.pcie_num[st] = pcie_num_of(gs.pcie_frac); @@ -4534,11 +5141,25 @@ int main(int argc, char** argv) { " (CUDA" + std::to_string(split_same ? 0 : stages[(size_t) st - 1]->dev) + ")"; std::fprintf(stderr, "strata serve: layer split: layers %s, one hand-off per window\n", plan_s.c_str()); } - if (!ver.init(wt, g, ss, vh, native_head.loaded() ? &native_head : nullptr, o.spec, err) || + ver.set_remote_expert_opt(remote_opt.get()); + if (!ver.init(wt, g, ss, vh, native_head.loaded() ? &native_head : nullptr, std::max(o.spec, o.batch), err) || !mtp.bind(last_st ? last_st->wt : wt, last_st ? &last_st->head : &native_head, ver.final_R_all(), err)) { std::fprintf(stderr, "strata serve: %s\n", err.c_str()); return 1; } + // each stage's verifier gets its stage's slot sessions + if (o.batch > 0) { + for (size_t k = 0; k < bslot_ss.size(); ++k) { + std::vector ptrs; + for (auto& u : bslot_ss[k]) ptrs.push_back(u.get()); + strata::core::Verifier& vk = k == 0 ? ver : stages[k - 1]->ver; + const strata::core::OnDevice on_k(k == 0 ? 0 : stages[k - 1]->dev); + if (!vk.init_slots(ptrs, err)) { + std::fprintf(stderr, "strata serve: --batch, stage %zu: %s\n", k + 1, err.c_str()); + return 1; + } + } + } for (int st = 0; st < n_stages && n_stages > 1; ++st) { split_drive.plan[st] = stage_ver(st).plan_sink(); if (st > 0) { @@ -4577,32 +5198,76 @@ int main(int argc, char** argv) { // addresses change: all parked images live in ordinary host vectors. auto park_current = [&](size_t held) -> bool { if (!conversations.enabled() || !live_ok || live.empty()) return true; - const strata::core::ConversationView view{live, live_imgs, checks, cvec_cached}; + // #342: before make_room evicts oldest-first, the copies of this conversation a turn back go (they hold + // nothing the outgoing chain does not, apart from the tail this conversation rewrote) + if (const size_t dropped = conversations.drop_superseded(live, live_imgs, checks, cvec_cached)) + std::fprintf(stderr, "strata serve: conversation cache: dropped %zu superseded cop%s of this " + "conversation; parked=%zu\n", dropped, dropped == 1 ? "y" : "ies", conversations.size()); + // A layer split parks one image per stage: the first stage's (with the draft layer's K/V) and one per + // later stage (without it - the draft ring is saved once). The checkpoints are MOVED apart into their + // stage parts for the capture and put back together whichever way this ends (no running state copied). + const size_t n_st = stages.size(); + // the draft layer's K/V lives on the last stage's GPU (the drafter is loaded there): with a split it is + // saved with that stage's image, under its device; stage 0's image holds none + const strata::core::QsaState* draft0 = n_st > 0 ? nullptr : &mtp.kv_state(); + auto draft_of = [&](size_t k) -> const strata::core::QsaState* { return k + 1 == n_st ? &mtp.kv_state() : nullptr; }; + strata::core::ConversationCheckpointSplit cs; + if (n_st > 0) cs = strata::core::conversation_checkpoints_split(std::move(checks), n_st); + struct MergeBack { + strata::core::ConversationCheckpointSplit& cs; std::vector& checks; bool on; + ~MergeBack() { if (on && !strata::core::conversation_checkpoints_merge(std::move(cs), checks)) checks.clear(); } + } merge_back{cs, checks, n_st > 0}; + const strata::core::ConversationView view{live, live_imgs, n_st > 0 ? cs.stage0 : checks, cvec_cached}; auto reuse = conversations.take_reuse(); + std::vector stage_reuse = std::move(reuse.stages); + stage_reuse.resize(n_st); + reuse.stages.clear(); + // the later stages' estimates, each with its own retained K/V when it still fits + size_t stage_estimate = 0, stage_retained = 0; + for (size_t k = 0; k < n_st; ++k) { + auto& st = stages[k]; + const strata::core::OnDevice on(st->dev); + const strata::core::ConversationView view_k{live, live_imgs, cs.parts[k], cvec_cached}; + size_t b = 0; + if (!strata::core::conversation_snapshot_bytes(view_k, st->ss, g, draft_of(k), b, err)) { + std::fprintf(stderr, "strata serve: conversation cache: skip parking (stage CUDA%d: %s)\n", + st->dev, err.c_str()); + err.clear(); + return true; + } + if (!stage_reuse[k].kv.empty()) { + size_t r = 0; + if (strata::core::conversation_snapshot_capture_bytes(stage_reuse[k], view_k, st->ss, g, draft_of(k), + r, err)) { + b = r; + stage_retained += stage_reuse[k].bytes(); + } else { + stage_reuse[k] = {}; + err.clear(); + } + } + stage_estimate += b; + } size_t estimate = 0; - if (!strata::core::conversation_snapshot_bytes(view, ss, g, mtp.kv_state(), estimate, err)) { + if (!strata::core::conversation_snapshot_bytes(view, ss, g, draft0, estimate, err)) { std::fprintf(stderr, "strata serve: conversation cache: skip parking (%s)\n", err.c_str()); err.clear(); // A recoverable miss must not poison the batched draft prefill's error channel. return true; } const size_t fresh_estimate = estimate; if (!reuse.kv.empty() && !strata::core::conversation_snapshot_capture_bytes( - reuse, view, ss, g, mtp.kv_state(), estimate, err)) { + reuse, view, ss, g, draft0, estimate, err)) { reuse = {}; estimate = fresh_estimate; err.clear(); } + estimate += stage_estimate; // A park carrying its own retained K/V replaces memory the cache // already held, so capacity is make_room's call - it runs next either // way, and put()'s accounting still bounds the budget. The with-reuse // estimate must stay uncapped: it counts the retained buffers' // capacity and directories, and put() charges that same true size - // a capped figure would under-evict and overfill the budget. - // #342: before make_room evicts oldest-first, the copies of this conversation a turn back go (they hold - // nothing the outgoing chain does not, apart from the tail this conversation rewrote) - if (const size_t dropped = conversations.drop_superseded(live, live_imgs, checks, cvec_cached)) - std::fprintf(stderr, "strata serve: conversation cache: dropped %zu superseded cop%s of this " - "conversation; parked=%zu\n", dropped, dropped == 1 ? "y" : "ies", conversations.size()); if (!conversations.make_room(estimate, held)) { std::fprintf(stderr, "strata serve: conversation cache: skip parking (snapshot %zu MiB exceeds available budget)\n", estimate >> 20); @@ -4611,7 +5276,8 @@ int main(int argc, char** argv) { const auto t0 = Clock::now(); try { const uint64_t floor = (uint64_t) o.conversation_cache_min_free_mib * 1024 * 1024; - const size_t additional = estimate - reuse.bytes(); + const size_t retained = reuse.bytes() + stage_retained; + const size_t additional = estimate > retained ? estimate - retained : 0; if (!strata::core::conversation_memory_admit(strata::core::conversation_available_memory(), additional, floor)) { std::fprintf(stderr, "strata serve: conversation cache: skip parking (physical RAM admission; need %zu MiB plus %lld MiB floor, or telemetry unavailable)\n", @@ -4620,8 +5286,19 @@ int main(int argc, char** argv) { } strata::core::SavedConversation image; size_t reused_bytes = 0; - if (!strata::core::conversation_snapshot_save(image, view, ss, g, mtp.kv_state(), err, + if (!strata::core::conversation_snapshot_save(image, view, ss, g, draft0, err, std::move(reuse), &reused_bytes)) return false; + for (size_t k = 0; k < n_st; ++k) { // the later stages (the last one with the draft layer's K/V) + auto& st = stages[k]; + const strata::core::OnDevice on(st->dev); + if (cudaDeviceSynchronize() != cudaSuccess) { err = "stage sync failed"; return false; } + const strata::core::ConversationView view_k{live, live_imgs, cs.parts[k], cvec_cached}; + strata::core::SavedConversation part; + if (!strata::core::conversation_snapshot_save(part, view_k, st->ss, g, draft_of(k), err, + std::move(stage_reuse[k]), &reused_bytes)) + return false; + image.stage_images.push_back(std::move(part)); + } if (!strata::core::conversation_memory_admit(strata::core::conversation_available_memory(), 0, floor)) { std::fprintf(stderr, "strata serve: conversation cache: skip parking (physical RAM floor after capture, or telemetry unavailable)\n"); return true; @@ -4658,7 +5335,18 @@ int main(int argc, char** argv) { std::vector part_next(stages.size() + 1, INT64_MAX); // a checkpoint of the state after `cur[0, L)`; false only when the copy itself failed. `parts`: the stages' // states saved at L (a split's mid-prompt checkpoint); without, they are read now (everything is at L) + // #613: why the last checkpoint_at failed - a failed device sync is the GPU itself (a hang Windows then resets), + // not the checkpoint, and the error says so + std::string ckpt_why; + auto gpu_sync_ok = [&]() -> bool { + const cudaError_t e = cudaDeviceSynchronize(); + if (e == cudaSuccess) return true; + ckpt_why = std::string(": the GPU stopped responding before it (") + cudaGetErrorString(e) + + ") - a GPU hang; on Windows the driver is then reset"; + return false; + }; auto checkpoint_at = [&](int64_t L, std::vector* parts = nullptr) -> bool { + ckpt_why.clear(); if (o.prompt_cache <= 0 || L < 1) return true; for (ConvCheckpoint& c : checks) if ((int64_t) c.ids.size() == L) { c.used = ++check_clock; return true; } @@ -4674,12 +5362,12 @@ int main(int argc, char** argv) { c.block_pos = std::move((*parts)[0].block_pos); for (size_t i = 1; i < parts->size(); ++i) c.stage_parts.push_back(std::move((*parts)[i])); } else { - if (cudaDeviceSynchronize() != cudaSuccess || !checkpoint_save(c, ss, g)) return false; + if (!gpu_sync_ok() || !checkpoint_save(c, ss, g)) return false; for (auto& st : stages) { // a layer split's later stages: their sessions' part const strata::core::OnDevice on(st->dev); ConvCheckpoint part; part.ids = c.ids; - if (cudaDeviceSynchronize() != cudaSuccess || !checkpoint_save(part, st->ss, g)) return false; + if (!gpu_sync_ok() || !checkpoint_save(part, st->ss, g)) return false; c.stage_parts.push_back(std::move(part)); } } @@ -4729,7 +5417,7 @@ int main(int argc, char** argv) { } else { saved = checkpoint_at(done); } - if (!saved) { e = "saving a conversation checkpoint failed"; return false; } + if (!saved) { e = "saving a conversation checkpoint failed" + ckpt_why; return false; } pp_next_check = done + o.prompt_cache_every; } return true; @@ -4756,24 +5444,14 @@ int main(int argc, char** argv) { } drive.d.plan = ver.plan_sink(); drive.d.pcie_num = std::max(0, std::min(256, (int) (o.pcie_frac * 256.0 + 0.5))); - const bool adapt_trace = std::getenv("STRATA_ADAPT_TRACE") != nullptr; - auto adapt_now_ns = []() -> uint64_t { - return (uint64_t) std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count(); - }; - if (o.adapt_every > 0 && o.adapt_swaps > 0) { - const size_t nr = (size_t) (g.n_layers * g.n_expert); - drive.d.usage.assign(nr, 0.0f); - if (adapt_trace) { - drive.d.first_miss_ns.assign(nr, 0); - drive.d.first_resident_ns.assign(nr, 0); - drive.d.traced_hit_reported.assign(nr, 0); - } - } + const bool all_experts_resident = !host_res.empty() && + std::all_of(host_res.begin(), host_res.end(), [](int32_t r) { return r >= 0; }); + if (o.adapt_every > 0 && o.adapt_swaps > 0 && !all_experts_resident) + drive.d.usage.assign((size_t) (g.n_layers * g.n_expert), 0.0f); // #477 --expert-profile-save: what the adaptive tier learned, kept across restarts (opt-in; off: `heat` stays - // empty and nothing below runs). It needs the adaptive tier's counts and the residency table. + // empty and nothing below runs). It needs the adaptive tier's counts and the residency table. std::vector heat; - if (!o.expert_profile_save.empty()) { + if (!o.expert_profile_save.empty() && !all_experts_resident) { if (drive.d.usage.empty() || host_res.empty()) std::fprintf(stderr, "strata serve: --expert-profile-save needs the adaptive tier (--adapt-every and " "--adapt-swaps above 0) and --expert-profile: nothing will be saved\n"); @@ -4786,13 +5464,9 @@ int main(int argc, char** argv) { std::fprintf(stderr, "strata serve: cannot create the refill stream\n"); return 1; } - // plan v0.3 P6: swaps in flight, admitted only after the asynchronous H2D copy completed. - struct PendingSwap { - int32_t index, slot, layer, in_expert, out_expert; - uint64_t first_miss_ns, copy_start_ns, bytes; - float gain; - }; - std::vector pending; + // plan v0.3 P6: swaps in flight - (residency index, slot) admitted when adapt_ev has completed + std::vector> pending; + std::vector pin_live; // the swaps' locked arena pages (pin_blob), unlocked once they have landed cudaEvent_t adapt_ev = nullptr; cudaEventCreateWithFlags(&adapt_ev, cudaEventDisableTiming); // a layer split's later stages keep a copy of the residency table on their devices, and swap on their own @@ -4815,30 +5489,19 @@ int main(int argc, char** argv) { else if (cudaEventQuery(st->adapt_ev) != cudaSuccess) return; } for (auto& st : stages) st->adapt_live = false; - // Upstream 0.1.30 resident-expert mode must commit the staged RAM complement - // before publishing the new GPU residency table. The fork's trace records the - // same publication point without changing the default path. - src.commit_exchanges(); - const uint64_t resident_ns = adapt_trace ? adapt_now_ns() : 0; - for (const PendingSwap& p : pending) { - host_res[(size_t) p.index] = p.slot; - if (adapt_trace) { - drive.d.first_resident_ns[(size_t) p.index] = resident_ns; - std::fprintf(stderr, - "ADAPT_TRACE resident layer=%d expert=%d evicted=%d slot=%d bytes=%llu gain=%.3f copy_us=%.3f miss_to_resident_us=%.3f\n", - (int) p.layer, (int) p.in_expert, (int) p.out_expert, (int) p.slot, - (unsigned long long) p.bytes, (double) p.gain, - (double) (resident_ns - p.copy_start_ns) / 1000.0, - p.first_miss_ns ? (double) (resident_ns - p.first_miss_ns) / 1000.0 : -1.0); - } + unpin_blobs(pin_live); + src.commit_exchanges(); // the resident RAM mode: the evicted experts take their places in RAM + for (const auto& [i, slot] : pending) { + host_res[(size_t) i] = slot; + srcp->release((int64_t) i / g.n_expert, (int64_t) i % g.n_expert); // in VRAM now: RAM not needed } - if (adapt_trace && !pending.empty()) std::fflush(stderr); pending.clear(); res_upload(); }; // the VRAM tier follows the conversation (the same rule as the speculative loop below) auto adapt = [&]() -> bool { if (!pending.empty()) return true; // the previous swaps are still in flight + if (remote_opt && !remote_opt->adapt(drive.d.usage, host_res, pending, o.adapt_swaps, *srcp)) return false; struct Swap { float gain; int32_t layer, in, out; }; std::vector swaps; std::vector> cand, vict; @@ -4848,7 +5511,7 @@ int main(int argc, char** argv) { const float* u = drive.d.usage.data() + l * g.n_expert; const int32_t* r = host_res.data() + l * g.n_expert; for (int32_t e = 0; e < (int32_t) g.n_expert; ++e) { - if (r[e] < 0) { if (u[e] >= 2.0f && !(peer.valid() && peer.has(l, e))) cand.emplace_back(u[e], e); } + if (r[e] < 0) { if (u[e] >= 2.0f && !(peer.valid() && peer.has(l, e)) && !(remote_opt && remote_opt->owns(l, e))) cand.emplace_back(u[e], e); } else vict.emplace_back(u[e], e); } if (cand.empty() || vict.empty()) continue; @@ -4864,32 +5527,35 @@ int main(int argc, char** argv) { std::sort(swaps.begin(), swaps.end(), [](const Swap& a, const Swap& b) { return a.gain > b.gain; }); if ((int) swaps.size() > o.adapt_swaps) swaps.resize((size_t) o.adapt_swaps); if (!resident_stage_swaps(src, xcache, host_res, g.n_expert, swaps, adapt_stream)) return false; + if (pin_blobs_on()) { // lock the batch's source pages (a file-backed arena on AMD; see pin_blobs) + std::vector> spans; + for (const Swap& s : swaps) + if (const uint8_t* b = srcp->blob(s.layer, s.in)) + spans.emplace_back((uintptr_t) b, + (uintptr_t) b + strata::kernels::cpu::expert_layout().blob_bytes(s.layer)); + pin_blobs(std::move(spans), pin_live); + } bool main_live = false; for (const Swap& s : swaps) { const size_t in = (size_t) s.layer * g.n_expert + s.in, out = (size_t) s.layer * g.n_expert + s.out; const int32_t slot = host_res[out]; const uint8_t* b = srcp->blob(s.layer, s.in); - const uint64_t bytes = strata::kernels::cpu::expert_layout().blob_bytes(s.layer); - const uint64_t copy_start_ns = adapt_trace ? adapt_now_ns() : 0; const int stn = multi_gpu ? stage_of(s.layer) : 0; // the swap stays in the layer's own cache GpuStage* gs = stn > 0 ? stages[(size_t) stn - 1].get() : nullptr; const strata::core::OnDevice on(gs ? gs->dev : -1); if (slot < 0 || b == nullptr || cudaMemcpyAsync(gs ? gs->cache.device_slot(slot) : xcache.device_slot(slot), b, - (size_t) bytes, cudaMemcpyHostToDevice, - gs ? gs->adapt_stream : adapt_stream) != cudaSuccess) + (size_t) strata::kernels::cpu::expert_layout().blob_bytes(s.layer), + cudaMemcpyHostToDevice, gs ? gs->adapt_stream : adapt_stream) != cudaSuccess) { + std::fprintf(stderr, "strata serve: adaptive swap copy failed (layer %d, slot %d, pinned %d): %s\n", + (int) s.layer, (int) slot, pin_live.empty() ? 0 : 1, cudaGetErrorString(cudaGetLastError())); return false; + } if (gs) gs->adapt_live = true; else main_live = true; host_res[out] = strata::core::kNotResident; // evicted now: the CPU computes it meanwhile - pending.push_back({(int32_t) in, slot, s.layer, s.in, s.out, - adapt_trace ? drive.d.first_miss_ns[in] : 0, copy_start_ns, bytes, s.gain}); - if (adapt_trace) { - std::fprintf(stderr, - "ADAPT_TRACE select layer=%d expert=%d evicted=%d slot=%d bytes=%llu gain=%.3f usage=%.3f\n", - (int) s.layer, (int) s.in, (int) s.out, (int) slot, - (unsigned long long) bytes, (double) s.gain, (double) drive.d.usage[in]); - } + srcp->prefetch(s.layer, s.out); // a file-backed arena released its pages: read them back ahead + pending.emplace_back((int32_t) in, slot); // resident once the copy has landed } if (!swaps.empty()) cudaEventRecord(adapt_ev, adapt_stream); (void) main_live; @@ -4904,7 +5570,7 @@ int main(int argc, char** argv) { for (size_t i = 0; i < heat.size(); ++i) heat[i] += (double) drive.d.usage[i]; if (peer.valid()) { // multi-GPU: the peer takes the next most-routed CPU misses std::vector r0 = host_res; - for (const PendingSwap& p : pending) r0[(size_t) p.index] = p.slot; // swapped into the primary already + for (const auto& [i, slot] : pending) r0[(size_t) i] = slot; // swapped into the primary already std::string perr; if (!peer.adapt(drive.d.usage.data(), r0.data(), o.peer_adapt_swaps >= 0 ? o.peer_adapt_swaps : o.adapt_swaps, perr)) { @@ -4921,7 +5587,7 @@ int main(int argc, char** argv) { if (heat.empty()) return; std::vector resident(host_res.size(), 0); for (size_t i = 0; i < host_res.size(); ++i) resident[i] = host_res[i] >= 0; - for (const PendingSwap& p : pending) resident[(size_t) p.index] = 1; + for (const auto& p : pending) resident[(size_t) p.first] = 1; std::string e; const auto ranked = strata::core::rank_learned_profile(g.n_layers, g.n_expert, resident, heat, profile_loaded); @@ -5005,6 +5671,7 @@ int main(int argc, char** argv) { const int64_t free_mib = (int64_t) (free_b >> 20); if (free_mib >= 256) { std::fprintf(stderr, "strata serve: %lld MiB of VRAM free with everything loaded\n", (long long) free_mib); + rss_probe("serving"); } else if (reserve_adapted) { // #496: the reserve was already lowered to make the cache fit - a bigger one would leave it no room std::fprintf(stderr, "strata serve: WARNING: %lld MiB of VRAM free with everything loaded - this card " @@ -5041,7 +5708,7 @@ int main(int argc, char** argv) { "expert_slots_primary=%lld expert_cache_primary_mib=%lld spec=%d " "mtp_max=%d lookup=%d vram_free_mib=%lld cvec=%s arena_mib=%lld pool_workers=%d pcie_frac=%.2f " "spec_min_p=%.2f conversation_cache_mib=%lld conversation_cache_slots=%d " - "conversation_cache_min_free_mib=%lld engine=" STRATA_VERSION "\n", + "conversation_cache_min_free_mib=%lld tail_role_token=%lld vram_elastic=%d%s engine=" STRATA_VERSION "\n", (long long) o.max_context, o.kv.c_str(), (long long) (g.n_qsa_layers() > 0 && ss.qsa_states[ss.qsa_primary()].kv_mode == 1 ? ss.qsa_states[ss.qsa_primary()].n_slots * 4 : 0), @@ -5052,7 +5719,9 @@ int main(int argc, char** argv) { (long long) ((o.mmap_experts ? src.resident_bytes() : strata::kernels::cpu::expert_layout().total) >> 20), pool.workers(), o.pcie_frac, o.spec_min_p, (long long) o.conversation_cache_mib, o.conversation_cache_slots, - (long long) o.conversation_cache_min_free_mib); + (long long) o.conversation_cache_min_free_mib, (long long) o.tail_role_token, xcache.segmented() ? 1 : 0, + o.batch > 0 ? (" batch_slots=" + std::to_string(o.batch) + + " slot_cache=" + std::to_string(o.prompt_cache > 0 ? 1 : 0)).c_str() : ""); } // issue #29: a request whose heartbeat (tokens, prompt chunks, verify windows) stops for this long is stuck on // a flag nobody will raise - end the engine with where it was, so the server starts it again instead of the @@ -5099,7 +5768,473 @@ int main(int argc, char** argv) { bool mrope_identity = true; std::vector img_rows; std::vector row_ptr; - while (next_line(line)) { + // ---- #533: VRAM , between requests, only with --vram-elastic. It shrinks the expert cache + // until that much VRAM is free for other programs (a game, a CAD session), or grows it back towards its full + // size when more than that is free. Never on its own: only this command (the server's POST /v1/vram) moves + // it. A shrink gives back the cache's LAST segments: the experts in their slots become CPU misses, as any + // expert outside the cache is (no output changes beyond what a smaller cache decodes), the prompt path's + // loan moves down to the end of what is left (a smaller chunk when it no longer fits). A grow maps them + // again and puts each slot's expert back (or, when the adaptive tier already brought that one back, the + // most-routed missing expert of the same layer); the adaptive tier carries on from there. + std::vector> vram_evicted; // (residency index, slot) the shrinks took + const int64_t chunk_full = o.prefill_chunk; // the loan's chunk with the whole cache + auto relend = [&]() { // the prompt path's loan: the end of the slots left, its chunk as large as fits + if (pf_parts.empty() || pf_parts[0].first < 0) return; + PfPart& p = pf_parts[0]; + const int64_t live = xcache.slots(); + auto fits = [&](int64_t c) { + const int64_t k = part_slots(p, c); + return k > 0 && k + 128 <= live && !(o.prefill_auto && k * 100 > kAutoLendPct * live); + }; + int64_t c = chunk_full; + while (c > 256 && !fits(c)) c = std::max(256, c / 2 / 256 * 256); + o.prefill_chunk = c; + p.first = (int32_t) std::max(0, live - part_slots(p, c)); + p.first_now = -1; // laid out again at the next loan + }; + auto vram_command = [&](const std::string& cmd, std::string& e) -> bool { + // `VRAM` alone: the reserve the engine started with (--vram-reserve-mib) + char* end = nullptr; + const bool bare = cmd.find_first_not_of(' ', 4) == std::string::npos; + const long long reserve = bare ? (long long) o.vram_reserve_mib : std::strtoll(cmd.c_str() + 4, &end, 10); + if (!bare && (end == cmd.c_str() + 4 || reserve < 0)) { e = "expected: VRAM [reserve_mib]"; return false; } + if (!xcache.segmented()) { + e = "VRAM needs an engine started with --vram-elastic (one NVIDIA GPU, --serve)"; + return false; + } + if (src.complement_ready()) { + e = "VRAM: not with the resident low-RAM mode (the cache holds experts RAM does not)"; + return false; + } + if (peer.valid()) { e = "VRAM: not with --peer-device"; return false; } + for (const PfPart& p : pf_parts) + if (!p.lent.empty()) { e = "VRAM: a prompt's loan is still out"; return false; } + if (!ver.wait_commit(e)) return false; + apply_pending(true); // the adaptive tier's swaps in flight land first + if (cudaDeviceSynchronize() != cudaSuccess) { + e = std::string("VRAM: ") + cudaGetErrorString(cudaGetLastError()); + return false; + } + const auto t0 = Clock::now(); + size_t free_b = 0, total_b = 0; + cudaMemGetInfo(&free_b, &total_b); + const int64_t want_free = (int64_t) reserve << 20, mapped = xcache.mapped_bytes(); + const int64_t before = xcache.slots(); + // what stays at least: the prompt path's smallest loan (256 tokens) and the 128 slots it must leave + const int64_t floor_slots = std::min(xcache.full_slots(), + 128 + (pf_parts.empty() || pf_parts[0].first < 0 ? 0 : part_slots(pf_parts[0], 256))); + std::string note; + if ((int64_t) free_b < want_free) { + const int64_t floor_b = xcache.bytes_of(floor_slots); + int64_t keep = mapped - (want_free - (int64_t) free_b); + if (keep < floor_b) { + keep = floor_b; + note = " (the cache keeps its smallest size: the prompt path's buffers)"; + } + if (!xcache.shrink(keep, e)) return false; + const int64_t live = xcache.slots(); + for (size_t i = 0; i < host_res.size(); ++i) + if (host_res[i] >= live) { + vram_evicted.emplace_back((int32_t) i, host_res[i]); + host_res[i] = strata::core::kNotResident; + } + res_upload(); + } else if (mapped < xcache.full_bytes()) { + std::string gerr; + if (!xcache.grow(mapped + ((int64_t) free_b - want_free), gerr)) note = " (" + gerr + ")"; + const int64_t live = xcache.slots(); + std::string ferr; + std::vector> keep_out; + for (const auto& [i, slot] : vram_evicted) { + if (slot >= live) { keep_out.emplace_back(i, slot); continue; } + const int64_t layer = i / g.n_expert; + int64_t pick = i; + if (host_res[(size_t) i] >= 0) { // back already (the adaptive tier): the layer's most-routed miss + pick = -1; + float best = -1.0f; + for (int64_t ex = 0; ex < g.n_expert; ++ex) { + const size_t j = (size_t) (layer * g.n_expert + ex); + if (host_res[j] >= 0) continue; + const float u = drive.d.usage.empty() ? 0.0f : drive.d.usage[j]; + if (u > best) { best = u; pick = (int64_t) j; } + } + } + if (pick < 0) continue; + const uint8_t* b = srcp->blob(layer, pick % g.n_expert); + if (b == nullptr || !xcache.fill_slot_queued(slot, b, ferr, + (int64_t) strata::kernels::cpu::expert_layout().blob_bytes(layer))) { + e = "VRAM: refilling the cache failed: " + ferr; + return false; + } + host_res[(size_t) pick] = slot; + } + vram_evicted.swap(keep_out); + if (!xcache.sync_queued(ferr)) { e = "VRAM: " + ferr; return false; } + res_upload(); + } + relend(); + cudaMemGetInfo(&free_b, &total_b); + std::fprintf(stderr, "strata serve: VRAM %lld MiB kept free: the expert cache %lld -> %lld of %lld slots " + "(%.2f of %.2f GiB), %lld MiB free now, prompt chunk %lld, in %.0f ms%s\n", + reserve, (long long) before, (long long) xcache.slots(), (long long) xcache.full_slots(), + (double) xcache.mapped_bytes() / 1073741824.0, (double) xcache.full_bytes() / 1073741824.0, + (long long) (free_b >> 20), (long long) o.prefill_chunk, + std::chrono::duration(Clock::now() - t0).count(), note.c_str()); + std::printf("VRAM reserve_mib=%lld expert_slots=%lld expert_slots_full=%lld expert_cache_mib=%lld " + "expert_cache_full_mib=%lld vram_free_mib=%lld prompt_chunk=%lld\n", reserve, + (long long) xcache.slots(), (long long) xcache.full_slots(), + (long long) (xcache.mapped_bytes() >> 20), (long long) (xcache.full_bytes() >> 20), + (long long) (free_b >> 20), (long long) o.prefill_chunk); + return true; + }; + // ---- --batch: the slots of the batch windows + struct BSlot { + bool active = false, stop = false; + int32_t x = 0; ///< the token the next window feeds (not yet in the slot's state) + int64_t p = 0; ///< its position + int64_t produced = 0, max_new = 0; + Clock::time_point t0; + // the slot's own conversation cache: the tokens its sessions hold (the prompt, then every token a window + // fed). Kept when the request ends (`cached`), so the next turn of that conversation continues from it + // instead of reading its history again (see `slot_source` in the request path). + std::vector ids; + bool cached = false; ///< idle, and its sessions still hold `ids` + bool cvec = true; ///< the control vector setting `ids` were read with + bool img = false; ///< the conversation has pictures (never reused from the slot) + // the checkpoint its prompt read took at the last turn boundary (the history before the new turn's + // header): a client's next turn renders the history again WITHOUT this reply's thinking, so it matches + // the slot's tokens only up to there - the slot's K/V up to it plus this state continue from it + std::vector checks; + // a prompt read that gave way to a waiting request (BYIELD): `ids` is the part read so far, and the same + // request continues from it (the read goes on with the same chunks; `from0`: it had started at token 0) + bool partial = false, partial_from0 = false; + }; + std::vector bs((size_t) std::max(o.batch, 0)); + // timing of the batch windows since the slots were last all idle (one stderr line then) + double bt_run = 0, bt_commit = 0, bt_emit = 0; + double bt_wait0 = 0, bt_pool0 = 0; + int64_t bt_miss0 = 0, bt_hits0 = 0, bt_pcie0 = 0; + int64_t bt_windows = 0, bt_rows = 0, bt_tokens = 0; + Clock::time_point bt_start = Clock::now(); + int admit_slot = -1; ///< the slot the request being read will continue in (BGEN) + long long admit_max_new = 0; + auto batch_on = [&] { for (const BSlot& b : bs) if (b.active) return true; return false; }; + auto try_next_line = [&](std::string& out) -> bool { + std::lock_guard lk(in_mu); + if (in_lines.empty()) return false; + out = std::move(in_lines.front()); + in_lines.pop_front(); + return true; + }; + // the session a request just left behind (its prompt) -> slot b's sessions, on every stage + auto copy_to_slot = [&](int b, const std::vector& ids, std::string& e) -> bool { + const int64_t upto = (int64_t) ids.size(); + for (size_t k = 0; k < bslot_ss.size(); ++k) { + strata::core::SessionState& from = k == 0 ? ss : stages[k - 1]->ss; + strata::core::SessionState& to = *bslot_ss[k][(size_t) b]; + const strata::core::OnDevice on_k(k == 0 ? 0 : stages[k - 1]->dev); + if (cudaDeviceSynchronize() != cudaSuccess) { e = "batch admission: device sync failed"; return false; } + strata::core::ConversationCheckpoint ck; + ck.ids = ids; + if (!strata::core::conversation_checkpoint_save(ck, from, g, e) || + !strata::core::conversation_checkpoint_restore(ck, to, g, e)) + return false; + for (int64_t j = 0; j < from.qsa_alloc; ++j) { + strata::core::ConversationKv img; + if (!strata::core::conversation_kv_save(img, from.qsa_states[from.qsa_ord0 + j], g, upto, true, e)) + return false; + if (cudaDeviceSynchronize() != cudaSuccess) { e = "batch admission: device sync failed"; return false; } + if (!strata::core::conversation_kv_restore(img, to.qsa_states[to.qsa_ord0 + j], g, upto, true, e)) + return false; + } + if (cudaDeviceSynchronize() != cudaSuccess) { e = "batch admission: device sync failed"; return false; } + } + return true; + }; + // slot b's sessions -> the main ones (the reverse of copy_to_slot): a request that continues the + // conversation an idle slot holds reads on from there. Every stage, the same calls. + // `at`: one of the slot's checkpoints - only the K/V up to it is copied and its state restored instead + auto copy_from_slot = [&](int b, const ConvCheckpoint* at, std::string& e) -> bool { + const int64_t upto = at != nullptr ? (int64_t) at->ids.size() : (int64_t) bs[(size_t) b].ids.size(); + if (at != nullptr && at->stage_parts.size() != stages.size()) { e = "a checkpoint without its stage parts"; return false; } + for (size_t k = 0; k < bslot_ss.size(); ++k) { + strata::core::SessionState& from = *bslot_ss[k][(size_t) b]; + strata::core::SessionState& to = k == 0 ? ss : stages[k - 1]->ss; + const strata::core::OnDevice on_k(k == 0 ? 0 : stages[k - 1]->dev); + if (cudaDeviceSynchronize() != cudaSuccess) { e = "batch slot restore: device sync failed"; return false; } + if (at != nullptr) { + if (!strata::core::conversation_checkpoint_restore(k == 0 ? *at : at->stage_parts[k - 1], to, g, e)) + return false; + } else { + strata::core::ConversationCheckpoint ck; + ck.ids = bs[(size_t) b].ids; + if (!strata::core::conversation_checkpoint_save(ck, from, g, e) || + !strata::core::conversation_checkpoint_restore(ck, to, g, e)) + return false; + } + for (int64_t j = 0; j < from.qsa_alloc; ++j) { + strata::core::ConversationKv img; + if (!strata::core::conversation_kv_save(img, from.qsa_states[from.qsa_ord0 + j], g, upto, true, e)) + return false; + if (cudaDeviceSynchronize() != cudaSuccess) { e = "batch slot restore: device sync failed"; return false; } + if (!strata::core::conversation_kv_restore(img, to.qsa_states[to.qsa_ord0 + j], g, upto, true, e)) + return false; + } + if (cudaDeviceSynchronize() != cudaSuccess) { e = "batch slot restore: device sync failed"; return false; } + } + return true; + }; + // one batch window over the active slots only (row t is the t-th active slot): an idle slot is not touched, + // so it keeps the conversation it holds + auto batch_step = [&]() -> bool { + int S = 0; + int rows[strata::kernels::kVerifyMaxT] = {}; + int32_t tok[strata::kernels::kVerifyMaxT] = {}, outb[strata::kernels::kVerifyMaxT] = {}; + int64_t pos[strata::kernels::kVerifyMaxT] = {}; + for (int b = 0; b < (int) bs.size() && S < strata::kernels::kVerifyMaxT; ++b) + if (bs[(size_t) b].active) { + rows[S] = b; + tok[S] = bs[(size_t) b].x; + pos[S] = bs[(size_t) b].p; + ++S; + } + if (S == 0) return true; + const bool was_busy = strata::core::progress().busy.load(); + strata::core::progress().busy.store(true); + drive.d.layers = 0; + drive.d.experts = 0; + drive.d.failed = false; + apply_pending(false); + if (bt_windows == 0) { + bt_start = Clock::now(); + bt_wait0 = ver.ms_wait; bt_pool0 = ver.ms_pool; + bt_miss0 = drive.d.multi_misses; bt_hits0 = drive.d.cache_hits; bt_pcie0 = drive.d.pcie_experts; + } + const Clock::time_point w0 = Clock::now(); + if (!ver.run_slot_rows(rows, S, tok, pos, win_pool_fn, win_pool_user, outb, err) || drive.d.failed) { + std::printf("ERR %s\n", drive.d.failed && drive.d.fail ? drive.d.fail : err.c_str()); + return false; + } + const Clock::time_point w1 = Clock::now(); + if (!ver.commit_slots(err)) { + std::printf("ERR %s\n", err.c_str()); + return false; + } + const Clock::time_point w2 = Clock::now(); + auto msd = [](Clock::time_point a0, Clock::time_point b0) { return std::chrono::duration(b0 - a0).count(); }; + bt_run += msd(w0, w1); + bt_commit += msd(w1, w2); + ++bt_windows; + bt_rows += S; + for (int t = 0; t < S; ++t) { + const int b = rows[t]; + BSlot& sl = bs[(size_t) b]; + const int32_t y = outb[t]; + sl.ids.push_back(sl.x); // the window fed it: the slot's sessions hold it now + std::printf("BT %d %d\n", b, (int) y); + ++sl.produced; + const bool eos = std::find(o.eos_ids.begin(), o.eos_ids.end(), (int64_t) y) != o.eos_ids.end(); + const char* fin = eos ? "stop" : sl.stop ? "cancel" : sl.produced >= sl.max_new ? "length" + : sl.p + 2 > o.max_context ? "length" : nullptr; + if (fin != nullptr) { + const double ms = std::chrono::duration(Clock::now() - sl.t0).count(); + std::printf("BDONE %d %lld %s %.1f\n", b, (long long) sl.produced, fin, ms); + sl.active = false; + sl.cached = o.prompt_cache > 0 && !sl.img; // its sessions hold sl.ids for the next turn + } else { + sl.x = y; + sl.p += 1; + } + } + std::fflush(stdout); + bt_emit += msd(w2, Clock::now()); + strata::core::progress().busy.store(was_busy); + if (!batch_on() && bt_windows > 0) { + const double w = (double) bt_windows, wall = msd(bt_start, Clock::now()); + const double L = (double) g.n_layers; + std::fprintf(stderr, "strata batch: %lld windows, avg %.2f rows, %.2f ms/window = run %.2f (CUDA0 GPU-reach " + "wait %.2f + CPU experts %.2f) + commit %.2f + emit %.2f; per layer-window: CPU experts " + "%.2f, VRAM hits %.2f, PCIe %.2f; %.1f rows/s over %.0f ms of wall time (admissions " + "included)\n", + (long long) bt_windows, bt_rows / w, (bt_run + bt_commit + bt_emit) / w, bt_run / w, + (ver.ms_wait - bt_wait0) / w, (ver.ms_pool - bt_pool0) / w, bt_commit / w, bt_emit / w, + (drive.d.multi_misses - bt_miss0) / (w * L), (drive.d.cache_hits - bt_hits0) / (w * L), + (drive.d.pcie_experts - bt_pcie0) / (w * L), 1000.0 * bt_rows / std::max(wall, 1e-9), wall); + for (size_t k = 0; k <= stages.size(); ++k) { + const std::string pr = (k == 0 ? ver : stages[k - 1]->ver).profile_report(); + if (!pr.empty()) std::fprintf(stderr, "strata batch GPU stages, stage %zu (ms/window):%s\n", k + 1, pr.c_str()); + } + bt_run = bt_commit = bt_emit = 0; + bt_windows = bt_rows = bt_tokens = 0; + } + return true; + }; + // ---- --batch-groups: the slot groups pipelined through the stages (--batch-groups G > 1 with a layer split) + const int n_pipe = (int) stages.size() + 1; + const bool piped = o.batch > 0 && o.batch_groups > 1 && n_pipe > 1; + const int GS = piped ? o.batch / o.batch_groups : o.batch; + struct PGroup { + bool inflight = false; + int stage = 0; ///< the stage it runs on or waits for + int32_t tok[strata::kernels::kVerifyMaxT] = {}; + int64_t pos[strata::kernels::kVerifyMaxT] = {}; + int64_t since = 0; ///< tick it started waiting (fairness) + }; + std::vector pg((size_t) (piped ? o.batch_groups : 0)); + std::vector stage_group((size_t) n_pipe, -1); + int64_t pipe_tick = 0, rr = 0; + auto stage_verifier = [&](int k) -> strata::core::Verifier& { return k == 0 ? ver : stages[(size_t) k - 1]->ver; }; + auto pipe_inflight = [&] { for (const PGroup& x : pg) if (x.inflight) return true; return false; }; + auto group_active = [&](int gi) { + for (int t = 0; t < GS; ++t) if (bs[(size_t) (gi * GS + t)].active) return true; + return false; + }; + // one turn: serve every running stage, hand finished groups on, start what can start + auto pump = [&](bool may_start) -> bool { + ++pipe_tick; + if (bt_windows == 0 && !pipe_inflight()) { + bt_start = Clock::now(); + bt_wait0 = ver.ms_wait; bt_pool0 = ver.ms_pool; + bt_miss0 = drive.d.multi_misses; bt_hits0 = drive.d.cache_hits; bt_pcie0 = drive.d.pcie_experts; + } + for (int k = 0; k < n_pipe; ++k) { + const int gi = stage_group[(size_t) k]; + if (gi < 0) continue; + strata::core::Verifier& vk = stage_verifier(k); + const int r = vk.batch_poll(win_pool_fn, win_pool_user, err); + if (r < 0) { std::printf("ERR %s\n", err.c_str()); return false; } + if (r == 0) continue; + stage_group[(size_t) k] = -1; + PGroup& G = pg[(size_t) gi]; + if (k + 1 < n_pipe) { G.stage = k + 1; G.since = pipe_tick; continue; } + // the last stage: the group's picks + const int32_t* outb = vk.batch_out(); + for (int t = 0; t < GS; ++t) { + BSlot& sl = bs[(size_t) (gi * GS + t)]; + if (!sl.active) continue; + const int32_t y = outb[t]; + std::printf("BT %d %d\n", gi * GS + t, (int) y); + ++sl.produced; + ++bt_rows; + const bool eos = std::find(o.eos_ids.begin(), o.eos_ids.end(), (int64_t) y) != o.eos_ids.end(); + const char* fin = eos ? "stop" : sl.stop ? "cancel" : sl.produced >= sl.max_new ? "length" + : sl.p + 2 > o.max_context ? "length" : nullptr; + sl.ids.push_back(sl.x); + if (fin != nullptr) { + const double ms = std::chrono::duration(Clock::now() - sl.t0).count(); + std::printf("BDONE %d %lld %s %.1f\n", gi * GS + t, (long long) sl.produced, fin, ms); + sl.active = false; + sl.cached = false; // the pipeline's pad rows: a pipelined slot is not reused as a cache + } else { + sl.x = y; + sl.p += 1; + } + } + std::fflush(stdout); + ++bt_windows; + G.inflight = false; + } + // start waiting groups on free stages, the longest waiting first; a new step on stage 0 round-robin + for (int k = n_pipe - 1; k >= 0; --k) { + if (stage_group[(size_t) k] >= 0) continue; + int pick = -1; + if (k > 0) { + for (int gi = 0; gi < (int) pg.size(); ++gi) + if (pg[(size_t) gi].inflight && pg[(size_t) gi].stage == k && + (pick < 0 || pg[(size_t) gi].since < pg[(size_t) pick].since)) + pick = gi; + if (pick < 0) continue; + } else { + if (!may_start) continue; + for (int j = 0; j < (int) pg.size() && pick < 0; ++j) { + const int gi = (int) ((rr + j) % (int64_t) pg.size()); + if (!pg[(size_t) gi].inflight && group_active(gi)) pick = gi; + } + if (pick < 0) continue; + rr = pick + 1; + PGroup& G = pg[(size_t) pick]; + for (int t = 0; t < GS; ++t) { + BSlot& sl = bs[(size_t) (pick * GS + t)]; + G.tok[t] = sl.active ? sl.x : 0; + G.pos[t] = sl.active ? sl.p : 0; + if (!sl.active) sl.cached = false; // its pad row writes its state + } + G.inflight = true; + G.stage = 0; + drive.d.failed = false; + apply_pending(false); + } + PGroup& G = pg[(size_t) pick]; + strata::core::progress().busy.store(true); + if (!stage_verifier(k).batch_launch(pick * GS, GS, G.tok, G.pos, err)) { + std::printf("ERR %s\n", err.c_str()); + return false; + } + stage_group[(size_t) k] = pick; + } + if (drive.d.failed) { std::printf("ERR %s\n", drive.d.fail ? drive.d.fail : "the expert pool failed"); return false; } + if (!pipe_inflight() && !batch_on() && bt_windows > 0) { // all idle: the timing line, as batch_step + const double w = (double) bt_windows, wall = std::chrono::duration(Clock::now() - bt_start).count(); + std::fprintf(stderr, "strata batch (pipelined, %d groups of %d): %lld group-steps, %lld rows in %.0f ms = " + "%.1f rows/s (admissions included)\n", o.batch_groups, GS, (long long) bt_windows, + (long long) bt_rows, wall, 1000.0 * bt_rows / std::max(wall, 1e-9)); + for (int k = 0; k < n_pipe; ++k) { + const std::string pr = stage_verifier(k).profile_report(); + if (!pr.empty()) std::fprintf(stderr, "strata batch GPU stages, stage %d (ms/window):%s\n", k + 1, pr.c_str()); + } + bt_windows = bt_rows = 0; + strata::core::progress().busy.store(false); + } + return true; + }; + auto pipe_drain = [&]() -> bool { + while (pipe_inflight()) + if (!pump(false)) return false; + return true; + }; + for (;;) { + if (batch_on() || (piped && pipe_inflight())) { + if (!try_next_line(line)) { + if (!(piped ? pump(true) : batch_step())) return 1; + continue; + } + // a request reads its prompt through every stage: the groups in flight finish first + if (piped && line.rfind("BSTOP ", 0) != 0 && !pipe_drain()) return 1; + } else if (!next_line(line)) { + break; + } + // --batch: BSTOP ends that slot at its next batch window; BGEN ... reads the + // request's prompt and first token as a GEN 1, then continues it in that slot + if (line.rfind("BSTOP ", 0) == 0) { + const int b = std::atoi(line.c_str() + 6); + if (b >= 0 && b < (int) bs.size()) bs[(size_t) b].stop = true; + continue; + } + if (line.rfind("BYIELD", 0) == 0) continue; // for a prompt read that has ended meanwhile + if (line.rfind("VRAM", 0) == 0) { // #533 (above): between requests, not a request + std::string verr; + if (batch_on()) verr = "VRAM: not while batch slots are decoding"; + else vram_command(line, verr); + if (!verr.empty()) std::printf("ERR %s\n", verr.c_str()); + std::fflush(stdout); + continue; + } + admit_slot = -1; + if (line.rfind("BGEN ", 0) == 0 || line.rfind("BGENI ", 0) == 0) { + const bool bimg = line.rfind("BGENI ", 0) == 0; + char* e1 = nullptr; + const long b = std::strtol(line.c_str() + (bimg ? 6 : 5), &e1, 10); + char* e2 = nullptr; + const long long mn = std::strtoll(e1, &e2, 10); + if (bs.empty() || b < 0 || b >= (long) bs.size() || bs[(size_t) b].active || mn < 1 || e2 == e1) { + std::printf("ERR BGEN: no such free slot (--batch %d) or a bad max_new\n", o.batch); + std::fflush(stdout); + continue; + } + admit_slot = (int) b; + admit_max_new = mn; + line = std::string(bimg ? "GENI 1" : "GEN 1") + e2; + } // #477: every --expert-profile-save-every minutes, before the next request (at QUIT: after the loop) if (!heat.empty() && line != "QUIT" && o.expert_profile_save_min > 0 && Clock::now() - profile_saved_at >= std::chrono::duration(o.expert_profile_save_min * 60.0)) @@ -5315,51 +6450,151 @@ int main(int argc, char** argv) { from_live = false; } } + // --batch: an idle slot that holds the start of this prompt (the conversation it served last) is a + // source too - its sessions are copied back below, so only the new part is read + int slot_source = -1; + int64_t slot_tokens = 0; + bool resumed_from0 = false; // a prompt read parked in a slot (BYIELD) that had started at token 0 + const ConvCheckpoint* slot_ck = nullptr; // the slot's checkpoint the prompt continues from (else its end) + if (o.prompt_cache > 0 && req_imgs.empty()) + for (int b = 0; b < (int) bs.size(); ++b) { + const BSlot& sl = bs[(size_t) b]; + if (sl.active || !sl.cached || sl.cvec != want_cvec) continue; + if ((int64_t) sl.ids.size() > std::max(resume, slot_tokens) && starts_with(sl.ids, {})) { + slot_source = b; + slot_tokens = (int64_t) sl.ids.size(); + slot_ck = nullptr; + } + for (const ConvCheckpoint& c : sl.checks) + if ((int64_t) c.ids.size() > std::max(resume, slot_tokens) && starts_with(c.ids, c.imgs)) { + slot_source = b; + slot_tokens = (int64_t) c.ids.size(); + slot_ck = &c; + } + } const auto parked = conversations.best(ids, req_imgs, want_cvec); std::optional incoming; - if (parked.tokens > resume) incoming.emplace(conversations.take(parked.index)); + if (parked.tokens > std::max(resume, slot_tokens)) incoming.emplace(conversations.take(parked.index)); + if (incoming) slot_source = -1; // Reject the entire image before parking/overwriting the outgoing // state. Invalid entries can safely fall back to its existing prefix. - if (incoming && !strata::core::conversation_snapshot_validate(*incoming, ss, g, mtp.kv_state(), err)) { + if (incoming && incoming->stage_images.size() != stages.size()) { + std::fprintf(stderr, "strata serve: conversation cache: discard snapshot (stage count)\n"); + incoming.reset(); + } + if (incoming) for (size_t i = 0; i < stages.size(); ++i) { + const strata::core::OnDevice on(stages[i]->dev); + if (!strata::core::conversation_snapshot_validate(incoming->stage_images[i], stages[i]->ss, g, + i + 1 == stages.size() ? &mtp.kv_state() : nullptr, err)) { // the draft: the last stage's + std::fprintf(stderr, "strata serve: conversation cache: discard invalid stage snapshot (%s)\n", + err.c_str()); + incoming.reset(); + err.clear(); + break; + } + } + if (incoming && !strata::core::conversation_snapshot_validate(*incoming, ss, g, + stages.empty() ? &mtp.kv_state() : nullptr, err)) { std::fprintf(stderr, "strata serve: conversation cache: discard invalid snapshot (%s)\n", err.c_str()); incoming.reset(); err.clear(); } // Preserve the outgoing branch before any checkpoint rewind, reset, // or incoming restore overwrites the positional state it requires. - if ((!from_live || incoming) && !park_current(incoming ? incoming->bytes() : 0)) { + if ((!from_live || incoming || slot_source >= 0) && !park_current(incoming ? incoming->bytes() : 0)) { std::printf("ERR %s\n", err.c_str()); return 1; } + if (slot_source >= 0) { + const auto t0 = Clock::now(); + if (!copy_from_slot(slot_source, slot_ck, err)) { + // the main session may be half written: read this prompt from the start + std::fprintf(stderr, "strata batch: restoring slot %d failed (%s); reading the prompt\n", + slot_source, err.c_str()); + err.clear(); + live.clear(); + live_imgs.clear(); + checks.clear(); + resume = 0; + from_live = false; + } else { + checks.clear(); // the main session's checkpoints were of the conversation it held before + // a read that gave way, the same request again (into its own slot): on with it - its segments and + // checkpoints as if it had not stopped + if (slot_ck == nullptr && bs[(size_t) slot_source].partial && admit_slot == slot_source) { + resumed_from0 = bs[(size_t) slot_source].partial_from0; + for (const ConvCheckpoint& c : bs[(size_t) slot_source].checks) checks.push_back(c); + bs[(size_t) slot_source].partial = false; + } + if (slot_ck != nullptr) { + live = slot_ck->ids; + checks.push_back(*slot_ck); // the main session's chain has it now + checks.back().used = ++check_clock; + } else { + live = bs[(size_t) slot_source].ids; + } + live_imgs.clear(); + resume = slot_tokens; + from_live = true; + std::fprintf(stderr, "strata batch: slot %d gave back %lld tokens of this conversation (%s) in " + "%.1f ms\n", slot_source, (long long) slot_tokens, + slot_ck != nullptr ? "its turn checkpoint" : "all it holds", + std::chrono::duration(Clock::now() - t0).count()); + } + } if (incoming) { const auto t0 = Clock::now(); - if (strata::core::conversation_snapshot_restore(*incoming, ss, g, mtp.kv_state(), err) != + if (strata::core::conversation_snapshot_restore(*incoming, ss, g, + stages.empty() ? &mtp.kv_state() : nullptr, err) != strata::core::ConversationRestore::restored) { // Already prevalidated above: a failure here is fatal, never // permission to decode from a partially restored session. std::printf("ERR restoring parked conversation: %s\n", err.c_str()); return 1; } + for (size_t i = 0; i < stages.size(); ++i) { // the later stages' parts + const strata::core::OnDevice on(stages[i]->dev); + if (strata::core::conversation_snapshot_restore(incoming->stage_images[i], stages[i]->ss, g, + i + 1 == stages.size() ? &mtp.kv_state() : nullptr, err) != + strata::core::ConversationRestore::restored) { + std::printf("ERR restoring parked conversation (stage CUDA%d): %s\n", stages[i]->dev, + err.c_str()); + return 1; + } + if (cudaDeviceSynchronize() != cudaSuccess) { std::printf("ERR stage sync\n"); return 1; } + } if (std::getenv("STRATA_SNAPSHOT_VERIFY") != nullptr) { uint64_t draft_hash = 0; - if (!strata::core::conversation_kv_verify(incoming->kv.back(), mtp.kv_state(), g, + const auto& draft_kv = stages.empty() ? incoming->kv.back() : incoming->stage_images.back().kv.back(); + const strata::core::OnDevice on_d(stages.empty() ? 0 : stages.back()->dev); + if (!strata::core::conversation_kv_verify(draft_kv, mtp.kv_state(), g, int64_t(incoming->live.ids.size()), false, draft_hash, err)) { std::printf("ERR verifying restored draft KV: %s\n", err.c_str()); return 1; } std::fprintf(stderr, "strata serve: SNAPSHOT_VERIFY draft=%016llx cells=%lld mode=%d source=%s resident=%lld\n", - (unsigned long long) draft_hash, (long long) incoming->kv.back().cells, + (unsigned long long) draft_hash, (long long) draft_kv.cells, mtp.kv_state().kv_mode, "ram", (long long) (mtp.kv_state().n_slots * strata::kernels::qsa_real_shapes().page_size)); } live = std::move(incoming->live.ids); live_imgs = std::move(incoming->live.imgs); - checks = std::move(incoming->checkpoints); + if (stages.empty()) { + checks = std::move(incoming->checkpoints); + } else { // give each checkpoint back its stage parts (moved, not copied); none if they do not line up + strata::core::ConversationCheckpointSplit cs; + cs.stage0 = std::move(incoming->checkpoints); + for (auto& si : incoming->stage_images) cs.parts.push_back(std::move(si.checkpoints)); + if (!strata::core::conversation_checkpoints_merge(std::move(cs), checks)) checks.clear(); + } cvec_cached = incoming->cvec; resume = parked.tokens; from_live = parked.live; - if (std::getenv("STRATA_SNAPSHOT_FULL_CAPTURE") == nullptr) - conversations.retain(std::move(incoming->kv), int64_t(live.size())); + if (std::getenv("STRATA_SNAPSHOT_FULL_CAPTURE") == nullptr) { + std::vector> stage_kv; // every stage's, for its next park + for (auto& si : incoming->stage_images) stage_kv.push_back(std::move(si.kv)); + conversations.retain(std::move(incoming->kv), int64_t(live.size()), std::move(stage_kv)); + } incoming.reset(); // Running-state/checkpoint copies are no longer needed. std::fprintf(stderr, "strata serve: conversation cache: restored %lld tokens (%s) in %.1f ms; parked=%zu bytes=%zu\n", (long long) resume, from_live ? "live" : "checkpoint", @@ -5618,6 +6853,99 @@ int main(int argc, char** argv) { } return true; }; + // --batch: a prompt read while slots are decoding goes one prompt chunk at a time (the same chunks as one + // run: each run starts where the last ended, at a multiple of the chunk), and between two chunks the slots + // decode for a share of the time the chunk took (STRATA_BATCH_DECODE_SHARE, default 0.5), so a long + // prompt does not stop the others. The slots' windows then see the cache without the slots this prompt + // borrowed (marked missing: those experts run on the CPU), never a prompt buffer. + static const double decode_share = [] { + const char* v = std::getenv("STRATA_BATCH_DECODE_SHARE"); + return v != nullptr ? std::max(0.0, std::atof(v)) : 0.5; + }(); + bool batch_fatal = false; + int64_t il_parts = 0; // the slots' windows run between this prompt's chunks + double il_ms = 0; + int64_t yielded_at = -1; // BYIELD: the read stopped here and its slot holds it + int yielded_slot = -1; + // `BYIELD ` (the server: a shorter request is waiting): at the next chunk boundary the part read so + // far is copied into (this admission's own, or a free one the server reserved for a solo request), + // the request ends with `YIELDED ` + DONE cancel, and the server sends it again later: it + // continues from the slot with the same chunks. #656's cooperative preemption, with a slot as the park. + auto read_part = [&](int64_t a0, int64_t b0, std::string& e) -> bool { + // (a layer split reads its stages as a pipeline over one run's chunks: in pieces only beside slots) + if (o.batch <= 0 || piped || (!stages.empty() && !batch_on())) return sp.run(ids.data() + a0, b0 - a0, a0, e); + const int64_t C = std::max(sp.chunk(), 1); + for (int64_t q = a0; q < b0;) { + const int64_t r = std::min(b0, q + C); + const auto tq = Clock::now(); + if (!sp.run(ids.data() + q, r - q, q, e)) return false; + q = r; + if (q >= b0) continue; + int ys = -1; + { // a BSTOP that came meanwhile ends its slot at its next window; a BYIELD is for this read + std::lock_guard lk(in_mu); + for (auto it = in_lines.begin(); it != in_lines.end();) { + if (it->rfind("BSTOP ", 0) == 0) { + const int b = std::atoi(it->c_str() + 6); + if (b >= 0 && b < (int) bs.size()) bs[(size_t) b].stop = true; + it = in_lines.erase(it); + } else if (it->rfind("BYIELD ", 0) == 0) { + ys = std::atoi(it->c_str() + 7); + it = in_lines.erase(it); + } else { + ++it; + } + } + } + if (ys >= 0) { + // only where the rest is read the same way after it (more than a chunk and more than a short + // read left), text only, and into a slot that is not decoding + const bool can = ys < (int) bs.size() && !bs[(size_t) ys].active && + (admit_slot < 0 || ys == admit_slot) && req_imgs.empty() && o.prompt_cache > 0 && + b0 - q > std::max(C, o.short_read); + std::string ye; + const auto ty = Clock::now(); + std::vector pre(ids.begin(), ids.begin() + q); + if (can && copy_to_slot(ys, pre, ye)) { + BSlot& sl = bs[(size_t) ys]; + sl = BSlot{}; + sl.ids = std::move(pre); + sl.cached = true; + sl.partial = true; + sl.partial_from0 = read_from == 0 || resumed_from0; + sl.cvec = cvec_cached; + for (const ConvCheckpoint& c : checks) // the root / periodic checkpoints of this read + if ((int64_t) c.ids.size() <= q && std::equal(c.ids.begin(), c.ids.end(), ids.begin(), + [](int32_t x, int64_t y) { return (int64_t) x == y; })) + sl.checks.push_back(c); + yielded_at = q; + yielded_slot = ys; + std::fprintf(stderr, "strata batch: the prompt read gives way at %lld of %lld tokens; slot %d " + "holds it (copied in %.1f ms)\n", (long long) q, (long long) n, ys, + std::chrono::duration(Clock::now() - ty).count()); + e = "yield"; + return false; + } + if (can) bs[(size_t) ys].cached = false; // half written + std::fprintf(stderr, "strata batch: BYIELD %d not taken at %lld of %lld%s%s\n", ys, (long long) q, + (long long) n, ye.empty() ? "" : ": ", ye.c_str()); + } + if (decode_share <= 0.0 || !batch_on()) continue; + const double budget = decode_share * std::chrono::duration(Clock::now() - tq).count(); + const auto td = Clock::now(); + do { + if (!batch_step()) { batch_fatal = true; e = "a batch window failed"; return false; } + } while (batch_on() && std::chrono::duration(Clock::now() - td).count() < budget); + ++il_parts; + il_ms += std::chrono::duration(Clock::now() - td).count(); + if (trace) { + std::fprintf(stderr, "strata trace: prompt chunk to %lld, then the slots decoded %.0f ms\n", + (long long) q, std::chrono::duration(Clock::now() - td).count()); + std::fflush(stderr); + } + } + return true; + }; apply_pending(true); // per-request sampling for the verify window's head (greedy when temperature is absent) strata::kernels::SamplerParams req_sp; @@ -5652,13 +6980,22 @@ int main(int argc, char** argv) { if (o.prompt_cache > 0 && o.turn_token >= 0) for (int64_t i = n - 1; i > resume; --i) if (ids[(size_t) i] == o.turn_token) { turn_at = i; break; } + // #458 (opt-in): a short turn of --tail-role-token's role right before the new assistant turn (the + // server's trailing reasoning-effort turn) stays out of the checkpoint, so the next request - another + // effort, or the next turn of the chat - finds the conversation without it + if (turn_at > 0 && o.tail_role_token >= 0) + for (int64_t i = turn_at - 1; i > resume; --i) + if (ids[(size_t) i] == o.turn_token) { + if (ids[(size_t) i + 1] == o.tail_role_token) turn_at = i; + break; + } // A prompt read from token 0 also stops at its FIRST turn boundary: the end of the system prompt (with // the tools), which every new chat of the same client shares. That checkpoint becomes the chain's root, // which the retention policy pins (conv_cache.hpp), so the next new chat reads only what comes after it. // (PR #65, code-martin.) Only for a system prompt of --prompt-cache-root tokens or more: a small one // is cheaper to read again than the extra part costs (~0.3 s). int64_t root_at = -1; - if (o.prompt_cache > 0 && o.turn_token >= 0 && o.prompt_cache_root > 0 && read_from == 0) + if (o.prompt_cache > 0 && o.turn_token >= 0 && o.prompt_cache_root > 0 && (read_from == 0 || resumed_from0)) for (int64_t i = 1; i < turn_at; ++i) if (ids[(size_t) i] == o.turn_token) { if (i >= o.prompt_cache_root) root_at = i; @@ -5678,7 +7015,7 @@ int main(int argc, char** argv) { return 1; } const auto tsp = Clock::now(); - const bool sp_ok = win ? read_windows(at, to, err) : sp.run(ids.data() + at, to - at, at, err); + const bool sp_ok = win ? read_windows(at, to, err) : read_part(at, to, err); if (trace) { std::fprintf(stderr, "strata trace: read %lld tokens (%s) in %.1f ms\n", (long long) (to - at), win ? "windows" : "batched", @@ -5686,7 +7023,7 @@ int main(int argc, char** argv) { std::fflush(stderr); } if (!sp_ok) { - if (!stop_req.load()) { + if (yielded_at < 0 && (batch_fatal || !stop_req.load())) { std::fprintf(stderr, "strata serve: %s\n", err.c_str()); std::printf("ERR %s\n", err.c_str()); // #224: a CUDA fault (an illegal address) poisons the context for the whole process, and @@ -5703,7 +7040,7 @@ int main(int argc, char** argv) { } at = to; if ((to == turn_at || to == root_at) && !checkpoint_at(to)) { - std::printf("ERR saving a conversation checkpoint failed\n"); + std::printf("ERR saving a conversation checkpoint failed%s\n", ckpt_why.c_str()); return 1; } } @@ -5712,6 +7049,9 @@ int main(int argc, char** argv) { return 1; } tr("prompt done (slots refilled)"); + if (il_parts > 0) + std::fprintf(stderr, "strata batch: the prompt was read in %lld parts, the slots decoding %.0f ms between " + "them\n", (long long) il_parts + 1, il_ms); const double prompt_ms = std::chrono::duration(Clock::now() - r0).count(); std::printf("REUSED %lld\n", (long long) resume); // the prompt is read; the first window comes next std::fflush(stdout); @@ -5753,6 +7093,7 @@ int main(int argc, char** argv) { const int64_t ram0 = src.ram_reads(), files0 = src.file_reads(); const uint64_t file_bytes0 = src.file_read_bytes(); const int64_t decode_look0 = drive.d.cache_hits + drive.d.cache_admitted + drive.d.cache_refused; + const int64_t offload0 = drive.d.offload_entries; // #588 if (cancelled) finish = "cancel"; while (!cancelled && produced_n < max_new) { int T = S_mtp; @@ -6000,6 +7341,7 @@ int main(int argc, char** argv) { } const int64_t req_hits = drive.d.cache_hits - decode_hits0; const int64_t req_look = (drive.d.cache_hits + drive.d.cache_admitted + drive.d.cache_refused) - decode_look0; + const int64_t req_offload = drive.d.offload_entries - offload0; // #471: the prompt tokens this request read - all the fresh ones, or as far as the prompt pass got when a // cancel stopped it part-way (a cancelled request used to be logged and counted as having read them all) const int64_t fresh = n - resume; @@ -6007,14 +7349,52 @@ int main(int argc, char** argv) { // DONE [hits] [lookups] // [RAM blobs] [file blobs] [file MB] (CS-T tiers; appended, so an older server reads the rest) // [prompt tokens read] (#471: fewer than - when a cancel stopped the read) - // [parked conversations] [parked bytes] [park evictions] (appended for lane-local parking telemetry) - std::printf("DONE %lld %lld %.1f %.1f %s %lld %lld %lld %lld %lld %lld %lld %.1f %lld %zu %zu %zu\n", (long long) produced_n, + // [offloaded] (#588: the decode's routed experts the GPU read over PCIe or another GPU computed; + // not in [lookups]) + if (yielded_at >= 0) std::printf("YIELDED %d %lld\n", yielded_slot, (long long) yielded_at); + std::printf("DONE %lld %lld %.1f %.1f %s %lld %lld %lld %lld %lld %lld %lld %.1f %lld %lld\n", + (long long) produced_n, (long long) n, prompt_ms, decode_ms, finish, (long long) draft_accepted, (long long) draft_offered, (long long) resume, (long long) req_hits, (long long) req_look, (long long) (src.ram_reads() - ram0), (long long) (src.file_reads() - files0), (double) (src.file_read_bytes() - file_bytes0) / 1e6, (long long) read_n, - conversations.size(), conversations.bytes(), conversations.evictions()); + (long long) req_offload); std::fflush(stdout); + if (admit_slot >= 0) { // --batch: BADM <1 = continues in the batch windows | 0 = done> + bool cont = !cancelled && produced_n == 1 && admit_max_new > 1 && std::strcmp(finish, "length") == 0 && + (int64_t) live.size() == p; + const auto tc0 = Clock::now(); + if (cont && !copy_to_slot(admit_slot, live, err)) { + std::fprintf(stderr, "strata serve: batch admission failed: %s\n", err.c_str()); + bs[(size_t) admit_slot].cached = false; // its sessions may be half written + cont = false; + } + if (cont) { + ver.set_slot_sampling(admit_slot, req_sp); // the request's own sampling, row by row + BSlot& sl = bs[(size_t) admit_slot]; + sl = BSlot{}; + sl.active = true; + sl.x = x; + sl.p = p; + sl.produced = 1; + sl.max_new = admit_max_new; + sl.t0 = Clock::now(); + sl.ids = live; + sl.cvec = cvec_cached; + sl.img = !live_imgs.empty(); // pictures: not matched again by tokens alone, so not cached + const ConvCheckpoint* best = nullptr; + for (const ConvCheckpoint& c : checks) + if (c.ids.size() < live.size() && (best == nullptr || c.ids.size() > best->ids.size()) && + std::equal(c.ids.begin(), c.ids.end(), live.begin())) + best = &c; + if (best != nullptr && !sl.img) sl.checks.push_back(*best); + std::fprintf(stderr, "strata batch: slot %d takes %lld tokens (copied in %.1f ms)\n", admit_slot, + (long long) live.size(), std::chrono::duration(Clock::now() - tc0).count()); + } + std::printf("BADM %d %d\n", admit_slot, cont ? 1 : 0); + std::fflush(stdout); + admit_slot = -1; + } if (drive.routing != nullptr) std::fflush(drive.routing); // the routing trace survives a crash and is watchable mid-session // "12288 of 98179" when cancelled mid-read (#471), the rate from what was read char read_txt[64]; @@ -6029,11 +7409,18 @@ int main(int argc, char** argv) { decode_ms > 0 ? 1000.0 * produced_n / decode_ms : 0.0, (long long) draft_accepted, (long long) draft_offered, checks.size(), cancelled ? " (cancelled)" : ""); // the VRAM share of the experts the pool looked up while decoding; experts it sent over PCIe for the GPU - // to read (--pcie-frac) are in neither count + // to read (--pcie-frac) are in neither count - #588: so that share is said beside it (raising --pcie-frac + // raises the hit rate while the PCIe reads may make the decode slower) if (req_look > 0) { - std::fprintf(stderr, "strata serve: decode expert cache hit rate: %.1f%% (%lld hits / %lld lookups)\n", + char off[160] = ""; + if (req_offload > 0) + std::snprintf(off, sizeof off, "; %lld more read by the GPU over PCIe or from another GPU (%.1f%% of " + "all %lld routed)", (long long) req_offload, + 100.0 * (double) req_offload / (double) (req_look + req_offload), + (long long) (req_look + req_offload)); + std::fprintf(stderr, "strata serve: decode expert cache hit rate: %.1f%% (%lld hits / %lld lookups)%s\n", 100.0 * (double) req_hits / (double) req_look, - (long long) req_hits, (long long) req_look); + (long long) req_hits, (long long) req_look, off); } // the resident RAM mode, cumulative: experts read from experts.bin since the copy was made (what the plain // mmap mode reads through the OS file cache, from the SSD when the RAM could not keep it) @@ -6447,6 +7834,7 @@ int main(int argc, char** argv) { strata::core::Verifier ver; strata::core::VerifyHits vh; vh.d_res = thits.d_res; + vh.h_res = host_res.empty() ? nullptr : host_res.data(); vh.cache_base = thits.cache_base; vh.blob = thits.blob; vh.slot_off = xcache.slot_offsets(); // E-6: the device plan's pointers @@ -6488,6 +7876,7 @@ int main(int argc, char** argv) { } // plan v0.3 P6: swaps in flight - (residency index, slot) admitted when adapt_ev has completed std::vector> pending; + std::vector pin_live; // the swaps' locked arena pages (pin_blob), unlocked once they have landed cudaEvent_t adapt_ev = nullptr; cudaEventCreateWithFlags(&adapt_ev, cudaEventDisableTiming); int64_t adapt_rounds = 0; // counted here: `rounds` is declared below the adapt lambda @@ -6504,8 +7893,12 @@ int main(int argc, char** argv) { } if (trace_pending) std::fprintf(stderr, "strata: PENDING landed, %zu experts become resident\n", pending.size()); + unpin_blobs(pin_live); src.commit_exchanges(); // the resident RAM mode: the evicted experts take their places in RAM - for (const auto& [i, slot] : pending) host_res[(size_t) i] = slot; + for (const auto& [i, slot] : pending) { + host_res[(size_t) i] = slot; + srcp->release((int64_t) i / g.n_expert, (int64_t) i % g.n_expert); // in VRAM now: RAM not needed + } pending.clear(); if (d_res != nullptr) cudaMemcpy(d_res, host_res.data(), host_res.size() * sizeof(int32_t), cudaMemcpyHostToDevice); @@ -6555,6 +7948,14 @@ int main(int argc, char** argv) { std::fprintf(stderr, "strata generate: an adaptive refill failed (copying evicted experts back)\n"); return false; } + if (pin_blobs_on()) { // lock the batch's source pages (a file-backed arena on AMD; see pin_blobs) + std::vector> spans; + for (const Swap& s : swaps) + if (const uint8_t* b = srcp->blob(s.layer, s.in)) + spans.emplace_back((uintptr_t) b, + (uintptr_t) b + strata::kernels::cpu::expert_layout().blob_bytes(s.layer)); + pin_blobs(std::move(spans), pin_live); + } for (const Swap& s : swaps) { const size_t in = (size_t) s.layer * g.n_expert + s.in, out = (size_t) s.layer * g.n_expert + s.out; const int32_t slot = host_res[out]; @@ -6567,6 +7968,7 @@ int main(int argc, char** argv) { return false; } host_res[out] = strata::core::kNotResident; // evicted now: the CPU computes it meanwhile + srcp->prefetch(s.layer, s.out); // a file-backed arena released its pages: read them back ahead pending.emplace_back((int32_t) in, slot); // resident once the copy has landed } if (!swaps.empty()) cudaEventRecord(adapt_ev, adapt_stream); diff --git a/sycl/.gitignore b/sycl/.gitignore new file mode 100644 index 0000000000..d73108ee74 --- /dev/null +++ b/sycl/.gitignore @@ -0,0 +1,4 @@ +# dpct output that is not part of the port (tools/migrate.sh) +dpct_report.* +**/*.yaml +migrate.log diff --git a/sycl/CMakeLists.txt b/sycl/CMakeLists.txt new file mode 100644 index 0000000000..1b6094afc3 --- /dev/null +++ b/sycl/CMakeLists.txt @@ -0,0 +1,241 @@ +# Strata on Intel GPUs - the SYCL build of the engine (Phase 4 of the Intel port, docs/INTEL.md). +# +# This is a separate project on purpose. The top-level build describes the CUDA engine; every source it lists is +# taken from ../src unless this directory carries a migrated copy: `src/X.cu` -> `sycl/src/X.dp.cpp`, +# `src/X.cpp` -> `sycl/src/X.cpp` (only host files that called the CUDA runtime were migrated). Headers resolve +# the same way through the include order. Upstream files are never edited, so the port is the content of this +# directory plus the tool that produced it (../../sycl/migrate.sh). +# +# cmake -S sycl -B build-sycl -G Ninja -DCMAKE_CXX_COMPILER=icpx && cmake --build build-sycl +# +# Device code is JIT-compiled from SPIR-V unless STRATA_SYCL_AOT names a device (bmg-g31 for the Arc Pro B70). +cmake_minimum_required(VERSION 3.24) +project(strata_sycl VERSION 0.1.39 LANGUAGES C CXX) # the upstream engine version this port tracks +set(STRATA_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE) +endif() +add_compile_definitions(STRATA_VERSION="${PROJECT_VERSION}-sycl") + +set(STRATA_SYCL_AOT "" CACHE STRING "AOT device for the GPU code, e.g. bmg-g31 (empty: SPIR-V, JIT at run time)") +set(_sycl_flags -fsycl) +if(STRATA_SYCL_AOT) + list(APPEND _sycl_flags -fsycl-targets=spir64_gen) + # the divide/sqrt rounding option (below) must reach ocloc here: an AOT build has no spir64 backend to take it + set(_sycl_link -fsycl -fsycl-targets=spir64_gen -Xsycl-target-backend=spir64_gen + "-device ${STRATA_SYCL_AOT} -options -cl-fp32-correctly-rounded-divide-sqrt") +else() + set(_sycl_link -fsycl) +endif() +# The Intel GPU's fp32 divide is not correctly rounded by default (OpenCL allows 2.5 ulp). Strata's quantizers +# are byte-exact against ggml through `amax / 127`, so the device compiler is told to round divides and sqrt +# properly; measured: quantize_act_parity goes from 303k mismatches to byte-exact. (Same as +# SYCL_PROGRAM_COMPILE_OPTIONS=-cl-fp32-correctly-rounded-divide-sqrt at run time.) The AOT build passes it to +# ocloc above; without that the 303k mismatches came back on 2026-09-30, unnoticed since the AOT switch. +if(NOT STRATA_SYCL_AOT) + list(APPEND _sycl_link "-Xsycl-target-backend=spir64" "-cl-fp32-correctly-rounded-divide-sqrt") +endif() +# Every translation unit gets -fsycl: the migrated host files construct queues and USM, not only the kernels. +# icpx defaults to a fast floating-point model (reciprocal approximations, reassociation); the kernels are +# compared byte-for-byte against ggml's, which nvcc built value-safe, so ask for the same here. +# Every kernel was written for 32-lane warps (lane = tid & 31, warp-synchronous reductions). dpct pins the +# sub-group size only where it saw a shuffle; the rest would get Xe2's default of 16 and race. Force 32 everywhere. +list(APPEND _sycl_flags -fsycl-default-sub-group-size=32) +# One device module per kernel: the first launch of a kernel then loads only its own module. With the default +# split the first launch in the prompt path (the embedding gather) paid 245 ms for the image it shared (2026-09-30). +list(APPEND _sycl_link -fsycl-device-code-split=per_kernel) +list(APPEND _sycl_flags -fsycl-device-code-split=per_kernel) +add_compile_options("$<$:${_sycl_flags}>" -fp-model=precise -Wno-unused-parameter + -Wno-unused-variable -Wno-deprecated-declarations) +add_link_options(${_sycl_link}) + +# --- source resolution: the migrated copy when there is one, the original otherwise ------------------------------- +function(strata_resolve out) + set(r "") + foreach(f IN LISTS ARGN) + if(f MATCHES "\\.cu$") + string(REGEX REPLACE "\\.cu$" ".dp.cpp" m "${f}") + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${m}) + list(APPEND r ${CMAKE_CURRENT_SOURCE_DIR}/${m}) + else() + message(FATAL_ERROR "no migrated copy of ${f} (expected ${m})") + endif() + elseif(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${f}) + list(APPEND r ${CMAKE_CURRENT_SOURCE_DIR}/${f}) + elseif(EXISTS ${STRATA_ROOT}/${f}) + list(APPEND r ${STRATA_ROOT}/${f}) + else() + message(FATAL_ERROR "no such source: ${f}") + endif() + endforeach() + set(${out} ${r} PARENT_SCOPE) +endfunction() + +add_library(strata_includes INTERFACE) +target_include_directories(strata_includes INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/include # migrated headers + dpct/ helpers, first + ${STRATA_ROOT}/include) +target_compile_options(strata_includes INTERFACE -Wall -Wextra) + +# --- CPU-side pieces, identical to the top-level build ------------------------------------------------------------ +strata_resolve(_spec src/spec/suffix_drafter.cpp src/spec/controller.cpp src/spec/draft_policy.cpp) +add_library(strata_spec STATIC ${_spec}) +target_link_libraries(strata_spec PUBLIC strata_includes) + +strata_resolve(_cpu src/kernels/cpu/expert.cpp src/kernels/cpu/pool.cpp src/kernels/cpu/expert_layout.cpp + src/kernels/cpu/q2_avx2.cpp) +add_library(strata_kernels_cpu STATIC ${_cpu}) +set_source_files_properties(${STRATA_ROOT}/src/kernels/cpu/expert.cpp PROPERTIES COMPILE_OPTIONS + "-mavx512f;-mavx512bw;-mavx512vl;-mavx512dq;-mavx512vnni;-mavx512vbmi;-mf16c;-mfma") +set_source_files_properties(${STRATA_ROOT}/src/kernels/cpu/q2_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c") +find_package(Threads REQUIRED) +target_link_libraries(strata_kernels_cpu PUBLIC strata_includes Threads::Threads) + +option(STRATA_NATIVE_EXPERTS "Native (IQ) expert support through ggml-cpu" ON) +set(STRATA_GGML_DIR "" CACHE PATH "llama.cpp checkout to take ggml from (empty: fetch the pinned commit)") +if(STRATA_NATIVE_EXPERTS) + set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) + set(GGML_STATIC ON CACHE BOOL "" FORCE) + set(GGML_CUDA OFF CACHE BOOL "" FORCE) + set(GGML_SYCL OFF CACHE BOOL "" FORCE) + set(GGML_OPENMP OFF CACHE BOOL "" FORCE) + set(GGML_BACKEND_DL OFF CACHE BOOL "" FORCE) + set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(GGML_NATIVE ON CACHE BOOL "" FORCE) + if(STRATA_GGML_DIR) + add_subdirectory(${STRATA_GGML_DIR}/ggml ${CMAKE_BINARY_DIR}/ggml EXCLUDE_FROM_ALL) + else() + include(FetchContent) + FetchContent_Declare(strata_llamacpp + GIT_REPOSITORY https://github.com/ggml-org/llama.cpp.git + GIT_TAG 3cf03257f219afbe7334045ff7c6a06ac68c627d + GIT_SHALLOW FALSE) + FetchContent_GetProperties(strata_llamacpp) + if(NOT strata_llamacpp_POPULATED) + FetchContent_Populate(strata_llamacpp) + endif() + add_subdirectory(${strata_llamacpp_SOURCE_DIR}/ggml ${CMAKE_BINARY_DIR}/ggml EXCLUDE_FROM_ALL) + endif() + strata_resolve(_cpu_native src/kernels/cpu/native_expert.cpp src/kernels/cpu/iq_avx512.cpp src/kernels/cpu/iq_avx2.cpp + src/kernels/cpu/kq_avx2.cpp) # 0.1.31: the Q4_K/Q5_K/Q5_1/Q8_0 expert rows + target_sources(strata_kernels_cpu PRIVATE ${_cpu_native}) + target_include_directories(strata_kernels_cpu PRIVATE ${STRATA_ROOT}/third_party/ggml) + set_source_files_properties(${STRATA_ROOT}/src/kernels/cpu/iq_avx512.cpp PROPERTIES COMPILE_OPTIONS + "-mavx512f;-mavx512bw;-mavx512vl;-mavx512dq;-mavx512vnni;-mavx512vbmi;-mf16c;-mfma") + set_source_files_properties(${STRATA_ROOT}/src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c") + set_source_files_properties(${STRATA_ROOT}/src/kernels/cpu/kq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c") + target_link_libraries(strata_kernels_cpu PUBLIC ggml-cpu ggml-base) + target_compile_definitions(strata_kernels_cpu PUBLIC STRATA_NATIVE_EXPERTS=1) +endif() + +# --- the GPU engine ------------------------------------------------------------------------------------------------ +strata_resolve(_core src/core/device.cu src/core/pinned.cu src/platform/memory.cpp src/core/graph.cpp + src/core/weights.cpp src/core/layout.cpp) +add_library(strata_core STATIC ${_core}) +target_link_libraries(strata_core PUBLIC strata_includes) + +set(_kernel_cu dequant_s2 s2_gemv s_gemv s2_gemv_quads s2_gemv_fast router_top10 quantize_act rope sampler s2_gemv_q8 + s2_expert_grouped shared_expert gr gdn qsa elementwise bf16_gemv native_mmvq native_bf16 native_gr_norm native_moe + native_gdn native_gr_postops native_router native_gdn_preprocess native_qsa native_rope native_ple_postops + native_flash_attn native_qsa_indexer native_qsa_score ple kv_q8 kv_stream kv_q4 fused_gr cvec qsa_decode_attn + qsa_prompt_attn fused_gdn dequant_bf16 qsa_select verify_kernels iq_kernels) +set(_kernel_files "") +foreach(k IN LISTS _kernel_cu) + list(APPEND _kernel_files src/kernels/cuda/${k}.cu) +endforeach() +strata_resolve(_kernels ${_kernel_files} src/kernels/ngram.cpp src/ngram/ple_reader.cpp src/platform/direct_file.cpp) +list(APPEND _kernels ${CMAKE_CURRENT_SOURCE_DIR}/src/kernels/cuda/qsa_prompt_attn_xmx.dp.cpp) # port-only: the XMX prompt attention +add_library(strata_kernels STATIC ${_kernels}) +target_include_directories(strata_kernels PRIVATE ${STRATA_ROOT}/third_party/ggml) +target_link_libraries(strata_kernels PUBLIC strata_core) + +strata_resolve(_engine src/core/layer.cpp src/core/session.cpp src/core/expert_source.cpp src/core/remote_experts.cpp + src/core/expert_cache.cpp src/core/native_head.cpp src/core/native_dense.cpp src/core/verify.cpp + src/core/mtp.cpp src/core/conversation_snapshot.cpp src/core/conversation_state.cpp + src/core/conversation_memory.cpp # 0.1.31: the conversation snapshots (#216) + src/core/peer_experts.cpp) # 0.1.36+: --peer-device (a stub in the port: no second GPU tier yet) +add_library(strata_engine STATIC ${_engine} ${CMAKE_CURRENT_SOURCE_DIR}/src/core/gguf_expert_source.cpp) +target_include_directories(strata_engine PRIVATE ${STRATA_ROOT}/src/core) # conversation_checked.hpp (#include "...") +target_link_libraries(strata_engine PUBLIC strata_core strata_kernels strata_kernels_cpu) + +# cuBLAS -> oneMKL through dpct::blas (needs the MKL SYCL BLAS domain) +strata_resolve(_prefill src/prefill/gemm.cu src/prefill/kernels.cu src/prefill/prefill.cpp) +add_library(strata_prefill STATIC ${_prefill} + ${CMAKE_CURRENT_SOURCE_DIR}/src/prefill/moe_fused_stub.cpp) # upstream's fused int8 kernels: not built here +target_compile_options(strata_prefill PUBLIC -qmkl=sequential) +target_link_options(strata_prefill PUBLIC -qmkl=sequential) +target_link_libraries(strata_prefill PUBLIC strata_engine) + +strata_resolve(_main src/program/generate.cpp) +add_executable(strata ${_main}) +target_link_libraries(strata PRIVATE strata_engine strata_prefill strata_spec) + +# --- the kernel parity self-tests (each runs on the card and compares against a CPU reference) --------------------- +option(STRATA_SYCL_PARITY "Build the kernel parity tests" ON) +if(STRATA_SYCL_PARITY) + enable_testing() + foreach(p IN ITEMS dequant_s2 s2_gemv iq shared_expert gr gdn s2_gemv_q8 sampler rope quantize_act router_top10 + s_gemv elementwise bf16_gemv s_gemv_q8k qsa ple kv_q8 kv_stream kv_q4 cvec) + strata_resolve(_src src/kernels/${p}_parity.cpp) + add_executable(${p}_parity ${_src}) + target_include_directories(${p}_parity PRIVATE ${STRATA_ROOT}/src/kernels) # the oracle .inc fixtures + target_link_libraries(${p}_parity PRIVATE strata_kernels) + add_test(NAME ${p}_parity COMMAND ${p}_parity --selftest) + endforeach() + # 0.1.31: the IQ kernels that decode a weight part once for every column/entry, bitwise vs the per-column ones + # (no --selftest flag: it runs its checks by default), and the rewritten per-hit/grouped expert kernels vs the + # previous ones, bitwise + strata_resolve(_src src/kernels/iq_multi_parity.cpp) + add_executable(iq_multi_parity ${_src}) + target_link_libraries(iq_multi_parity PRIVATE strata_kernels) + add_test(NAME iq_multi_parity COMMAND iq_multi_parity) + strata_resolve(_src src/kernels/s2_expert_grouped_parity.cpp) + add_executable(s2_expert_grouped_parity ${_src}) + target_link_libraries(s2_expert_grouped_parity PRIVATE strata_kernels) + add_test(NAME s2_expert_grouped_parity COMMAND s2_expert_grouped_parity --selftest) + # upstream PR #363: native_expert_grouped's group stride and fused SwiGLU + q8_1, bitwise vs the v1 launches + strata_resolve(_src src/kernels/native_grouped_parity.cpp) + add_executable(native_grouped_parity ${_src}) + target_link_libraries(native_grouped_parity PRIVATE strata_kernels) + add_test(NAME native_grouped_parity COMMAND native_grouped_parity) + # (upstream PR #413's gdn_rec_parity is an SM-holding NVIDIA bench - PTX timers, __nanosleep - not built here; the + # key-head kernel is checked through the engine: output hashes with STRATA_GDN_KEYHEAD=1 and without) + # the conversation snapshots (#216) against the engine, as upstream's STRATA_BUILD_CONVERSATION_TESTS + strata_resolve(_src src/core/conversation_snapshot_test.cpp) + add_executable(conversation_snapshot_test ${_src}) + target_link_libraries(conversation_snapshot_test PRIVATE strata_engine) + add_test(NAME conversation_snapshot_test COMMAND conversation_snapshot_test) + set_tests_properties(conversation_snapshot_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 120) + # the prompt-attention parity takes a context length, not --selftest, and needs the tensor-core kernel (not + # ported yet: it reports "refused the pools" here). Built, not registered. + strata_resolve(_src src/kernels/qsa_prompt_attn_parity.cpp) + add_executable(qsa_prompt_attn_parity ${_src}) + target_link_libraries(qsa_prompt_attn_parity PRIVATE strata_kernels) + # the hybrid K8V4 KV test (upstream 0.1.27) passes its appends, gathers and attention here and fails only its + # last step, the same tensor-core prompt kernel. Built, not registered, until that kernel is ported. + strata_resolve(_src src/kernels/kv_hybrid_parity.cpp) + add_executable(kv_hybrid_parity ${_src}) + target_include_directories(kv_hybrid_parity PRIVATE ${STRATA_ROOT}/src/kernels) + target_link_libraries(kv_hybrid_parity PRIVATE strata_kernels) + add_executable(mmvq_bench ${CMAKE_CURRENT_SOURCE_DIR}/src/kernels/mmvq_bench.cpp) # dense mmvq timing + target_link_libraries(mmvq_bench PRIVATE strata_kernels) + add_executable(mmvq_sg_bench ${CMAKE_CURRENT_SOURCE_DIR}/src/kernels/mmvq_sg_bench.cpp) # SIMD16 vs 32 decode mmvq + target_link_libraries(mmvq_sg_bench PRIVATE strata_kernels) + add_executable(q6k_align_bench ${CMAKE_CURRENT_SOURCE_DIR}/src/kernels/q6k_align_bench.cpp) # Q6_K block alignment + target_link_libraries(q6k_align_bench PRIVATE strata_kernels) + add_executable(gr_bench ${CMAKE_CURRENT_SOURCE_DIR}/src/kernels/gr_bench.cpp) # fused GR read timing + target_link_libraries(gr_bench PRIVATE strata_kernels) + add_executable(xmx_gemm_bench ${CMAKE_CURRENT_SOURCE_DIR}/src/kernels/xmx_gemm_bench.cpp) # XMX quantized GEMM vs dequant+oneMKL + target_link_libraries(xmx_gemm_bench PRIVATE strata_kernels strata_prefill) # -qmkl for the oneMKL reference + # one native expert three ways on real GGUF rows (ggml reference, CPU path, GPU native_expert_grouped): + # native_expert_parity [layer ...] + if(STRATA_NATIVE_EXPERTS) + add_executable(native_expert_parity ${CMAKE_CURRENT_SOURCE_DIR}/src/kernels/native_expert_parity.cpp) + target_include_directories(native_expert_parity PRIVATE ${STRATA_ROOT}/third_party/ggml) + target_link_libraries(native_expert_parity PRIVATE strata_kernels strata_kernels_cpu) + endif() +endif() diff --git a/sycl/include/dpct/LICENSE.TXT b/sycl/include/dpct/LICENSE.TXT new file mode 100644 index 0000000000..a209b428a6 --- /dev/null +++ b/sycl/include/dpct/LICENSE.TXT @@ -0,0 +1,236 @@ +============================================================================== +The SYCLomatic (dpct) helper headers in this directory, vendored from Intel oneAPI DPC++ Compatibility Tool +2025.3 (https://github.com/oneapi-src/SYCLomatic), are under the Apache License v2.0 with LLVM Exceptions +(SPDX: Apache-2.0 WITH LLVM-exception; Copyright (C) Intel Corporation): +============================================================================== + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + +============================================================================== +Software from third parties included in the Unified Memory Framework: +============================================================================== +The Unified Memory Framework contains third party software which is under different license +terms. All such code will be identified clearly using at least one of two +mechanisms: +1) It will be in a separate directory tree with its own `LICENSE.txt` or + `LICENSE` file at the top containing the specific license and restrictions + which apply to that software, or +2) It will contain specific license and restriction terms at the top of every + file. diff --git a/sycl/include/dpct/atomic.hpp b/sycl/include/dpct/atomic.hpp new file mode 100644 index 0000000000..2bbecc9485 --- /dev/null +++ b/sycl/include/dpct/atomic.hpp @@ -0,0 +1,868 @@ +//==---- atomic.hpp -------------------------------*- C++ -*----------------==// +// +// Copyright (C) Intel Corporation +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// See https://llvm.org/LICENSE.txt for license information. +// +//===----------------------------------------------------------------------===// + +#ifndef __DPCT_ATOMIC_HPP__ +#define __DPCT_ATOMIC_HPP__ + +#include + +namespace dpct { + +/// Atomically add the value operand to the value at the addr and assign the +/// result to the value at addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand The value to add to the value at \p addr. +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_fetch_add(T *addr, T operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.fetch_add(operand); +} + +template +inline T1 atomic_fetch_add(T1 *addr, T2 operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.fetch_add(operand); +} + +template +inline sycl::half2 atomic_fetch_add(sycl::half2 *addr, sycl::half2 operand) { + auto atm = sycl::atomic_ref( + *reinterpret_cast(addr)); + + union { + unsigned i; + sycl::half2 h; + } old{0}, output{0}; + + while (true) { + old.i = atm.load(); + output.h = old.h + operand; + if (atm.compare_exchange_strong(old.i, output.i)) + break; + } + + return output.h; +} + +/// Atomically add the value operand to the value at the addr and assign the +/// result to the value at addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand The value to add to the value at \p addr. +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_fetch_add(T *addr, T operand, + sycl::memory_order memoryOrder) { + switch (memoryOrder) { + case sycl::memory_order::relaxed: + return atomic_fetch_add(addr, operand); + case sycl::memory_order::acq_rel: + return atomic_fetch_add(addr, operand); + case sycl::memory_order::seq_cst: + return atomic_fetch_add(addr, operand); + default: + assert(false && "Invalid memory_order for atomics. Valid memory_order for " + "atomics are: sycl::memory_order::relaxed, " + "sycl::memory_order::acq_rel, sycl::memory_order::seq_cst!"); + } +} + +template +inline T1 atomic_fetch_add(T1 *addr, T2 operand, + sycl::memory_order memoryOrder) { + atomic_fetch_add(addr, operand, memoryOrder); +} + +/// Atomically subtract the value operand from the value at the addr and assign +/// the result to the value at addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand The value to subtract from the value at \p addr +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_fetch_sub(T *addr, T operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.fetch_sub(operand); +} + +template +inline T1 atomic_fetch_sub(T1 *addr, T2 operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.fetch_sub(operand); +} + +/// Atomically subtract the value operand from the value at the addr and assign +/// the result to the value at addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand The value to subtract from the value at \p addr +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_fetch_sub(T *addr, T operand, + sycl::memory_order memoryOrder) { + switch (memoryOrder) { + case sycl::memory_order::relaxed: + return atomic_fetch_sub(addr, operand); + case sycl::memory_order::acq_rel: + return atomic_fetch_sub(addr, operand); + case sycl::memory_order::seq_cst: + return atomic_fetch_sub(addr, operand); + default: + assert(false && "Invalid memory_order for atomics. Valid memory_order for " + "atomics are: sycl::memory_order::relaxed, " + "sycl::memory_order::acq_rel, sycl::memory_order::seq_cst!"); + } +} + +template +inline T1 atomic_fetch_sub(T1 *addr, T2 operand, + sycl::memory_order memoryOrder) { + atomic_fetch_sub(addr, operand, memoryOrder); +} + +/// Atomically perform a bitwise AND between the value operand and the value at the addr +/// and assign the result to the value at addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand The value to use in bitwise AND operation with the value at the \p addr. +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_fetch_and(T *addr, T operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.fetch_and(operand); +} + +template +inline T1 atomic_fetch_and(T1 *addr, T2 operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.fetch_and(operand); +} + +/// Atomically perform a bitwise AND between the value operand and the value at the addr +/// and assign the result to the value at addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand The value to use in bitwise AND operation with the value at the \p addr. +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_fetch_and(T *addr, T operand, + sycl::memory_order memoryOrder) { + switch (memoryOrder) { + case sycl::memory_order::relaxed: + return atomic_fetch_and(addr, operand); + case sycl::memory_order::acq_rel: + return atomic_fetch_and(addr, operand); + case sycl::memory_order::seq_cst: + return atomic_fetch_and(addr, operand); + default: + assert(false && "Invalid memory_order for atomics. Valid memory_order for " + "atomics are: sycl::memory_order::relaxed, " + "sycl::memory_order::acq_rel, sycl::memory_order::seq_cst!"); + } +} + +template +inline T1 atomic_fetch_and(T1 *addr, T2 operand, + sycl::memory_order memoryOrder) { + atomic_fetch_and(addr, operand, memoryOrder); +} + +/// Atomically or the value at the addr with the value operand, and assign +/// the result to the value at addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand The value to use in bitwise OR operation with the value at the \p addr. +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_fetch_or(T *addr, T operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.fetch_or(operand); +} + +template +inline T1 atomic_fetch_or(T1 *addr, T2 operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.fetch_or(operand); +} + +/// Atomically or the value at the addr with the value operand, and assign +/// the result to the value at addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand The value to use in bitwise OR operation with the value at the \p addr. +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_fetch_or(T *addr, T operand, + sycl::memory_order memoryOrder) { + switch (memoryOrder) { + case sycl::memory_order::relaxed: + return atomic_fetch_or(addr, operand); + case sycl::memory_order::acq_rel: + return atomic_fetch_or(addr, operand); + case sycl::memory_order::seq_cst: + return atomic_fetch_or(addr, operand); + default: + assert(false && "Invalid memory_order for atomics. Valid memory_order for " + "atomics are: sycl::memory_order::relaxed, " + "sycl::memory_order::acq_rel, sycl::memory_order::seq_cst!"); + } +} + +template +inline T1 atomic_fetch_or(T1 *addr, T2 operand, + sycl::memory_order memoryOrder) { + atomic_fetch_or(addr, operand, memoryOrder); +} + +/// Atomically xor the value at the addr with the value operand, and assign +/// the result to the value at addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand The value to use in bitwise XOR operation with the value at the \p addr. +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_fetch_xor(T *addr, T operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.fetch_xor(operand); +} + +template +inline T1 atomic_fetch_xor(T1 *addr, T2 operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.fetch_xor(operand); +} + +/// Atomically xor the value at the addr with the value operand, and assign +/// the result to the value at addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand The value to use in bitwise XOR operation with the value at the \p addr. +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_fetch_xor(T *addr, T operand, + sycl::memory_order memoryOrder) { + switch (memoryOrder) { + case sycl::memory_order::relaxed: + return atomic_fetch_xor(addr, operand); + case sycl::memory_order::acq_rel: + return atomic_fetch_xor(addr, operand); + case sycl::memory_order::seq_cst: + return atomic_fetch_xor(addr, operand); + default: + assert(false && "Invalid memory_order for atomics. Valid memory_order for " + "atomics are: sycl::memory_order::relaxed, " + "sycl::memory_order::acq_rel, sycl::memory_order::seq_cst!"); + } +} + +template +inline T1 atomic_fetch_xor(T1 *addr, T2 operand, + sycl::memory_order memoryOrder) { + atomic_fetch_xor(addr, operand, memoryOrder); +} + +/// Atomically calculate the minimum of the value at addr and the value operand +/// and assign the result to the value at addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand. +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_fetch_min(T *addr, T operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.fetch_min(operand); +} + +template +inline T1 atomic_fetch_min(T1 *addr, T2 operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.fetch_min(operand); +} + +/// Atomically calculate the minimum of the value at addr and the value operand +/// and assign the result to the value at addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand. +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_fetch_min(T *addr, T operand, + sycl::memory_order memoryOrder) { + switch (memoryOrder) { + case sycl::memory_order::relaxed: + return atomic_fetch_min(addr, operand); + case sycl::memory_order::acq_rel: + return atomic_fetch_min(addr, operand); + case sycl::memory_order::seq_cst: + return atomic_fetch_min(addr, operand); + default: + assert(false && "Invalid memory_order for atomics. Valid memory_order for " + "atomics are: sycl::memory_order::relaxed, " + "sycl::memory_order::acq_rel, sycl::memory_order::seq_cst!"); + } +} + +template +inline T1 atomic_fetch_min(T1 *addr, T2 operand, + sycl::memory_order memoryOrder) { + atomic_fetch_min(addr, operand, memoryOrder); +} + +/// Atomically calculate the maximum of the value at addr and the value operand +/// and assign the result to the value at addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand. +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_fetch_max(T *addr, T operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.fetch_max(operand); +} + +template +inline T1 atomic_fetch_max(T1 *addr, T2 operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.fetch_max(operand); +} + +/// Atomically calculate the maximum of the value at addr and the value operand +/// and assign the result to the value at addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand. +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_fetch_max(T *addr, T operand, + sycl::memory_order memoryOrder) { + switch (memoryOrder) { + case sycl::memory_order::relaxed: + return atomic_fetch_max(addr, operand); + case sycl::memory_order::acq_rel: + return atomic_fetch_max(addr, operand); + case sycl::memory_order::seq_cst: + return atomic_fetch_max(addr, operand); + default: + assert(false && "Invalid memory_order for atomics. Valid memory_order for " + "atomics are: sycl::memory_order::relaxed, " + "sycl::memory_order::acq_rel, sycl::memory_order::seq_cst!"); + } +} + +template +inline T1 atomic_fetch_max(T1 *addr, T2 operand, + sycl::memory_order memoryOrder) { + atomic_fetch_max(addr, operand, memoryOrder); +} + +/// Atomically set \p operand to the value stored in \p addr, if old value stored in +/// \p addr is equal to zero or greater than \p operand, else decrease the value stored +/// in \p addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand The threshold value. +/// \param memoryOrder The memory ordering used. +/// \returns The old value stored in \p addr. +template +inline unsigned int atomic_fetch_compare_dec(unsigned int *addr, + unsigned int operand) { + auto atm = sycl::atomic_ref(addr[0]); + unsigned int old; + + while (true) { + old = atm.load(); + if (old == 0 || old > operand) { + if (atm.compare_exchange_strong(old, operand)) + break; + } else if (atm.compare_exchange_strong(old, old - 1)) + break; + } + + return old; +} + +/// Atomically increment the value stored in \p addr if old value stored in \p +/// addr is less than \p operand, else set 0 to the value stored in \p addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand The threshold value. +/// \param memoryOrder The memory ordering used. +/// \returns The old value stored in \p addr. +template +inline unsigned int atomic_fetch_compare_inc(unsigned int *addr, + unsigned int operand) { + auto atm = sycl::atomic_ref(addr[0]); + unsigned int old; + while (true) { + old = atm.load(); + if (old >= operand) { + if (atm.compare_exchange_strong(old, 0)) + break; + } else if (atm.compare_exchange_strong(old, old + 1)) + break; + } + return old; +} + +/// Atomically increment the value stored in \p addr if old value stored in \p +/// addr is less than \p operand, else set 0 to the value stored in \p addr. +/// \param [in, out] addr The pointer to the data. +/// \param operand The threshold value. +/// \param memoryOrder The memory ordering used. +/// \returns The old value stored in \p addr. +template +inline unsigned int +atomic_fetch_compare_inc(unsigned int *addr, unsigned int operand, + sycl::memory_order memoryOrder) { + switch (memoryOrder) { + case sycl::memory_order::relaxed: + return atomic_fetch_compare_inc(addr, + operand); + case sycl::memory_order::acq_rel: + return atomic_fetch_compare_inc(addr, + operand); + case sycl::memory_order::seq_cst: + return atomic_fetch_compare_inc(addr, + operand); + default: + assert(false && "Invalid memory_order for atomics. Valid memory_order for " + "atomics are: sycl::memory_order::relaxed, " + "sycl::memory_order::acq_rel, sycl::memory_order::seq_cst!"); + } +} + +/// Atomically exchange the value at the address addr with the value operand. +/// \param [in, out] addr The pointer to the data. +/// \param operand The value to be exchanged with the value pointed by \p addr. +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_exchange(T *addr, T operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.exchange(operand); +} + +template +inline T1 atomic_exchange(T1 *addr, T2 operand) { + auto atm = + sycl::atomic_ref(addr[0]); + return atm.exchange(operand); +} + +/// Atomically exchange the value at the address addr with the value operand. +/// \param [in, out] addr The pointer to the data. +/// \param operand The value to be exchanged with the value pointed by \p addr. +/// \param memoryOrder The memory ordering used. +/// \returns The value at the \p addr before the call. +template +inline T atomic_exchange(T *addr, T operand, + sycl::memory_order memoryOrder) { + switch (memoryOrder) { + case sycl::memory_order::relaxed: + return atomic_exchange(addr, operand); + case sycl::memory_order::acq_rel: + return atomic_exchange(addr, operand); + case sycl::memory_order::seq_cst: + return atomic_exchange(addr, operand); + default: + assert(false && "Invalid memory_order for atomics. Valid memory_order for " + "atomics are: sycl::memory_order::relaxed, " + "sycl::memory_order::acq_rel, sycl::memory_order::seq_cst!"); + } +} + +template +inline T1 atomic_exchange(T1 *addr, T2 operand, + sycl::memory_order memoryOrder) { + atomic_exchange(addr, operand, memoryOrder); +} + +/// Atomically compare the value at \p addr to the value expected and exchange +/// with the value desired if the value at \p addr is equal to the value expected. +/// Returns the value at the \p addr before the call. +/// \param [in, out] addr Multi_ptr. +/// \param expected The value to compare against the value at \p addr. +/// \param desired The value to assign to \p addr if the value at \p addr is expected. +/// \param success The memory ordering used when comparison succeeds. +/// \param fail The memory ordering used when comparison fails. +/// \returns The value at the \p addr before the call. +template +T atomic_compare_exchange_strong( + sycl::multi_ptr addr, T expected, T desired, + sycl::memory_order success = sycl::memory_order::relaxed, + sycl::memory_order fail = sycl::memory_order::relaxed) { + auto atm = sycl::atomic_ref(*addr); + + atm.compare_exchange_strong(expected, desired, success, fail); + return expected; +} + +template +T1 atomic_compare_exchange_strong( + sycl::multi_ptr addr, T2 expected, T3 desired, + sycl::memory_order success = sycl::memory_order::relaxed, + sycl::memory_order fail = sycl::memory_order::relaxed) { + auto atm = + sycl::atomic_ref(*addr); + T1 expected_value = expected; + atm.compare_exchange_strong(expected_value, desired, success, fail); + return expected_value; +} + +/// Atomically compare the value at \p addr to the value expected and exchange +/// with the value desired if the value at \p addr is equal to the value expected. +/// Returns the value at the \p addr before the call. +/// \param [in] addr The pointer to the data. +/// \param expected The value to compare against the value at \p addr. +/// \param desired The value to assign to \p addr if the value at \p addr is expected. +/// \param success The memory ordering used when comparison succeeds. +/// \param fail The memory ordering used when comparison fails. +/// \returns The value at the \p addr before the call. +template +T atomic_compare_exchange_strong( + T *addr, T expected, T desired, + sycl::memory_order success = sycl::memory_order::relaxed, + sycl::memory_order fail = sycl::memory_order::relaxed) { + auto atm = + sycl::atomic_ref(addr[0]); + atm.compare_exchange_strong(expected, desired, success, fail); + return expected; +} + +template +T1 atomic_compare_exchange_strong( + T1 *addr, T2 expected, T3 desired, + sycl::memory_order success = sycl::memory_order::relaxed, + sycl::memory_order fail = sycl::memory_order::relaxed) { + T1 expected_value = expected; + auto atm = + sycl::atomic_ref(addr[0]); + atm.compare_exchange_strong(expected_value, desired, success, fail); + return expected_value; +} + +/// Atomic extension to implement standard APIs in std::atomic +namespace detail{ +template struct IsValidAtomicType { + static constexpr bool value = + (std::is_same::value || std::is_same::value || + std::is_same::value || std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || std::is_same::value || + std::is_pointer::value); +}; +} // namespace detail + +template +class atomic { + static_assert( + detail::IsValidAtomicType::value, + "Invalid atomic type. Valid types are int, unsigned int, long, " + "unsigned long, long long, unsigned long long, float, double " + "and pointer types"); + T __d; + +public: + /// default memory synchronization order + static constexpr sycl::memory_order default_read_order = + sycl::atomic_ref::default_read_order; + static constexpr sycl::memory_order default_write_order = + sycl::atomic_ref::default_write_order; + static constexpr sycl::memory_scope default_scope = DefaultScope; + static constexpr sycl::memory_order default_read_modify_write_order = + DefaultOrder; + + /// Default constructor. + constexpr atomic() noexcept = default; + /// Constructor with initialize value. + constexpr atomic(T d) noexcept : __d(d){}; + + /// atomically replaces the value of the referenced object with a non-atomic argument + /// \param operand The value to replace the pointed value. + /// \param memoryOrder The memory ordering used. + /// \param memoryScope The memory scope used. + void store(T operand, sycl::memory_order memoryOrder = default_write_order, + sycl::memory_scope memoryScope = default_scope) noexcept { + sycl::atomic_ref atm(__d); + atm.store(operand, memoryOrder, memoryScope); + } + + /// atomically obtains the value of the referenced object + /// \param memoryOrder The memory ordering used. + /// \param memoryScope The memory scope used. + /// \returns The value of the referenced object + T load(sycl::memory_order memoryOrder = default_read_order, + sycl::memory_scope memoryScope = default_scope) const noexcept { + sycl::atomic_ref atm( + const_cast(__d)); + return atm.load(memoryOrder, memoryScope); + } + + /// atomically replaces the value of the referenced object and obtains the value held previously + /// \param operand The value to replace the pointed value. + /// \param memoryOrder The memory ordering used. + /// \param memoryScope The memory scope used. + /// \returns The value of the referenced object before the call. + T exchange(T operand, + sycl::memory_order memoryOrder = default_read_modify_write_order, + sycl::memory_scope memoryScope = default_scope) noexcept { + + sycl::atomic_ref atm(__d); + return atm.exchange(operand, memoryOrder, memoryScope); + } + + /// atomically compares the value of the referenced object with non-atomic argument + /// and performs atomic exchange if equal or atomic load if not + /// \param expected The value expected to be found in the object referenced by the atomic_ref object + /// \param desired The value to store in the referenced object if it is as expected + /// \param success The memory models for the read-modify-write + /// \param failure The memory models for load operations + /// \param memoryScope The memory scope used. + /// \returns true if the referenced object was successfully changed, false otherwise. + bool compare_exchange_weak( + T &expected, T desired, + sycl::memory_order success, sycl::memory_order failure, + sycl::memory_scope memoryScope = default_scope) noexcept { + sycl::atomic_ref atm(__d); + return atm.compare_exchange_weak(expected, desired, success, failure, memoryScope); + } + /// \param expected The value expected to be found in the object referenced by the atomic_ref object + /// \param desired The value to store in the referenced object if it is as expected + /// \param memoryOrder The memory synchronization ordering for operations + /// \param memoryScope The memory scope used. + /// \returns true if the referenced object was successfully changed, false otherwise. + bool compare_exchange_weak(T &expected, T desired, + sycl::memory_order memoryOrder = default_read_modify_write_order, + sycl::memory_scope memoryScope = default_scope) noexcept { + sycl::atomic_ref atm(__d); + return atm.compare_exchange_weak(expected, desired, memoryOrder, memoryScope); + } + + /// atomically compares the value of the referenced object with non-atomic argument + /// and performs atomic exchange if equal or atomic load if not + /// \param expected The value expected to be found in the object referenced by the atomic_ref object + /// \param desired The value to store in the referenced object if it is as expected + /// \param success The memory models for the read-modify-write + /// \param failure The memory models for load operations + /// \param memoryScope The memory scope used. + /// \returns true if the referenced object was successfully changed, false otherwise. + bool compare_exchange_strong( + T &expected, T desired, + sycl::memory_order success, sycl::memory_order failure, + sycl::memory_scope memoryScope = default_scope) noexcept { + + sycl::atomic_ref atm(__d); + return atm.compare_exchange_strong(expected, desired, success, failure, memoryScope); + } + /// \param expected The value expected to be found in the object referenced by the atomic_ref object + /// \param desired The value to store in the referenced object if it is as expected + /// \param memoryOrder The memory synchronization ordering for operations + /// \param memoryScope The memory scope used. + /// \returns true if the referenced object was successfully changed, false otherwise. + bool compare_exchange_strong(T &expected, T desired, + sycl::memory_order memoryOrder = default_read_modify_write_order, + sycl::memory_scope memoryScope = default_scope) noexcept { + sycl::atomic_ref atm(__d); + return atm.compare_exchange_strong(expected, desired, memoryOrder, memoryScope); + } + + /// atomically adds the argument to the value stored in the atomic object and obtains the value held previously + /// \param operand The other argument of arithmetic addition + /// \param memoryOrder The memory ordering used. + /// \param memoryScope The memory scope used. + /// \returns The value of the referenced object before the call. + T fetch_add(T operand, + sycl::memory_order memoryOrder = default_read_modify_write_order, + sycl::memory_scope memoryScope = default_scope) noexcept { + + sycl::atomic_ref atm(__d); + return atm.fetch_add(operand, memoryOrder, memoryScope); + } + + /// atomically subtracts the argument from the value stored in the atomic object and obtains the value held previously + /// \param operand The other argument of arithmetic subtraction + /// \param memoryOrder The memory ordering used. + /// \param memoryScope The memory scope used. + /// \returns The value of the referenced object before the call. + T fetch_sub(T operand, + sycl::memory_order memoryOrder = default_read_modify_write_order, + sycl::memory_scope memoryScope = default_scope) noexcept { + + sycl::atomic_ref atm(__d); + return atm.fetch_sub(operand, memoryOrder, memoryScope); + } +}; + +} // namespace dpct +#endif // __DPCT_ATOMIC_HPP__ diff --git a/sycl/include/dpct/bindless_images.hpp b/sycl/include/dpct/bindless_images.hpp new file mode 100644 index 0000000000..042321d328 --- /dev/null +++ b/sycl/include/dpct/bindless_images.hpp @@ -0,0 +1,2061 @@ +//===--- bindless_images.hpp ----------------------*- C++ -*---------------===// +// +// Copyright (C) Intel Corporation +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// See https://llvm.org/LICENSE.txt for license information. +// +//===----------------------------------------------------------------------===// + +#ifndef __DPCT_BINDLESS_IMAGE_HPP__ +#define __DPCT_BINDLESS_IMAGE_HPP__ + +#ifdef _WIN32 +// DirectX headers +#include +#include +#include +#endif // _WIN32 + +namespace dpct { +namespace experimental { + +#ifdef SYCL_EXT_ONEAPI_BINDLESS_IMAGES + +/// The wrapper structure for external memory resource handle desc +class external_mem_handle_desc { +#ifdef _WIN32 + void *handle; + const void *name; +#else + int handle; +#endif // _WIN32 + sycl::ext::oneapi::experimental::external_mem_handle_type handle_type; + size_t size_in_bytes; + int flags; + +public: + external_mem_handle_desc() { +#ifdef _WIN32 + handle = nullptr; + name = nullptr; +#else + handle = -1; +#endif // _WIN32 + handle_type = + sycl::ext::oneapi::experimental::external_mem_handle_type::opaque_fd; + size_in_bytes = 0; + flags = 0; + } + +#ifdef _WIN32 + /// handle setter + /// \param [in] win32_handle The win32 NT handle of the external memory + /// resource + void set_win32_handle(void *win32_handle) { handle = win32_handle; } + + /// name setter + /// \param [in] win32_obj_name The object of the external memory resource + void set_win32_obj_name(const void *win32_obj_name) { name = win32_obj_name; } +#else + /// fd setter + /// \param [in] fd The file descriptor of the external memory resource + void set_fd_handle(int fd) { handle = fd; } +#endif // _WIN32 + + /// handle_type setter + /// \param [in] type The type of the external memory resource handle + void set_handle_type( + sycl::ext::oneapi::experimental::external_mem_handle_type type) { + handle_type = type; + } + + /// size setter + /// \param [in] size The size (in bytes) of the external memory resource + void set_res_size(unsigned long long size) { size_in_bytes = size; } + + /// flags setter + /// \param [in] flags The flags used to handle importing of external memory + /// resource + void set_flags(int flags) { this->flags = flags; } + +#ifdef _WIN32 + /// handle getter + /// \param [out] handle The win32 NT handle of the external memory resource + void *get_win32_handle() { return handle; } + + /// name getter + /// \param [out] name The object of the external memory resource + const void *get_win32_obj_name() { return name; } +#else + /// fd getter + /// \param [out] fd The file descriptor of the external memory resource + int get_fd_handle() { return handle; } +#endif // _WIN32 + + /// handle_type getter + /// \param [out] handle_type The type of the external memory resource handle + sycl::ext::oneapi::experimental::external_mem_handle_type get_handle_type() { + return handle_type; + } + + /// size getter + /// \param [out] size The size (in bytes) of the external memory resource + unsigned long long get_res_size() { return size_in_bytes; } + + /// flags getter + /// \param [out] flags The flags used to handle importing of external memory + /// resource + int get_flags() { return flags; } +}; + +/// The wrapper structure for external image memory desc +class external_mem_img_desc { + sycl::range<3> size; + image_channel channel; + unsigned int num_levels; + sycl::ext::oneapi::experimental::image_type type; + +public: + external_mem_img_desc() { + size = sycl::range<3>{0, 0, 0}; + channel = image_channel::create(); + num_levels = 1; + type = sycl::ext::oneapi::experimental::image_type::standard; + } + + /// size setter + /// \param [in] img_dims The dimensions of the imported image resource + void set_size(sycl::range<3> img_dims) { + size = img_dims; + + for (int i = 0; i < 3; i++) { + if (size[i] == 1) { + size[i] = 0; + } + } + } + + /// image_channel setter + /// \param [in] img_ch The channel info of the imported image resource + void set_image_channel(image_channel img_ch) { channel = img_ch; } + + /// num_levels setter + /// \param [in] numLevels The no. of levels in the imported image resource + void set_num_levels(unsigned int num_levels) { + this->num_levels = num_levels; + + if (num_levels > 1) { + set_image_type(sycl::ext::oneapi::experimental::image_type::mipmap); + } + } + + /// type setter + /// \param [in] img_type The type of the imported image resource + void set_image_type(sycl::ext::oneapi::experimental::image_type img_type) { + type = img_type; + } + + /// size getter + /// \param [out] size The dimensions of the imported image resource + sycl::range<3> get_size() { return size; } + + /// image_channel getter + /// \param [out] image_channel The channel info of the imported image resource + image_channel get_image_channel() { return channel; } + + /// num_levels getter + /// \param [out] num_levels The no. of levels in the imported image resource + unsigned int get_num_levels() { return num_levels; } + + /// type getter + /// \param [out] type The image type of the imported image resource + sycl::ext::oneapi::experimental::image_type get_image_type() { return type; } +}; + +/// The wrapper structure for external buffer memory desc +class external_mem_buf_desc { + uint64_t size_in_bytes; + uint64_t offset; + unsigned int flags; + +public: + external_mem_buf_desc() { + size_in_bytes = 0; + offset = 0; + flags = 0; + } + + /// size setter + /// \param [in] size The size (in bytes) of the external buffer resource + void set_res_size(uint64_t size) { size_in_bytes = size; } + + /// offset setter + /// \param [in] offset The memory offset of the external buffer resource + void set_mem_offset(uint64_t offset) { this->offset = offset; } + + /// flags setter + /// \param [in] flags The flags used to import external buffer resource + void set_flags(unsigned int flags) { this->flags = flags; } + + /// size getter + /// \param [out] size_in_bytes The size (in bytes) of the external buffer res + uint64_t get_res_size() { return size_in_bytes; } + + /// offset getter + /// \param [out] offset The memory offset of the external buffer resource + uint64_t get_mem_offset() { return offset; } + + /// flags getter + /// \param [out] flags The flags used to import external buffer resource + unsigned int get_flags() { return flags; } +}; + +/// The wrapper class for external semaphore resource handle desc +class external_sem_handle_desc { +#ifdef _WIN32 + void *handle; + const void *name; +#else + int handle; +#endif // _WIN32 + sycl::ext::oneapi::experimental::external_semaphore_handle_type handle_type; + int flags; + +public: + external_sem_handle_desc() { +#ifdef _WIN32 + handle = nullptr; + name = nullptr; +#else + handle = -1; +#endif // _WIN32 + handle_type = sycl::ext::oneapi::experimental:: + external_semaphore_handle_type::opaque_fd; + flags = 0; + } + +#ifdef _WIN32 + /// handle setter + /// \param [in] win32_handle The win32 NT handle of the external semaphore + /// resource + void set_win32_handle(void *win32_handle) { handle = win32_handle; } + + /// name setter + /// \param [in] win32_obj_name The object of the external semaphore resource + void set_win32_obj_name(const void *win32_obj_name) { name = win32_obj_name; } +#else + /// fd setter + /// \param [in] fd The file descriptor of the external semaphore resource + void set_fd_handle(int fd) { handle = fd; } +#endif // _WIN32 + + /// handle_type setter + /// \param [in] type The type of the external semaphore resource handle + void set_handle_type( + sycl::ext::oneapi::experimental::external_semaphore_handle_type type) { + handle_type = type; + } + + /// flags setter + /// \param [in] flags The flags used to handle importing of external semaphore + /// resource + void set_flags(int flags) { this->flags = flags; } + +#ifdef _WIN32 + /// handle getter + /// \param [out] handle The win32 NT handle of the external semaphore resource + void *get_win32_handle() { return handle; } + + /// name getter + /// \param [out] name The object of the external semaphore resource + const void *get_win32_obj_name() { return name; } +#else + /// fd getter + /// \param [out] fd The file descriptor of the external semaphore resource + int get_fd_handle() { return handle; } +#endif // _WIN32 + + /// handle_type getter + /// \param [out] handle_type The type of the external semaphore resource + /// handle + sycl::ext::oneapi::experimental::external_semaphore_handle_type + get_handle_type() { + return handle_type; + } + + /// flags getter + /// \param [out] flags The flags used to handle importing of external + /// semaphore + int get_flags() { return flags; } +}; + +/// The wrapper structure for external semaphore signal/wait params +class external_sem_params { + uint64_t value; + +public: + external_sem_params() { value = 0; } + + /// value setter + /// \param [in] val The state value of the semaphore + void set_value(unsigned long long val) { value = val; } + + /// value getter + /// \param [out] value The state value of the semaphore + unsigned long long get_value() { return value; } +}; + +/// The wrapper class of external semaphore +class external_sem_wrapper { + sycl::ext::oneapi::experimental::external_semaphore ext_sem; + sycl::ext::oneapi::experimental::external_semaphore_handle_type + ext_sem_handle_type; + +public: + /// Create external semaphore wrapper. + /// \param [in] ext_sem The external semaphore resource object + /// \param [in] ext_sem_handle_type The external semaphore resource handle + /// type + external_sem_wrapper( + sycl::ext::oneapi::experimental::external_semaphore ext_sem, + sycl::ext::oneapi::experimental::external_semaphore_handle_type + ext_sem_handle_type) { + this->ext_sem = ext_sem; + this->ext_sem_handle_type = ext_sem_handle_type; + } + + /// destory external semaphore resource + ~external_sem_wrapper() { + sycl::ext::oneapi::experimental::release_external_semaphore( + ext_sem, get_default_queue()); + } + + /// ext_sem getter + /// \param [out] ext_sem The external semaphore resource object + sycl::ext::oneapi::experimental::external_semaphore get() { return ext_sem; } + + /// ext_sem_handle_type getter + /// \param [out] ext_sem_handle_type The external semaphore resource handle + /// type + sycl::ext::oneapi::experimental::external_semaphore_handle_type + get_handle_type() { + return ext_sem_handle_type; + } +}; + +/// The wrapper class of bindless image memory handle. +class image_mem_wrapper { +public: + /// Create bindless image memory wrapper. + /// \tparam dimensions The dimensions of image memory. + /// \param [in] channel The image channel used to create bindless image + /// \param [in] range The sizes of each dimension of bindless image memory. + /// memory. + template + image_mem_wrapper(image_channel channel, sycl::range range, + sycl::ext::oneapi::experimental::image_type type = + sycl::ext::oneapi::experimental::image_type::standard, + unsigned int num_levels = 1) + : _channel(channel) { + unsigned array_size = 1; + if (type == sycl::ext::oneapi::experimental::image_type::array) { + assert(dimensions == 3); + array_size = range[2]; + range[2] = 0; + } + _desc = sycl::ext::oneapi::experimental::image_descriptor( + range, _channel.get_channel_num(), _channel.get_channel_type(), type, + num_levels, array_size); + auto q = get_default_queue(); + _handle = alloc_image_mem(_desc, q); + init_mip_level_wrappers(q); + } + /// Create bindless image memory wrapper. + /// \param [in] channel The image channel used to create bindless image + /// \param [in] width The width of bindless image memory. + /// \param [in] height The height of bindless image memory. + /// \param [in] depth The depth of bindless image memory. + image_mem_wrapper(image_channel channel, size_t width, size_t height = 0, + size_t depth = 0) + : image_mem_wrapper(channel, {width, height, depth}) {} + /// Create bindless image memory wrapper. + /// \param [in] desc The image descriptor used to create bindless image + image_mem_wrapper( + const sycl::ext::oneapi::experimental::image_descriptor *desc) + : _desc(*desc) { + _channel.set_channel_type(desc->channel_type); + _channel.set_channel_num(desc->num_channels); + auto q = get_default_queue(); + _handle = alloc_image_mem(_desc, q); + } + /// Create bindless image memory wrapper. + /// \param [in] desc The image descriptor of bindless image. + /// \param [in] img_mem_handle The memory handle of bindless image. + image_mem_wrapper( + sycl::ext::oneapi::experimental::image_descriptor desc, + sycl::ext::oneapi::experimental::image_mem_handle img_mem_handle) + : _desc(desc), _handle(img_mem_handle) { + _channel.set_channel_type(_desc.channel_type); +#if (__SYCL_COMPILER_VERSION && __SYCL_COMPILER_VERSION >= 20240725) + _channel.set_channel_num(_desc.num_channels); +#endif + auto q = get_default_queue(); + init_mip_level_wrappers(q); + } + /// Create bindless image memory wrapper. + /// \param [in] extMem SYCL external memory resource object. + /// \param [in] ext_img_mem_desc The img desc of the imported image resource. + image_mem_wrapper(sycl::ext::oneapi::experimental::external_mem extMem, + external_mem_img_desc *ext_img_mem_desc) { + auto q = get_default_queue(); + unsigned int array_size = 1; + + _channel = ext_img_mem_desc->get_image_channel(); + _desc = sycl::ext::oneapi::experimental::image_descriptor( + ext_img_mem_desc->get_size(), _channel.get_channel_num(), + _channel.get_channel_type(), ext_img_mem_desc->get_image_type(), + ext_img_mem_desc->get_num_levels(), array_size); + _handle = sycl::ext::oneapi::experimental::map_external_image_memory( + extMem, _desc, q); + + init_mip_level_wrappers(q); + } + /// Create bindless image memory wrapper. + /// \param [in] desc The image descriptor of bindless image. + /// \param [in] num_levels Number of mipmap levels to allocate + image_mem_wrapper(sycl::ext::oneapi::experimental::image_descriptor desc, + unsigned int num_levels) + : _desc(desc) { + _desc.type = sycl::ext::oneapi::experimental::image_type::mipmap; + _desc.num_levels = num_levels; + auto q = get_default_queue(); + _handle = alloc_image_mem(_desc, q); + init_mip_level_wrappers(q); + } + + image_mem_wrapper(const image_mem_wrapper &) = delete; + image_mem_wrapper &operator=(const image_mem_wrapper &) = delete; + /// Destroy bindless image memory wrapper. + ~image_mem_wrapper() { + if (_sub_wrappers) { + std::destroy_n(_sub_wrappers, _desc.num_levels); + std::free(_sub_wrappers); + } + free_image_mem(_handle, _desc.type, get_default_queue()); + } + /// Get the image channel of the bindless image memory. + /// \returns The image channel of bindless image memory. + image_channel get_channel() const noexcept { return _channel; } + /// Get the sizes of each dimension of the bindless image memory. + /// \returns The sizes of each dimension of bindless image memory. + sycl::range<3> get_range() const { + return {_desc.width, _desc.height, _desc.depth}; + } + /// Get the image descriptor of the bindless image memory. + /// \returns The image descriptor of bindless image memory. + const sycl::ext::oneapi::experimental::image_descriptor & + get_desc() const noexcept { + return _desc; + } + /// Get the image handle of the bindless image memory. + /// \returns The image handle of bindless image memory. + sycl::ext::oneapi::experimental::image_mem_handle + get_handle() const noexcept { + return _handle; + } + /// Get the image mip level of the bindless image memory. + /// \returns The image mip level of the bindless image memory. + image_mem_wrapper *get_mip_level(unsigned int level) { + if (_desc.type == sycl::ext::oneapi::experimental::image_type::standard) { + assert(level == 0); + return this; + } + + assert(_desc.type == sycl::ext::oneapi::experimental::image_type::mipmap); + return _sub_wrappers + level; + } + + sycl::ext::oneapi::experimental::image_type get_image_type(void) { + return _desc.type; + } + +private: + image_mem_wrapper( + const image_channel &channel, + const sycl::ext::oneapi::experimental::image_descriptor &desc, + const sycl::ext::oneapi::experimental::image_mem_handle &handle) + : _channel(channel), _desc(desc), _handle(handle) {} + + /// Initialize image mem wrappers for all the levels of mipmap image + /// \param [in] q The Queue to be used to query the mip levels + void init_mip_level_wrappers(sycl::queue q) { + auto num_levels = _desc.num_levels; + if (_desc.type == sycl::ext::oneapi::experimental::image_type::mipmap) { + assert(num_levels > 1); + _sub_wrappers = (image_mem_wrapper *)std::malloc( + sizeof(image_mem_wrapper) * num_levels); + for (unsigned i = 0; i < num_levels; ++i) + new (_sub_wrappers + i) image_mem_wrapper( + _channel, _desc.get_mip_level_desc(i), + sycl::ext::oneapi::experimental::get_mip_level_mem_handle( + _handle, i, q.get_device(), q.get_context())); + } + } + + image_channel _channel; + sycl::ext::oneapi::experimental::image_descriptor _desc; + sycl::ext::oneapi::experimental::image_mem_handle _handle; + image_mem_wrapper *_sub_wrappers{nullptr}; +}; + +/// The base wrapper class of external memory resource handle. +class external_mem_wrapper_base { +public: + /// Cleans up the external mem wrapper by releasing the SYCL external + /// memory handle + virtual ~external_mem_wrapper_base() {} + + external_mem_wrapper_base(const external_mem_wrapper_base &) = delete; + + external_mem_wrapper_base & + operator=(const external_mem_wrapper_base &) = delete; + + const sycl::ext::oneapi::experimental::external_mem + get_external_mem() const noexcept { + return _res_external_mem; + } + + /// Map the buf/texture resource memory and get its pointer/mem handle. + /// \param [in] q The queue used to map the resource with. + void map_resource(sycl::queue q) { + if (_res_is_buffer) { + if (_res_buf_ptr) { + throw std::runtime_error("Resource is already mapped! " + "Resource cannot be mapped more than once."); + } + +#if (__SYCL_COMPILER_VERSION && __SYCL_COMPILER_VERSION >= 20240823) + _res_buf_ptr = + sycl::ext::oneapi::experimental::map_external_linear_memory( + get_external_mem(), 0, _res_size_bytes, q); +#else + throw std::runtime_error( + "Mapping a buffer resoure is not yet supported!"); +#endif + } else { + if (_res_img_mem_wrapper_ptr) { + throw std::runtime_error("Resource is already mapped! " + "Resource cannot be mapped more than once."); + } + + auto img_mem_handle = + sycl::ext::oneapi::experimental::map_external_image_memory( + get_external_mem(), _res_img_desc, q); + + _res_img_mem_wrapper_ptr = + new image_mem_wrapper(_res_img_desc, img_mem_handle); + } + } + + /// Gets the pointer and size of the mapped resource + /// \param [out] ptr The pointer to the resource memory. + /// \param [out] size The size of the resource memory. + /// and resource size. + void get_mapped_pointer(void **ptr, size_t *size) { + if (!_res_is_buffer) { + throw std::runtime_error( + "Non buffer resouce cannot be accessed as a pointer!"); + } + + *ptr = _res_buf_ptr; + if (!*ptr) { + throw std::runtime_error( + "Resource is not mapped! " + "Resource should be mapped before accessing its memory"); + } + + *size = _res_size_bytes; + } + + /// Returns mapped memory as image_mem_wrapper* + image_mem_wrapper *get_mapped_mipmapped_array() { + if (_res_is_buffer) { + throw std::runtime_error( + "Buffer resource cannot be accessed as an array!"); + } + if (!_res_img_mem_wrapper_ptr) { + throw std::runtime_error( + "Resource is not mapped! " + "Resource should be mapped before accessing its memory."); + } + + return _res_img_mem_wrapper_ptr; + } + + /// Returns mapped subresource memory as image_mem_wrapper* + /// \param [in] array_index The layer of the image array to be returned. + /// \param [in] mip_level The level of the mipmap image to be returned. + image_mem_wrapper *get_sub_resource_mapped_array(unsigned int array_index, + unsigned int mip_level) { + if (_res_is_buffer) { + throw std::runtime_error( + "Buffer resouce cannot be accessed as an array!"); + } + if (array_index != 0) { + throw std::runtime_error("Retrieving a particular layer from image array " + "is not yet supported!"); + } + if (array_index >= _res_img_desc.array_size) { + throw std::runtime_error( + "Array index requested exceeds total array size in resource!"); + } + if (mip_level >= _res_img_desc.num_levels) { + throw std::runtime_error( + "Mip level requested exceeds total num of levels in resource!"); + } + if (!_res_img_mem_wrapper_ptr) { + throw std::runtime_error( + "Resouce is not mapped! " + "Resource should be mapped before accessing its memory"); + } + + return (mip_level == 0) + ? _res_img_mem_wrapper_ptr + : _res_img_mem_wrapper_ptr->get_mip_level(mip_level); + } + + /// Unmap the buf/texture resource memory and get its pointer/mem handle. + /// \param [in] q The queue used to unmap the resource with. + void unmap_resource(sycl::queue q) { + if (_res_is_buffer) { + if (!_res_buf_ptr) { + throw std::runtime_error("Resource is not mapped! " + "Resource should be mapped before unmapping."); + } + + _res_buf_ptr = nullptr; + + throw std::runtime_error( + "Unmapping a buffer resoure is not yet supported!"); + } else { + if (!_res_img_mem_wrapper_ptr) { + throw std::runtime_error("Resource is not mapped! " + "Resource should be mapped before unmapping."); + } + + delete _res_img_mem_wrapper_ptr; + _res_img_mem_wrapper_ptr = nullptr; + } + } + +protected: + /// Init external mem handle for a given resource. + external_mem_wrapper_base( + sycl::ext::oneapi::experimental::external_mem external_mem, + unsigned reg_flags) + : _res_external_mem(external_mem), _res_reg_flags(reg_flags) {} + + unsigned _res_reg_flags = 0; + bool _res_is_buffer = false; + size_t _res_size_bytes = 0; + + sycl::ext::oneapi::experimental::image_descriptor _res_img_desc; + sycl::ext::oneapi::experimental::external_mem _res_external_mem; + +private: + void *_res_buf_ptr = nullptr; + image_mem_wrapper *_res_img_mem_wrapper_ptr = nullptr; +}; + +#ifdef _WIN32 +class external_mem_wrapper : public external_mem_wrapper_base { +public: + /// Creates external memory resource wrapper for a given DX11 resource. + /// \param [in] d3d11_res Pointer to the ID3D11Resource to be wrapped. + /// \param [in] reg_flags Registration flags used for resource registration. + external_mem_wrapper(ID3D11Resource *d3d11_res, unsigned reg_flags) + : _res_D3D11(d3d11_res), + external_mem_wrapper_base( + sycl::ext::oneapi::experimental::external_mem(), reg_flags) { + // Check if the resource is already registered to prevent double + // registration. + if (_res_D3D11_set.find(_res_D3D11) != _res_D3D11_set.end()) { + throw std::runtime_error("This D3D11 resource is already registered!"); + } + + // Query the information from D3D11 resource object + query_res_info(_res_D3D11); + + // Create a shared handle for the DX11 resource to enable interoperability. + _res_win_nt_handle = create_shared_handle(_res_D3D11); + + // Prepare the external memory resource descriptor for SYCL interop. + sycl::ext::oneapi::experimental::resource_win32_handle ext_mem_win_handle{ + _res_win_nt_handle}; + + sycl::ext::oneapi::experimental::external_mem_descriptor< + sycl::ext::oneapi::experimental::resource_win32_handle> + ext_mem_desc{ext_mem_win_handle, + sycl::ext::oneapi::experimental::external_mem_handle_type:: + win32_nt_dx12_resource, + _res_size_bytes}; + + // Import resource's external memory into SYCL for use with SYCL APIs. + _res_external_mem = sycl::ext::oneapi::experimental::external_mem( + sycl::ext::oneapi::experimental::import_external_memory( + ext_mem_desc, get_default_queue())); + + // Register the resource to track its uniqueness. + _res_D3D11_set.insert(_res_D3D11); + } + + /// Cleans up the external mem wrapper by releasing the SYCL external + /// memory handle and the shared handle created for the DX11 resource. + virtual ~external_mem_wrapper() { + // Release the mutex when done + _res_keyed_mutex->ReleaseSync(0); + + // Release the keyed mutex interface + _res_keyed_mutex->Release(); + + // Close the shared handle to the DX11 resource. + CloseHandle(_res_win_nt_handle); + + // Release the resource's external memory associated with the SYCL external + // mem handle. + sycl::ext::oneapi::experimental::release_external_memory( + get_external_mem(), get_default_queue()); + + // Remove the resource from the tracking set. + if (_res_D3D11_set.find(_res_D3D11) != _res_D3D11_set.end()) { + _res_D3D11_set.erase(_res_D3D11); + } + } + +private: + inline static std::set _res_D3D11_set; + + ID3D11Resource *_res_D3D11 = nullptr; + IDXGIKeyedMutex *_res_keyed_mutex = nullptr; + HANDLE _res_win_nt_handle = nullptr; + + /// Helper function to query the properties of DX11 resource + void query_res_info(ID3D11Resource *resource) { + unsigned int res_width = 0; + unsigned int res_height = 0; + unsigned int res_depth = 0; + unsigned int res_num_levels = 1; + unsigned int res_arr_size = 1; + + image_channel channel; + + // Get the dimension info of DX11 resource + D3D11_RESOURCE_DIMENSION dimension; + resource->GetType(&dimension); + + switch (dimension) { + case D3D11_RESOURCE_DIMENSION_BUFFER: { + _res_is_buffer = true; + + D3D11_BUFFER_DESC desc; + ((ID3D11Buffer *)resource)->GetDesc(&desc); + + _res_size_bytes = desc.ByteWidth; + break; + } + case D3D11_RESOURCE_DIMENSION_TEXTURE1D: { + D3D11_TEXTURE1D_DESC desc; + ((ID3D11Texture1D *)resource)->GetDesc(&desc); + + res_width = desc.Width; + res_arr_size = desc.ArraySize; + res_num_levels = (desc.MipLevels ? desc.MipLevels : 1); + + channel = get_img_ch_info(desc.Format); + _res_size_bytes = res_width; + break; + } + case D3D11_RESOURCE_DIMENSION_TEXTURE2D: { + D3D11_TEXTURE2D_DESC desc; + ((ID3D11Texture2D *)resource)->GetDesc(&desc); + + res_width = desc.Width; + res_height = desc.Height; + res_arr_size = desc.ArraySize; + res_num_levels = (desc.MipLevels ? desc.MipLevels : 1); + + channel = get_img_ch_info(desc.Format); + _res_size_bytes = res_width * res_height; + break; + } + case D3D11_RESOURCE_DIMENSION_TEXTURE3D: { + D3D11_TEXTURE3D_DESC desc; + ((ID3D11Texture3D *)resource)->GetDesc(&desc); + + res_width = desc.Width; + res_height = desc.Height; + res_depth = desc.Depth; + res_num_levels = (desc.MipLevels ? desc.MipLevels : 1); + + channel = get_img_ch_info(desc.Format); + _res_size_bytes = res_width * res_height * res_depth; + break; + } + default: + throw std::runtime_error("Unsupported DX11 resource type!"); + break; + } + + if (!_res_is_buffer) { + if (res_arr_size > 1) { + throw std::runtime_error( + "Images with multiple array levels are not supported. " + "Only standard and mipmap images are supported!"); + } + + // Calculate the size of a resource in bytes by multiplyting its dims + // with arr size, num levels & channel size + _res_size_bytes *= + res_arr_size * res_num_levels * channel.get_total_size(); + + sycl::ext::oneapi::experimental::image_type img_type = + (res_num_levels > 1) + ? sycl::ext::oneapi::experimental::image_type::mipmap + : sycl::ext::oneapi::experimental::image_type::standard; + + _res_img_desc = sycl::ext::oneapi::experimental::image_descriptor{ + sycl::range<3>{res_width, res_height, res_depth}, + channel.get_channel_num(), + channel.get_channel_type(), + img_type, + res_num_levels, + res_arr_size}; + } + } + + /// Helper function to create shared handle for DXD11 resource + HANDLE create_shared_handle(ID3D11Resource *resource) { + HANDLE win_nt_handle = nullptr; + + IDXGIResource1 *pDXGIResource1; + resource->QueryInterface(__uuidof(IDXGIResource1), + reinterpret_cast(&pDXGIResource1)); + + if (_res_is_buffer) { + pDXGIResource1->GetSharedHandle(&win_nt_handle); + } else { + pDXGIResource1->CreateSharedHandle( + nullptr, DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, + nullptr, &win_nt_handle); + } + + pDXGIResource1->Release(); + + // Query for the keyed mutex interface + resource->QueryInterface(__uuidof(IDXGIKeyedMutex), + reinterpret_cast(&_res_keyed_mutex)); + + // Use the keyed mutex to synchronize access to the shared resource + _res_keyed_mutex->AcquireSync(0, INFINITE); + + return win_nt_handle; + } + + /// Helper function to get the texture channel info of DX11 resource using + /// DXGI format + image_channel get_img_ch_info(DXGI_FORMAT format) { + image_channel channel; + + switch (format) { + case DXGI_FORMAT_R16G16B16A16_FLOAT: + channel = image_channel::create(); + break; + case DXGI_FORMAT_R8G8B8A8_UNORM: + channel = image_channel::create(); + break; + case DXGI_FORMAT_R16G16_FLOAT: + channel = image_channel::create(); + break; + case DXGI_FORMAT_R32_FLOAT: + channel = image_channel::create(); + break; + default: + throw std::runtime_error("Unsupported DX11 resource format!"); + break; + } + + return channel; + } +}; +#endif // _WIN32 + +namespace detail { +struct sampled_image_handle_compare { + template bool operator()(T L, T R) const { + return L.raw_handle < R.raw_handle; + } +}; + +inline std::pair &get_img_info_map( + const sycl::ext::oneapi::experimental::sampled_image_handle handle) { + static std::map, + sampled_image_handle_compare> + img_info_map; + return img_info_map[handle]; +} + +inline std::pair &get_img_info_map( + const sycl::ext::oneapi::experimental::unsampled_image_handle handle) { + static std::map, + sampled_image_handle_compare> + img_info_map; + return img_info_map[handle]; +} + +static inline size_t +get_ele_size(const sycl::ext::oneapi::experimental::image_descriptor &decs) { + size_t channel_size; + switch (decs.channel_type) { + case sycl::image_channel_type::signed_int8: + case sycl::image_channel_type::unsigned_int8: + channel_size = 1; + break; + case sycl::image_channel_type::fp16: + case sycl::image_channel_type::signed_int16: + case sycl::image_channel_type::unsigned_int16: + channel_size = 2; + break; + case sycl::image_channel_type::fp32: + case sycl::image_channel_type::signed_int32: + case sycl::image_channel_type::unsigned_int32: + channel_size = 4; + break; + default: + throw std::runtime_error("Unsupported channel_type in get_ele_size!"); + break; + } + return decs.num_channels * channel_size; +} + +static inline sycl::event +dpct_memcpy(sycl::ext::oneapi::experimental::image_mem_handle src, + const sycl::ext::oneapi::experimental::image_descriptor &desc_src, + size_t w_offset_src, size_t h_offset_src, void *dest, size_t p, + size_t w, size_t h, sycl::queue q) { + const auto ele_size = get_ele_size(desc_src); + const auto src_offset = + sycl::range<3>(w_offset_src / ele_size, h_offset_src, 0); + const auto dest_offset = sycl::range<3>(0, 0, 0); + const auto dest_extend = sycl::range<3>(p / ele_size, 0, 0); + const auto copy_extend = sycl::range<3>(w / ele_size, h, 0); + return q.ext_oneapi_copy(src, src_offset, desc_src, dest, dest_offset, + dest_extend, copy_extend); +} + +static inline std::vector dpct_memcpy_to_host( + sycl::ext::oneapi::experimental::image_mem_handle src, + const sycl::ext::oneapi::experimental::image_descriptor &desc_src, + size_t w_offset_src, size_t h_offset_src, void *dest_host_ptr, size_t s, + sycl::queue q) { + std::vector event_list; + const auto ele_size = get_ele_size(desc_src); + const auto w = desc_src.width * ele_size; + size_t offset_dest = 0; + while (s - offset_dest > w - w_offset_src) { + const auto src_offset = + sycl::range<3>(w_offset_src / ele_size, h_offset_src, 0); + const auto dest_offset = sycl::range<3>(offset_dest / ele_size, 0, 0); + const auto dest_extend = sycl::range<3>(0, 0, 0); + const auto copy_extend = + sycl::range<3>((w - w_offset_src) / ele_size, 1, 0); + event_list.push_back(q.ext_oneapi_copy(src, src_offset, desc_src, + dest_host_ptr, dest_offset, + dest_extend, copy_extend)); + offset_dest += w - w_offset_src; + w_offset_src = 0; + ++h_offset_src; + } + if (s - offset_dest > 0) { + const auto src_offset = + sycl::range<3>(w_offset_src / ele_size, h_offset_src, 0); + const auto dest_offset = sycl::range<3>(offset_dest / ele_size, 0, 0); + const auto dest_extend = sycl::range<3>(0, 0, 0); + const auto copy_extend = sycl::range<3>((s - offset_dest) / ele_size, 1, 0); + event_list.push_back(q.ext_oneapi_copy(src, src_offset, desc_src, + dest_host_ptr, dest_offset, + dest_extend, copy_extend)); + } + return event_list; +} + +static inline std::vector +dpct_memcpy(sycl::ext::oneapi::experimental::image_mem_handle src, + const sycl::ext::oneapi::experimental::image_descriptor &desc_src, + size_t w_offset_src, size_t h_offset_src, void *dest, size_t s, + sycl::queue q) { + if (dpct::detail::get_pointer_attribute(q, dest) == + dpct::detail::pointer_access_attribute::device_only) { + std::vector event_list; + dpct::detail::host_buffer buf(s, q, event_list); + auto copy_events = dpct_memcpy_to_host(src, desc_src, w_offset_src, + h_offset_src, buf.get_ptr(), s, q); + event_list.push_back(dpct::detail::dpct_memcpy( + q, dest, buf.get_ptr(), s, memcpy_direction::host_to_device, + copy_events)); + return event_list; + } + return dpct_memcpy_to_host(src, desc_src, w_offset_src, h_offset_src, dest, s, + q); +} + +static inline sycl::event +dpct_memcpy(const void *src, + sycl::ext::oneapi::experimental::image_mem_handle dest, + const sycl::ext::oneapi::experimental::image_descriptor &desc_dest, + size_t w_offset_dest, size_t h_offset_dest, size_t p, size_t w, + size_t h, sycl::queue q) { + const auto ele_size = get_ele_size(desc_dest); + const auto src_offset = sycl::range<3>(0, 0, 0); + const auto src_extend = sycl::range<3>(p / ele_size, 0, 0); + const auto dest_offset = + sycl::range<3>(w_offset_dest / ele_size, h_offset_dest, 0); + const auto copy_extend = sycl::range<3>(w / ele_size, h, 0); + // TODO: Remove const_cast after refining the signature of ext_oneapi_copy. + return q.ext_oneapi_copy(const_cast(src), src_offset, src_extend, + dest, dest_offset, desc_dest, copy_extend); +} + +static inline std::vector dpct_memcpy_from_host( + const void *src_host_ptr, + sycl::ext::oneapi::experimental::image_mem_handle dest, + const sycl::ext::oneapi::experimental::image_descriptor &desc_dest, + size_t w_offset_dest, size_t h_offset_dest, size_t s, sycl::queue q) { + std::vector event_list; + const auto ele_size = get_ele_size(desc_dest); + const auto w = desc_dest.width * ele_size; + size_t offset_src = 0; + while (s - offset_src >= w - w_offset_dest) { + const auto src_offset = sycl::range<3>(offset_src / ele_size, 0, 0); + const auto src_extend = sycl::range<3>(0, 0, 0); + const auto dest_offset = + sycl::range<3>(w_offset_dest / ele_size, h_offset_dest, 0); + const auto copy_extend = + sycl::range<3>((w - w_offset_dest) / ele_size, 1, 0); + // TODO: Remove const_cast after refining the signature of ext_oneapi_copy. + event_list.push_back(q.ext_oneapi_copy( + const_cast(src_host_ptr), src_offset, src_extend, dest, + dest_offset, desc_dest, copy_extend)); + offset_src += w - w_offset_dest; + w_offset_dest = 0; + ++h_offset_dest; + } + if (s - offset_src > 0) { + const auto src_offset = sycl::range<3>(offset_src / ele_size, 0, 0); + const auto src_extend = sycl::range<3>(0, 0, 0); + const auto dest_offset = + sycl::range<3>(w_offset_dest / ele_size, h_offset_dest, 0); + const auto copy_extend = sycl::range<3>((s - offset_src) / ele_size, 1, 0); + // TODO: Remove const_cast after refining the signature of ext_oneapi_copy. + event_list.push_back(q.ext_oneapi_copy( + const_cast(src_host_ptr), src_offset, src_extend, dest, + dest_offset, desc_dest, copy_extend)); + } + return event_list; +} + +static inline std::vector dpct_memcpy( + const void *src, sycl::ext::oneapi::experimental::image_mem_handle dest, + const sycl::ext::oneapi::experimental::image_descriptor &desc_dest, + size_t w_offset_dest, size_t h_offset_dest, size_t s, sycl::queue q) { + if (dpct::detail::get_pointer_attribute(q, src) == + dpct::detail::pointer_access_attribute::device_only) { + std::vector event_list; + dpct::detail::host_buffer buf(s, q, event_list); + event_list.push_back(dpct::detail::dpct_memcpy( + q, buf.get_ptr(), src, s, memcpy_direction::device_to_host)); + auto copy_events = dpct_memcpy_from_host( + buf.get_ptr(), dest, desc_dest, w_offset_dest, h_offset_dest, s, q); + event_list.insert(event_list.end(), copy_events.begin(), copy_events.end()); + return event_list; + } + return dpct_memcpy_from_host(src, dest, desc_dest, w_offset_dest, + h_offset_dest, s, q); +} + +static inline sycl::event +dpct_memcpy(const image_mem_wrapper *src, const sycl::id<3> &src_id, + const size_t src_x_offest_byte, pitched_data &dest, + const sycl::id<3> &dest_id, const size_t dest_x_offest_byte, + const sycl::range<3> &size, const size_t copy_x_size_byte, + sycl::queue q) { + const auto ele_size = get_ele_size(src->get_desc()); + const auto src_offset = sycl::range<3>( + src_x_offest_byte != 0 ? src_x_offest_byte / ele_size : src_id[0], + src_id[1], src_id[2]); + const auto dest_offset = sycl::range<3>( + dest_x_offest_byte != 0 ? dest_x_offest_byte / ele_size : dest_id[0], + dest_id[1], dest_id[2]); + const auto dest_extend = + sycl::range<3>(dest.get_pitch() / ele_size, dest.get_y(), 1); + const auto copy_extend = sycl::range<3>( + copy_x_size_byte != 0 ? copy_x_size_byte / ele_size : size[0], size[1], + size[2]); + return q.ext_oneapi_copy(src->get_handle(), src_offset, src->get_desc(), + dest.get_data_ptr(), dest_offset, dest_extend, + copy_extend); +} + +static inline sycl::event +dpct_memcpy(pitched_data src, const sycl::id<3> &src_id, + const size_t src_x_offest_byte, image_mem_wrapper *dest, + const sycl::id<3> &dest_id, const size_t dest_x_offest_byte, + const sycl::range<3> &size, const size_t copy_x_size_byte, + sycl::queue q) { + const auto ele_size = get_ele_size(dest->get_desc()); + const auto src_offset = sycl::range<3>( + src_x_offest_byte != 0 ? src_x_offest_byte / ele_size : src_id[0], + src_id[1], src_id[2]); + const auto src_extend = + sycl::range<3>(src.get_pitch() / ele_size, src.get_y(), 1); + const auto dest_offset = sycl::range<3>( + dest_x_offest_byte != 0 ? dest_x_offest_byte / ele_size : dest_id[0], + dest_id[1], dest_id[2]); + const auto copy_extend = sycl::range<3>( + copy_x_size_byte != 0 ? copy_x_size_byte / ele_size : size[0], size[1], + size[2]); + return q.ext_oneapi_copy(src.get_data_ptr(), src_offset, src_extend, + dest->get_handle(), dest_offset, dest->get_desc(), + copy_extend); +} + +static inline sycl::event +dpct_memcpy(const image_mem_wrapper *src, const sycl::id<3> &src_id, + const size_t src_x_offest_byte, image_mem_wrapper *dest, + const sycl::id<3> &dest_id, const size_t dest_x_offest_byte, + const sycl::range<3> &size, const size_t copy_x_size_byte, + sycl::queue q) { + // TODO: Need change logic when sycl support image_mem to image_mem copy. + const auto from_ele_size = get_ele_size(src->get_desc()); + const auto to_ele_size = get_ele_size(dest->get_desc()); + const auto copy_extend = sycl::range<3>( + copy_x_size_byte != 0 ? copy_x_size_byte / from_ele_size : size[0], + size[1], size[2]); + std::vector event_list; + dpct::detail::host_buffer buf( + copy_extend.size() * std::max(from_ele_size, to_ele_size), q, event_list); + auto to = pitched_data(buf.get_ptr(), copy_extend[0] * from_ele_size, + copy_extend[0], copy_extend[1]); + dpct_memcpy(src, src_id, src_x_offest_byte, to, sycl::id<3>(0, 0, 0), 0, + copy_extend, 0, q); + auto from = pitched_data(buf.get_ptr(), copy_extend[0] * to_ele_size, + copy_extend[0], copy_extend[1]); + event_list.push_back(dpct_memcpy(from, sycl::id<3>(0, 0, 0), 0, dest, dest_id, + dest_x_offest_byte, copy_extend, 0, q)); + return event_list.front(); +} + +template +inline bool check_duplicate_entries(int count, T **entries) { + std::set uniqueEntries; + + for (size_t i = 0; i < count; ++i) { + auto result = uniqueEntries.insert(entries[i]); + if (!result.second) { + return true; + } + } + + return false; +} +} // namespace detail + +/// Get image_mem_wrapper according to sampled image handle. +/// \param [in] handle The bindless image handle. +/// \returns The image_mem_wrapper of sampled image. +inline image_mem_wrapper *&get_img_mem( + const sycl::ext::oneapi::experimental::sampled_image_handle handle) { + static std::map + img_mem_map; + return img_mem_map[handle]; +} + +/// Get image_mem_wrapper according to unsampled image handle. +/// \param [in] handle The unsampled bindless image handle. +/// \returns The image_mem_wrapper of unsampled image. +inline image_mem_wrapper *&get_img_mem( + const sycl::ext::oneapi::experimental::unsampled_image_handle handle) { + static std::map + img_mem_map; + return img_mem_map[handle]; +} + +/// Associate an image memory wrapper with a bindless unsampled image handle in +/// the global registry. +/// +/// Inserts or updates an entry in the internal image memory map, using the +/// specified bindless unsampled image handle as the key and the provided image +/// memory wrapper as the mapped value. +/// \param [in] handle The unsampled bindless image handle. +/// \param [in] img_mem The image_mem_wrapper associated with the unsampled handle. +static inline void set_img_mem( + const sycl::ext::oneapi::experimental::unsampled_image_handle handle, + image_mem_wrapper *img_mem) { + dpct::experimental::get_img_mem(handle) = img_mem; +} + +/// Associate an image memory wrapper with a bindless sampled image handle in +/// the global registry. +/// +/// Inserts or updates an entry in the internal image memory map, using the +/// specified bindless unsampled image handle as the key and the provided image +/// memory wrapper as the mapped value. +/// \param [in] handle The sampled bindless image handle. +/// \param [in] img_mem The image_mem_wrapper associated with the sampled handle. +static inline void set_img_mem( + const sycl::ext::oneapi::experimental::sampled_image_handle handle, + image_mem_wrapper *img_mem) { + dpct::experimental::get_img_mem(handle) = img_mem; +} + +#ifdef _WIN32 +/// Map the resource memories to mem handles +/// \param [in] count The count of resources to map. +/// \param [in] handles The external mem wrappers used to map the resources. +/// \param [in] q The queue used to map the resource with. +inline void map_resources(int count, external_mem_wrapper **handles, + queue_ptr q_ptr = &get_default_queue()) { + if (detail::check_duplicate_entries(count, handles)) { + throw std::runtime_error( + "Duplicate handle entries found during resource mapping!"); + } + + for (int i = 0; i < count; i++) { + handles[i]->map_resource(*q_ptr); + } +} + +/// Unmap the resource memories to mem handles +/// \param [in] count The count of resources to unmap. +/// \param [in] handles The external mem wrappers used to unmap the resources. +/// \param [in] q The queue used to unmap the resource with. +inline void unmap_resources(int count, external_mem_wrapper **handles, + queue_ptr q_ptr = &get_default_queue()) { + if (detail::check_duplicate_entries(count, handles)) { + throw std::runtime_error( + "Duplicate handle entries found during resource unmapping!"); + } + + for (int i = 0; i < count; i++) { + handles[i]->unmap_resource(*q_ptr); + } +} +#endif // _WIN32 + +/// Imports resource's external memory into a SYCL external memory object. +/// \param [in] extMem SYCL external memory object ptr to be initialized. +/// \param [in] memHandleDesc Pointer to the external memory resource handle +/// descriptor. +/// \throws std::runtime_error If no valid external memory resource handle is +/// provided. +inline void +import_external_memory(sycl::ext::oneapi::experimental::external_mem *extMem, + external_mem_handle_desc *memHandleDesc) { +#ifdef _WIN32 + if (memHandleDesc->get_win32_handle()) { + *extMem = sycl::ext::oneapi::experimental::import_external_memory( + sycl::ext::oneapi::experimental::external_mem_descriptor< + sycl::ext::oneapi::experimental::resource_win32_handle>{ + {memHandleDesc->get_win32_handle()}, + memHandleDesc->get_handle_type(), + memHandleDesc->get_res_size()}, + get_default_queue()); + } else if (memHandleDesc->get_win32_obj_name()) { + throw std::runtime_error( + "Importing external memory resource using Windows NT name" + " type is not supported. Only NT handle type is" + " supported"); + } +#else + if (memHandleDesc->get_fd_handle() != -1) { + *extMem = sycl::ext::oneapi::experimental::import_external_memory( + sycl::ext::oneapi::experimental::external_mem_descriptor< + sycl::ext::oneapi::experimental::resource_fd>{ + {memHandleDesc->get_fd_handle()}, + memHandleDesc->get_handle_type(), + memHandleDesc->get_res_size()}, + get_default_queue()); + } +#endif // _WIN32 + else { + throw std::runtime_error( + "No external memory resource handle is set for importing!"); + } +} + +/// Imports external semaphore resource into a SYCL external semaphore object. +/// \param [in] extSem SYCL external semaphore object ptr to be initialized. +/// \param [in] semHandleDesc Pointer to the external semaphore resource handle +/// descriptor. +/// \throws std::runtime_error If no valid external semaphore resource handle is +/// provided. +inline void import_external_semaphore(external_sem_wrapper **extSem, + external_sem_handle_desc *semHandleDesc) { + sycl::ext::oneapi::experimental::external_semaphore extSemObj; +#ifdef _WIN32 + if (semHandleDesc->get_win32_handle()) { + extSemObj = sycl::ext::oneapi::experimental::import_external_semaphore( + sycl::ext::oneapi::experimental::external_semaphore_descriptor< + sycl::ext::oneapi::experimental::resource_win32_handle>{ + {semHandleDesc->get_win32_handle()}, + semHandleDesc->get_handle_type()}, + get_default_queue()); + } else if (semHandleDesc->get_win32_obj_name()) { + throw std::runtime_error( + "Importing external semaphore resource using Windows NT name" + " type is not supported. Only NT handle type is" + " supported"); + } +#else + if (semHandleDesc->get_fd_handle() != -1) { + extSemObj = sycl::ext::oneapi::experimental::import_external_semaphore( + sycl::ext::oneapi::experimental::external_semaphore_descriptor< + sycl::ext::oneapi::experimental::resource_fd>{ + {semHandleDesc->get_fd_handle()}, semHandleDesc->get_handle_type()}, + get_default_queue()); + } +#endif // _WIN32 + else { + throw std::runtime_error( + "No external semaphore resource handle is set for importing!"); + } + + *extSem = + new external_sem_wrapper(extSemObj, semHandleDesc->get_handle_type()); +} + +/// Signals the external semaphore resource. +/// \param [in] extSem ptr to dpct external semaphore resource ptr +/// \param [in] semSignalParams Pointer to the external semaphore signal +/// parameters. +/// \param [in] numExtSems Number of external semaphores to signal. +/// \param [in] q_ptr The queue used to signal the external semaphore resource. +inline void signal_external_semaphores(external_sem_wrapper **extSem, + external_sem_params *semSignalParams, + unsigned int numExtSems, + queue_ptr q_ptr = &get_default_queue()) { + for (int i = 0; i < numExtSems; i++) { + auto extSemType = extSem[i]->get_handle_type(); + switch (extSemType) { +#ifdef _WIN32 + case sycl::ext::oneapi::experimental::external_semaphore_handle_type:: + win32_nt_dx12_fence: + case sycl::ext::oneapi::experimental::external_semaphore_handle_type:: + timeline_win32_nt_handle: + q_ptr->ext_oneapi_signal_external_semaphore( + extSem[i]->get(), semSignalParams[i].get_value()); + break; + case sycl::ext::oneapi::experimental::external_semaphore_handle_type:: + win32_nt_handle: + q_ptr->ext_oneapi_signal_external_semaphore(extSem[i]->get()); + break; +#else // _WIN32 + case sycl::ext::oneapi::experimental::external_semaphore_handle_type:: + timeline_fd: + q_ptr->ext_oneapi_signal_external_semaphore( + extSem[i]->get(), semSignalParams[i].get_value()); + break; + case sycl::ext::oneapi::experimental::external_semaphore_handle_type:: + opaque_fd: + q_ptr->ext_oneapi_signal_external_semaphore(extSem[i]->get()); + break; +#endif // _WIN32 + default: + throw std::runtime_error( + "Unsupported external semaphore resource handle type!"); + break; + } + } +} + +/// Wait for the external semaphore resource. +/// \param [in] extSem ptr to dpct external semaphore resource ptr +/// \param [in] semWaitParams Pointer to the external semaphore wait parameters. +/// \param [in] numExtSems Number of external semaphores to wait. +/// \param [in] q_ptr The queue used to wait on the external semaphore resource. +inline void wait_external_semaphores(external_sem_wrapper **extSem, + external_sem_params *semWaitParams, + unsigned int numExtSems, + queue_ptr q_ptr = &get_default_queue()) { + for (int i = 0; i < numExtSems; i++) { + auto extSemType = extSem[i]->get_handle_type(); + switch (extSemType) { +#ifdef _WIN32 + case sycl::ext::oneapi::experimental::external_semaphore_handle_type:: + win32_nt_dx12_fence: + case sycl::ext::oneapi::experimental::external_semaphore_handle_type:: + timeline_win32_nt_handle: + q_ptr->ext_oneapi_wait_external_semaphore(extSem[i]->get(), + semWaitParams[i].get_value()); + break; + case sycl::ext::oneapi::experimental::external_semaphore_handle_type:: + win32_nt_handle: + q_ptr->ext_oneapi_wait_external_semaphore(extSem[i]->get()); + break; +#else // _WIN32 + case sycl::ext::oneapi::experimental::external_semaphore_handle_type:: + timeline_fd: + q_ptr->ext_oneapi_wait_external_semaphore(extSem[i]->get(), + semWaitParams[i].get_value()); + break; + case sycl::ext::oneapi::experimental::external_semaphore_handle_type:: + opaque_fd: + q_ptr->ext_oneapi_wait_external_semaphore(extSem[i]->get()); + break; +#endif // _WIN32 + default: + throw std::runtime_error( + "Unsupported external semaphore resource handle type!"); + break; + } + } +} + +/// Create bindless image according to image data and sampling info. +/// \param [in] data The image data used to create bindless image. +/// \param [in] info The image sampling info used to create bindless image. +/// \param [in] q The queue where the image creation be executed. +/// \returns The sampled image handle of created bindless image. +static inline sycl::ext::oneapi::experimental::sampled_image_handle +create_bindless_image(image_data data, sampling_info info, + sycl::queue q = get_default_queue()) { + auto samp = sycl::ext::oneapi::experimental::bindless_image_sampler( + info.get_addressing_mode(), info.get_coordinate_normalization_mode(), + info.get_filtering_mode(), info.get_mipmap_filtering(), + info.get_min_mipmap_level_clamp(), info.get_max_mipmap_level_clamp(), + info.get_max_anisotropy()); + + switch (data.get_data_type()) { + case image_data_type::linear: { + // linear memory only use sycl::filtering_mode::nearest. + samp.filtering = sycl::filtering_mode::nearest; + // TODO: Use pointer to create image when bindless image support. + auto mem = new image_mem_wrapper( + data.get_channel(), data.get_x() / data.get_channel().get_total_size()); + auto img = sycl::ext::oneapi::experimental::create_image( + mem->get_handle(), samp, mem->get_desc(), q); + set_img_mem(img, mem); + auto ptr = data.get_data_ptr(); +#ifdef DPCT_USM_LEVEL_NONE + q.ext_oneapi_copy(get_buffer(ptr).get_host_access().get_pointer(), + mem->get_handle(), mem->get_desc()) + .wait(); +#else + q.ext_oneapi_copy(ptr, mem->get_handle(), mem->get_desc()).wait(); +#endif + detail::get_img_info_map(img) = {data, info}; + return img; + } + case image_data_type::pitch: { +#ifdef DPCT_USM_LEVEL_NONE + auto mem = + new image_mem_wrapper(data.get_channel(), data.get_x(), data.get_y()); + auto img = sycl::ext::oneapi::experimental::create_image( + mem->get_handle(), samp, mem->get_desc(), q); + set_img_mem(img, mem); + q.ext_oneapi_copy( + get_buffer(data.get_data_ptr()).get_host_access().get_pointer(), + mem->get_handle(), mem->get_desc()) + .wait(); +#else + auto desc = sycl::ext::oneapi::experimental::image_descriptor( + {data.get_x(), data.get_y()}, data.get_channel().get_channel_num(), + data.get_channel_type()); + auto img = sycl::ext::oneapi::experimental::create_image( + data.get_data_ptr(), data.get_pitch(), samp, desc, q); +#endif + detail::get_img_info_map(img) = {data, info}; + return img; + } + case image_data_type::matrix: { + const auto mem = static_cast(data.get_data_ptr()); + auto img = sycl::ext::oneapi::experimental::create_image( + mem->get_handle(), samp, mem->get_desc(), q); + detail::get_img_info_map(img) = {data, info}; + return img; + } + default: + throw std::runtime_error( + "Unsupported image_data_type in create_bindless_image!"); + break; + } + // Must not reach here. + return sycl::ext::oneapi::experimental::sampled_image_handle(); +} + +/// Create bindless image according to image data. +/// \param [in] data The image data used to create bindless image. +/// \param [in] q The queue where the image creation be executed. +/// \returns The sampled image handle of created bindless image. +static inline sycl::ext::oneapi::experimental::unsampled_image_handle +create_bindless_image(image_data data, sycl::queue q = get_default_queue()) { + switch (data.get_data_type()) { + case image_data_type::linear: { + // TODO: Use pointer to create image when bindless image support. + auto mem = new image_mem_wrapper( + data.get_channel(), data.get_x() / data.get_channel().get_total_size()); + auto img = sycl::ext::oneapi::experimental::create_image( + mem->get_handle(), mem->get_desc(), q); + set_img_mem(img, mem); + auto ptr = data.get_data_ptr(); +#ifdef DPCT_USM_LEVEL_NONE + q.ext_oneapi_copy(get_buffer(ptr).get_host_access().get_pointer(), + mem->get_handle(), mem->get_desc()) + .wait(); +#else + q.ext_oneapi_copy(ptr, mem->get_handle(), mem->get_desc()).wait(); +#endif + return img; + } + case image_data_type::pitch: { + auto mem = + new image_mem_wrapper(data.get_channel(), data.get_x(), data.get_y()); + auto img = sycl::ext::oneapi::experimental::create_image( + mem->get_handle(), mem->get_desc(), q); + set_img_mem(img, mem); +#ifdef DPCT_USM_LEVEL_NONE + q.ext_oneapi_copy( + get_buffer(data.get_data_ptr()).get_host_access().get_pointer(), + mem->get_handle(), mem->get_desc()) + .wait(); +#else + q.ext_oneapi_copy(data.get_data_ptr(), mem->get_handle(), mem->get_desc()) + .wait(); + +#endif + return img; + } + case image_data_type::matrix: { + const auto mem = static_cast(data.get_data_ptr()); + auto img = sycl::ext::oneapi::experimental::create_image( + mem->get_handle(), mem->get_desc(), q); + return img; + } + default: + throw std::runtime_error( + "Unsupported image_data_type in create_bindless_image!"); + break; + } + // Must not reach here. + return sycl::ext::oneapi::experimental::unsampled_image_handle(); +} + +/// Destroy bindless image. +/// \param [in] handle The bindless image should be destroyed. +/// \param [in] q The queue where the image destruction be executed. +template +static inline void destroy_bindless_image(T handle, + sycl::queue q = get_default_queue()) { + auto &mem = get_img_mem(handle); + if (mem) { + delete mem; + mem = nullptr; + } + sycl::ext::oneapi::experimental::destroy_image_handle(handle, q); +} + +/// Get the image data according to sampled image handle. +/// \param [in] handle The bindless image handle. +/// \returns The image data of sampled image. +template static inline image_data get_data(const T handle) { + return detail::get_img_info_map(handle).first; +} + +/// Get the sampling info according to sampled image handle. +/// \param [in] handle The bindless image handle. +/// \returns The sampling info of sampled image. +static inline sampling_info get_sampling_info( + const sycl::ext::oneapi::experimental::sampled_image_handle handle) { + return detail::get_img_info_map(handle).second; +} + +/// The base class of different template specialization bindless_image_wrapper +/// class. +class bindless_image_wrapper_base { +public: + bindless_image_wrapper_base() { + // Make sure that singleton class dev_mgr will destruct later than this. + dev_mgr::instance(); + } + + /// Destroy bindless image wrapper. + ~bindless_image_wrapper_base() { + destroy_bindless_image(_img, get_default_queue()); + } + + /// Attach linear data to bindless image. + /// \param [in] data The linear data used to create bindless image. + /// \param [in] size The size of linear data used to create bindless image. + /// \param [in] channel The image channel used to create bindless image. + /// \param [in] q The queue where the image creation be executed. + void attach(void *data, size_t size, const image_channel &channel, + sycl::queue q = get_default_queue()) { + detach(q); + auto samp = sycl::ext::oneapi::experimental::bindless_image_sampler( + _addressing_mode, _coordinate_normalization_mode, _filtering_mode); + // TODO: Use pointer to create image when bindless image support. + auto mem = new image_mem_wrapper(channel, size); + _img = sycl::ext::oneapi::experimental::create_image( + mem->get_handle(), samp, mem->get_desc(), q); + set_img_mem(_img, mem); + auto ptr = data; +#ifdef DPCT_USM_LEVEL_NONE + q.ext_oneapi_copy(get_buffer(data).get_host_access().get_pointer(), + mem->get_handle(), mem->get_desc()) + .wait(); +#else + q.ext_oneapi_copy(ptr, mem->get_handle(), mem->get_desc()).wait(); +#endif + } + + /// Attach linear data to bindless image. + /// \param [in] data The linear data used to create bindless image. + /// \param [in] size The size of linear data used to create bindless image. + /// \param [in] q The queue where the image creation be executed. + void attach(void *data, size_t size, sycl::queue q = get_default_queue()) { + attach(data, size, _channel, q); + } + + /// Attach device_ptr data to bindless image. + /// \param [in] desc The image_descriptor used to create bindless image. + /// \param [in] ptr The data pointer used to create bindless image. + /// \param [in] pitch The pitch of 2D data used to create bindless image. + /// \param [in] q The queue where the image creation be executed. + void attach(const sycl::ext::oneapi::experimental::image_descriptor *desc, + device_ptr ptr, size_t pitch, + sycl::queue q = get_default_queue()) { + detach(q); + auto samp = sycl::ext::oneapi::experimental::bindless_image_sampler( + _addressing_mode, _coordinate_normalization_mode, _filtering_mode); +#ifdef DPCT_USM_LEVEL_NONE + auto mem = new image_mem_wrapper(desc); + _img = sycl::ext::oneapi::experimental::create_image(mem->get_handle(), + samp, *desc, q); + set_img_mem(_img, mem); + q.ext_oneapi_copy(get_buffer(ptr).get_host_access().get_pointer(), + mem->get_handle(), mem->get_desc()) + .wait(); +#else + _img = sycl::ext::oneapi::experimental::create_image(ptr, pitch, samp, + *desc, q); +#endif + } + + /// Attach 2D data to bindless image. + /// \param [in] data The 2D data used to create bindless image. + /// \param [in] width The width of 2D data used to create bindless image. + /// \param [in] height The height of 2D data used to create bindless image. + /// \param [in] pitch The pitch of 2D data used to create bindless image. + /// \param [in] channel The image channel used to create bindless image. + /// \param [in] q The queue where the image creation be executed. + void attach(void *data, size_t width, size_t height, size_t pitch, + const image_channel &channel, + sycl::queue q = get_default_queue()) { + auto desc = sycl::ext::oneapi::experimental::image_descriptor( + {width, height}, channel.get_channel_num(), channel.get_channel_type()); + attach(&desc, static_cast(data), pitch, q); + } + + /// Attach 2D data to bindless image. + /// \param [in] data The 2D data used to create bindless image. + /// \param [in] width The width of 2D data used to create bindless image. + /// \param [in] height The height of 2D data used to create bindless image. + /// \param [in] pitch The pitch of 2D data used to create bindless image. + /// \param [in] q The queue where the image creation be executed. + void attach(void *data, size_t width, size_t height, size_t pitch, + sycl::queue q = get_default_queue()) { + attach(data, width, height, pitch, _channel, q); + } + + /// Attach image memory to bindless image. + /// \param [in] mem The image memory used to create bindless image. + /// \param [in] q The queue where the image creation be executed. + void attach(image_mem_wrapper *mem, const image_channel &channel, + sycl::queue q = get_default_queue()) { + detach(q); + auto samp = sycl::ext::oneapi::experimental::bindless_image_sampler( + _addressing_mode, _coordinate_normalization_mode, _filtering_mode); + _img = sycl::ext::oneapi::experimental::create_image( + mem->get_handle(), samp, mem->get_desc(), q); + set_img_mem(_img, mem); + } + + /// Attach image memory to bindless image. + /// \param [in] mem The image memory used to create bindless image. + /// \param [in] q The queue where the image creation be executed. + void attach(image_mem_wrapper *mem, sycl::queue q = get_default_queue()) { + attach(mem, _channel, q); + } + + /// Detach bindless image data. + /// \param [in] q The queue where the image destruction be executed. + void detach(sycl::queue q = get_default_queue()) { + destroy_bindless_image(_img, q); + } + + /// Set image channel of bindless image. + /// \param [in] channel The image channel used to set. + void set_channel(image_channel channel) { _channel = channel; } + /// Get image channel of bindless image. + /// \returns The image channel of bindless image. + image_channel get_channel() { return _channel; } + + /// Set channel size of image channel. + /// \param [in] channel_num The channels number to set. + /// \param [in] channel_size The size for each channel in bits. + void set_channel_size(unsigned channel_num, unsigned channel_size) { + return _channel.set_channel_size(channel_num, channel_size); + } + /// Get channel size of image channel. + /// \returns The size for each channel in bits. + unsigned get_channel_size() { return _channel.get_channel_size(); } + + /// Set image channel data type of image channel. + /// \param [in] type The image channel data type to set. + void set_channel_data_type(image_channel_data_type type) { + _channel.set_channel_data_type(type); + } + /// Get image channel data type of image channel. + /// \returns The image channel data type of image channel. + image_channel_data_type get_channel_data_type() { + return _channel.get_channel_data_type(); + } + + /// Set channel num of bindless image. + /// \param [in] filtering_mode The channel num used to set. + void set_channel_num(unsigned num) { return _channel.set_channel_num(num); } + + /// Set channel type of bindless image. + /// \param [in] filtering_mode The channel type used to set. + void set_channel_type(sycl::image_channel_type type) { + return _channel.set_channel_type(type); + } + + /// Set addressing mode of bindless image. + /// \param [in] addressing_mode The addressing mode used to set. + void set(sycl::addressing_mode addressing_mode) { + _addressing_mode = addressing_mode; + } + /// Get addressing mode of bindless image. + /// \returns The addressing mode of bindless image. + sycl::addressing_mode get_addressing_mode() { return _addressing_mode; } + + /// Set coordinate normalization mode of bindless image. + /// \param [in] coordinate_normalization_mode The coordinate normalization + /// mode used to set. + void set(sycl::coordinate_normalization_mode coordinate_normalization_mode) { + _coordinate_normalization_mode = coordinate_normalization_mode; + } + /// Set coordinate normalization mode of bindless image. + /// \param [in] is_normalized Determine whether the coordinate should be + /// normalized. + void set_coordinate_normalization_mode(int is_normalized) { + _coordinate_normalization_mode = + is_normalized ? sycl::coordinate_normalization_mode::normalized + : sycl::coordinate_normalization_mode::unnormalized; + } + /// Get coordinate normalization mode of bindless image. + /// \returns The coordinate normalization mode of bindless image. + bool is_coordinate_normalized() { + return _coordinate_normalization_mode == + sycl::coordinate_normalization_mode::normalized; + } + + /// Set filtering mode of bindless image. + /// \param [in] filtering_mode The filtering mode used to set. + void set(sycl::filtering_mode filtering_mode) { + _filtering_mode = filtering_mode; + } + /// Get filtering mode of bindless image. + /// \returns The filtering mode of bindless image. + sycl::filtering_mode get_filtering_mode() { return _filtering_mode; } + + /// Get bindless image handle. + /// \returns The sampled image handle of created bindless image. + inline sycl::ext::oneapi::experimental::sampled_image_handle get_handle() { + return _img; + } + + /// Set mipmap sample filtering mode for bindless image handle + /// \param [in] filtering_mode The mipmap filtering mode + inline void set_mip_filtering_mode(sycl::filtering_mode mode) { + auto sampling_info = get_sampling_info(_img); + sampling_info.set_mipmap_filtering(mode); + } + + /// Get mipmap sample filtering mode for bindless image handle + /// \return The mipmap filtering mode + inline sycl::filtering_mode get_mip_filtering_mode(void) { + auto sampling_info = get_sampling_info(_img); + return sampling_info.get_mipmap_filtering(); + } + + /// Get mipmap sample filtering mode for bindless image handle + /// \return The mipmap filtering mode + inline void get_mip_level_clamp(float *min_level_clamp, + float *max_level_clamp) { + auto sampling_info = get_sampling_info(_img); + *min_level_clamp = sampling_info.get_min_mipmap_level_clamp(); + *max_level_clamp = sampling_info.get_max_mipmap_level_clamp(); + } + + /// Get mipmap memory wrapper attached the bindless image + /// \return The mipmap memory wrapper + inline image_mem_wrapper *get_attached_mipmap_data(void) { + auto mem = get_img_mem(_img); + + if (mem->get_image_type() != + sycl::ext::oneapi::experimental::image_type::mipmap) + throw std::runtime_error("Bindless image data is not of mipmap type"); + + return mem; + } + +private: + image_channel _channel; + sycl::addressing_mode _addressing_mode = sycl::addressing_mode::clamp_to_edge; + sycl::coordinate_normalization_mode _coordinate_normalization_mode = + sycl::coordinate_normalization_mode::unnormalized; + sycl::filtering_mode _filtering_mode = sycl::filtering_mode::nearest; + sycl::ext::oneapi::experimental::sampled_image_handle _img{0}; +}; + +/// The wrapper class of bindless image. +template +class bindless_image_wrapper : public bindless_image_wrapper_base { +public: + /// Create bindless image wrapper according to template argument \p T. + bindless_image_wrapper() { set_channel(image_channel::create()); } +}; + +/// Asynchronously copies from the image memory to memory specified by a +/// pointer, The return of the function does NOT guarantee the copy is +/// completed. +/// \param [in] dest The destination memory address. +/// \param [in] p The pitch of destination memory. +/// \param [in] src The source image memory. +/// \param [in] w_offset_src The x offset of source image memory. +/// \param [in] h_offset_src The y offset of source image memory. +/// \param [in] w The width of matrix to be copied. +/// \param [in] h The height of matrix to be copied. +/// \param [in] q The queue to execute the copy task. +static inline void async_dpct_memcpy(void *dest, size_t p, + const image_mem_wrapper *src, + size_t w_offset_src, size_t h_offset_src, + size_t w, size_t h, + sycl::queue q = get_default_queue()) { + detail::dpct_memcpy(src->get_handle(), src->get_desc(), w_offset_src, + h_offset_src, dest, p, w, h, q); +} + +/// Synchronously copies from the image memory to memory specified by a +/// pointer, The function will return after the copy is completed. +/// \param [in] dest The destination memory address. +/// \param [in] p The pitch of destination memory. +/// \param [in] src The source image memory. +/// \param [in] w_offset_src The x offset of source image memory. +/// \param [in] h_offset_src The y offset of source image memory. +/// \param [in] w The width of matrix to be copied. +/// \param [in] h The height of matrix to be copied. +/// \param [in] q The queue to execute the copy task. +static inline void dpct_memcpy(void *dest, size_t p, + const image_mem_wrapper *src, + size_t w_offset_src, size_t h_offset_src, + size_t w, size_t h, + sycl::queue q = get_default_queue()) { + detail::dpct_memcpy(src->get_handle(), src->get_desc(), w_offset_src, + h_offset_src, dest, p, w, h, q) + .wait(); +} + +/// Asynchronously copies from the image memory to memory specified by a +/// pointer, The return of the function does NOT guarantee the copy is +/// completed. +/// \param [in] dest The destination memory address. +/// \param [in] src The source image memory. +/// \param [in] w_offset_src The x offset of source image memory. +/// \param [in] h_offset_src The y offset of source image memory. +/// \param [in] s The size to be copied. +/// \param [in] q The queue to execute the copy task. +static inline void async_dpct_memcpy(void *dest, const image_mem_wrapper *src, + size_t w_offset_src, size_t h_offset_src, + size_t s, + sycl::queue q = get_default_queue()) { + detail::dpct_memcpy(src->get_handle(), src->get_desc(), w_offset_src, + h_offset_src, dest, s, q); +} + +/// Synchronously copies from the image memory to memory specified by a +/// pointer, The function will return after the copy is completed. +/// \param [in] dest The destination memory address. +/// \param [in] src The source image memory. +/// \param [in] w_offset_src The x offset of source image memory. +/// \param [in] h_offset_src The y offset of source image memory. +/// \param [in] s The size to be copied. +/// \param [in] q The queue to execute the copy task. +static inline void dpct_memcpy(void *dest, const image_mem_wrapper *src, + size_t w_offset_src, size_t h_offset_src, + size_t s, sycl::queue q = get_default_queue()) { + sycl::event::wait(detail::dpct_memcpy(src->get_handle(), src->get_desc(), + w_offset_src, h_offset_src, dest, s, + q)); +} + +/// Asynchronously copies from memory specified by a pointer to the image +/// memory, The return of the function does NOT guarantee the copy is completed. +/// \param [in] dest The destination image memory. +/// \param [in] w_offset_dest The x offset of destination image memory. +/// \param [in] h_offset_dest The y offset of destination image memory. +/// \param [in] src The source memory address. +/// \param [in] p The pitch of source memory. +/// \param [in] w The width of matrix to be copied. +/// \param [in] h The height of matrix to be copied. +/// \param [in] q The queue to execute the copy task. +static inline void async_dpct_memcpy(image_mem_wrapper *dest, + size_t w_offset_dest, size_t h_offset_dest, + const void *src, size_t p, size_t w, + size_t h, + sycl::queue q = get_default_queue()) { + detail::dpct_memcpy(src, dest->get_handle(), dest->get_desc(), w_offset_dest, + h_offset_dest, p, w, h, q); +} + +/// Synchronously copies from memory specified by a pointer to the image +/// memory, The function will return after the copy is completed. +/// \param [in] dest The destination image memory. +/// \param [in] w_offset_dest The x offset of destination image memory. +/// \param [in] h_offset_dest The y offset of destination image memory. +/// \param [in] src The source memory address. +/// \param [in] p The pitch of source memory. +/// \param [in] w The width of matrix to be copied. +/// \param [in] h The height of matrix to be copied. +/// \param [in] q The queue to execute the copy task. +static inline void dpct_memcpy(image_mem_wrapper *dest, size_t w_offset_dest, + size_t h_offset_dest, const void *src, size_t p, + size_t w, size_t h, + sycl::queue q = get_default_queue()) { + detail::dpct_memcpy(src, dest->get_handle(), dest->get_desc(), w_offset_dest, + h_offset_dest, p, w, h, q) + .wait(); +} + +/// Asynchronously copies from memory specified by a pointer to the image +/// memory, The return of the function does NOT guarantee the copy is completed. +/// \param [in] dest The destination image memory. +/// \param [in] w_offset_dest The x offset of destination image memory. +/// \param [in] h_offset_dest The y offset of destination image memory. +/// \param [in] src The source memory address. +/// \param [in] s The size to be copied. +/// \param [in] q The queue to execute the copy task. +static inline void async_dpct_memcpy(image_mem_wrapper *dest, + size_t w_offset_dest, size_t h_offset_dest, + const void *src, size_t s, + sycl::queue q = get_default_queue()) { + detail::dpct_memcpy(src, dest->get_handle(), dest->get_desc(), w_offset_dest, + h_offset_dest, s, q); +} + +/// Synchronously copies from memory specified by a pointer to the image +/// memory, The function will return after the copy is completed. +/// \param [in] dest The destination image memory. +/// \param [in] w_offset_dest The x offset of destination image memory. +/// \param [in] h_offset_dest The y offset of destination image memory. +/// \param [in] src The source memory address. +/// \param [in] s The size to be copied. +/// \param [in] q The queue to execute the copy task. +static inline void dpct_memcpy(image_mem_wrapper *dest, size_t w_offset_dest, + size_t h_offset_dest, const void *src, size_t s, + sycl::queue q = get_default_queue()) { + sycl::event::wait(detail::dpct_memcpy(src, dest->get_handle(), + dest->get_desc(), w_offset_dest, + h_offset_dest, s, q)); +} + +/// Synchronously copies from image memory to the image memory, The function +/// will return after the copy is completed. +/// \param [in] dest The destination image memory. +/// \param [in] w_offset_dest The x offset of destination image memory. +/// \param [in] h_offset_dest The y offset of destination image memory. +/// \param [in] src The source image memory. +/// \param [in] w_offset_src The x offset of source image memory. +/// \param [in] h_offset_src The y offset of source image memory. +/// \param [in] w The width of matrix to be copied. +/// \param [in] h The height of matrix to be copied. +/// \param [in] q The queue to execute the copy task. +static inline void dpct_memcpy(image_mem_wrapper *dest, size_t w_offset_dest, + size_t h_offset_dest, + const image_mem_wrapper *src, + size_t w_offset_src, size_t h_offset_src, + size_t w, size_t h, + sycl::queue q = get_default_queue()) { + auto temp = (void *)sycl::malloc_device(w * h, q); + // TODO: Need change logic when sycl support image_mem to image_mem copy. + dpct_memcpy(temp, w, src, w_offset_src, h_offset_src, w, h, q); + dpct_memcpy(dest, w_offset_dest, h_offset_dest, temp, w, w, h, q); + sycl::free(temp, q); +} + +/// Synchronously copies from image memory to the image memory, The function +/// will return after the copy is completed. +/// \param [in] dest The destination image memory. +/// \param [in] w_offset_dest The x offset of destination image memory. +/// \param [in] h_offset_dest The y offset of destination image memory. +/// \param [in] src The source image memory. +/// \param [in] w_offset_src The x offset of source image memory. +/// \param [in] h_offset_src The y offset of source image memory. +/// \param [in] s The size to be copied. +/// \param [in] q The queue to execute the copy task. +static inline void dpct_memcpy(image_mem_wrapper *dest, size_t w_offset_dest, + size_t h_offset_dest, + const image_mem_wrapper *src, + size_t w_offset_src, size_t h_offset_src, + size_t s, sycl::queue q = get_default_queue()) { + auto temp = (void *)sycl::malloc_device(s, q); + // TODO: Need change logic when sycl support image_mem to image_mem copy. + dpct_memcpy(temp, src, w_offset_src, h_offset_src, s, q); + dpct_memcpy(dest, w_offset_dest, h_offset_dest, temp, s, q); + sycl::free(temp, q); +} +// A wrapper for sycl fetch_image function for the byte addressing image. +template +DataT fetch_image_by_byte( + const sycl::ext::oneapi::experimental::unsampled_image_handle &imageHandle, + CoordT &&coords) { + if constexpr (std::is_scalar_v) { + return sycl::ext::oneapi::experimental::fetch_image( + imageHandle, coords / sizeof(DataT)); + } else { + coords[0] = coords[0] / sizeof(DataT); + return sycl::ext::oneapi::experimental::fetch_image( + imageHandle, coords); + } +} + +using image_mem_wrapper_ptr = image_mem_wrapper *; +#ifdef _WIN32 +using external_mem_wrapper_ptr = external_mem_wrapper *; +#else +using external_mem_wrapper_ptr = external_mem_wrapper_base *; +#endif // _WIN32 +using external_sem_wrapper_ptr = external_sem_wrapper *; +using bindless_image_wrapper_base_p = bindless_image_wrapper_base *; + +#endif + +} // namespace experimental +} // namespace dpct + +#endif // !__DPCT_BINDLESS_IMAGE_HPP__ diff --git a/sycl/include/dpct/blas_gemm_utils.hpp b/sycl/include/dpct/blas_gemm_utils.hpp new file mode 100644 index 0000000000..f41fe0737b --- /dev/null +++ b/sycl/include/dpct/blas_gemm_utils.hpp @@ -0,0 +1,896 @@ +//==---- blas_gemm_utils.hpp ----------------------*- C++ -*----------------==// +// +// Copyright (C) Intel Corporation +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// See https://llvm.org/LICENSE.txt for license information. +// +//===----------------------------------------------------------------------===// +// This file contains the implementation of GEneral Matrix Multiplication by +// using oneDNN. More datatype combinations and the epilogue can be supported by +// oneDNN, which is not available in blas_utils.hpp using oneMKL. +//===----------------------------------------------------------------------===// + +#ifndef __DPCT_BLAS_GEMM_UTILS_HPP__ +#define __DPCT_BLAS_GEMM_UTILS_HPP__ + +#include "compat_service.hpp" +#include "blas_utils.hpp" +#include "dnnl_utils.hpp" + +namespace dpct::blas_gemm::experimental { +enum class order_t : std::uint8_t { + col, + row, + col32, + col4_4r2_8c, + col32_2r_4r4 +}; +} // namespace dpct::blas_gemm::experimental + +#include "detail/blas_gemm_utils_detail.hpp" + +namespace dpct::blas_gemm::experimental { +enum class pointer_mode_t { + host, + device, + device_vector, + alpha_device_vector_beta_zero, + alpha_device_vector_beta_host +}; +enum class epilogue_t { + nop = 1, + relu, + bias, + gelu, + gelu_bias, + gelu_aux, + gelu_aux_bias, + dgelu, + bgradb +}; + +class descriptor; +using descriptor_ptr = descriptor *; +class matrix_layout_t; +using matrix_layout_ptr = matrix_layout_t *; +class matmul_desc_t; +using matmul_desc_ptr = matmul_desc_t *; +class transform_desc_t; +using transform_desc_ptr = transform_desc_t *; + +class descriptor { +public: + descriptor() {} + void init(sycl::queue *q_ptr) { + _engine = ::dnnl::sycl_interop::make_engine(q_ptr->get_device(), + q_ptr->get_context()); + _engine_stream = ::dnnl::sycl_interop::make_stream(_engine, *q_ptr); + } + ::dnnl::engine get_engine() const noexcept { return _engine; } + ::dnnl::stream get_engine_stream() const noexcept { return _engine_stream; }; + +private: + ::dnnl::engine _engine; + ::dnnl::stream _engine_stream; +}; + +class matrix_layout_t { +public: + enum class attribute { + type, + order, + rows, + cols, + ld, + batch_count, + strided_batch_offset + }; + + matrix_layout_t(library_data_t type, std::uint64_t rows, std::uint64_t cols, + std::int64_t ld) + : _type(type), _rows(rows), _cols(cols), _ld(ld) {} + + void set_attribute(attribute attr, const void *mem) { + get_set_attr(attr, const_cast(mem)); + } + void get_attribute(attribute attr, void *mem) { + get_set_attr(attr, mem); + } + +private: + template void get_set_attr(attribute attr, void *mem) { +#define CASE(tag) \ + case attribute::tag: \ + if constexpr (is_set) { \ + _##tag = *static_cast(mem); \ + } else { \ + *static_cast(mem) = _##tag; \ + } \ + break; + switch (attr) { + CASE(type) + CASE(order) + CASE(rows) + CASE(cols) + CASE(ld) + CASE(batch_count) + CASE(strided_batch_offset) + } +#undef CASE + } + + library_data_t _type; + order_t _order = order_t::col; + std::uint64_t _rows; + std::uint64_t _cols; + std::int64_t _ld; + std::uint64_t _batch_count = 1; + std::uint64_t _strided_batch_offset = 0; + + friend sycl::event matmul(descriptor_ptr handle, matmul_desc_ptr computeDesc, + const void *alpha, const void *a, + matrix_layout_ptr a_desc, const void *b, + matrix_layout_ptr b_desc, const void *beta, + const void *c, matrix_layout_ptr c_desc, void *d, + matrix_layout_ptr d_desc, + ::dpct::cs::queue_ptr q_ptr); + friend sycl::event + matrix_transform(transform_desc_ptr transform_desc, const void *alpha, + const void *a, matrix_layout_ptr a_desc, const void *beta, + const void *b, matrix_layout_ptr b_desc, void *c, + matrix_layout_ptr c_desc, ::dpct::cs::queue_ptr q_ptr); +}; + +class matmul_desc_t { +public: + enum class attribute { + compute_type, + scale_type, + bias_data_type, + bias_pointer, + pointer_mode, + trans_a, + trans_b, + trans_c, + epilogue, + epilogue_aux_ld, + epilogue_aux_pointer, + epilogue_aux_data_type, + a_scale_pointer, + b_scale_pointer, + d_scale_pointer, + absmax_d_pointer, + unsupport + }; + + matmul_desc_t(compute_type compute_type, library_data_t scale_type) + : _compute_type(compute_type), _scale_type(scale_type) {} + + void set_attribute(attribute attr, const void *mem) { + if (attr != attribute::unsupport) + get_set_attr(attr, const_cast(mem)); + } + void get_attribute(attribute attr, void *mem) { + if (attr != attribute::unsupport) + get_set_attr(attr, mem); + } + +private: + template void get_set_attr(attribute attr, void *mem) { +#define CASE(tag) \ + case attribute::tag: \ + if constexpr (is_set) { \ + _##tag = *static_cast(mem); \ + } else { \ + *static_cast(mem) = _##tag; \ + } \ + break; + switch (attr) { + CASE(compute_type) + CASE(scale_type) + CASE(bias_data_type) + CASE(bias_pointer) + CASE(pointer_mode) + CASE(trans_a) + CASE(trans_b) + CASE(trans_c) + CASE(epilogue) + CASE(a_scale_pointer) + CASE(b_scale_pointer) + CASE(d_scale_pointer) + CASE(absmax_d_pointer) + CASE(epilogue_aux_ld) + CASE(epilogue_aux_pointer) + CASE(epilogue_aux_data_type) + default: + break; + } +#undef CASE + } + + compute_type _compute_type; + library_data_t _scale_type; + library_data_t _bias_data_type = library_data_t::real_float; + pointer_mode_t _pointer_mode = pointer_mode_t::host; + oneapi::mkl::transpose _trans_a = oneapi::mkl::transpose::nontrans; + oneapi::mkl::transpose _trans_b = oneapi::mkl::transpose::nontrans; + oneapi::mkl::transpose _trans_c = oneapi::mkl::transpose::nontrans; + epilogue_t _epilogue = epilogue_t::nop; + library_data_t _epilogue_aux_data_type = library_data_t::real_float; + size_t _epilogue_aux_ld = 0; + void *_a_scale_pointer = nullptr; + void *_b_scale_pointer = nullptr; + void *_d_scale_pointer = nullptr; + void *_absmax_d_pointer = nullptr; + void *_bias_pointer = nullptr; + void *_epilogue_aux_pointer = nullptr; + + friend sycl::event matmul(descriptor_ptr handle, matmul_desc_ptr computeDesc, + const void *alpha, const void *a, + matrix_layout_ptr a_desc, const void *b, + matrix_layout_ptr b_desc, const void *beta, + const void *c, matrix_layout_ptr c_desc, void *d, + matrix_layout_ptr d_desc, + ::dpct::cs::queue_ptr q_ptr); +}; + +/// This function does the following operations: +/// (1) D_temp = epilogue(alpha * scale_a * op_a(A) * scale_b * op_b(B) + beta * C) +/// (2) Amax = absmax(D_temp) when matmul_desc_t::attribute::absmax_d_pointer is specified +/// (3) D = scale_d * D_temp +/// "op_a" is specified by the matmul_desc_t::attribute::trans_a +/// (default is nontrans) +/// "op_b" is specified by the matmul_desc_t::attribute::trans_b +/// (default is nontrans) +/// "scale_a" is specified by the matmul_desc_t::attribute::a_scale_pointer +/// (default is nullptr, which behaves as 1.0) +/// "scale_b" is specified by the matmul_desc_t::attribute::b_scale_pointer +/// (default is nullptr, which behaves as 1.0) +/// "scale_d" is specified by the matmul_desc_t::attribute::d_scale_pointer +/// (default is nullptr, which behaves as 1.0) +/// "alpha" can be a scalar value or a vector, which is specified by the +/// matmul_desc_tattribute::::pointer_mode +/// "epilogue" is specified by the matmul_desc_t::attribute::epilogue +/// Currently, this function only supports the following type combinations: +/// scale_type==int32 && a_type==int8 && b_type==int8 && c_type==int32; +/// scale_type==float && a_type==int8 && b_type==int8 && c_type==int8; +/// scale_type==float && a_type==int8 && b_type==int8 && c_type==int32; +/// scale_type==float && a_type==float && b_type==float && c_type==float. +/// Currently, this function only supports beta==0 or beta==1. +/// Currently, this function only supports the relu, bias, gelu, gelu_bias, +/// gelu_aux, gelu_aux_bias, dgelu and bgradb epilogue. +/// NOTE: Non-col-major matrix will be converted to col-major matrix before. +/// TODO: Impl row-major matmul without layout conversion. +/// multiplication and converted back after multiplication. +/// \param [in] handle A handle containing context info. +/// \param [in] compute_desc Describe the computation. +/// \param [in] alpha Scaling factor alpha. +/// \param [in] a Input matrix A. +/// \param [in] a_desc Describe the matrix A. +/// \param [in] b Input matrix B. +/// \param [in] b_desc Describe the matrix B. +/// \param [in] beta Scaling factor beta. +/// \param [in] c Input matrix C. +/// \param [in] c_desc Describe the matrix C. +/// \param [out] d Output matrix D. +/// \param [in] d_desc Describe the matrix D. +/// \param [in] q_ptr The queue where the routine should be executed. +inline sycl::event matmul(descriptor_ptr handle, matmul_desc_ptr compute_desc, + const void *alpha, const void *a, + matrix_layout_ptr a_desc, const void *b, + matrix_layout_ptr b_desc, const void *beta, + const void *c, matrix_layout_ptr c_desc, void *d, + matrix_layout_ptr d_desc, + ::dpct::cs::queue_ptr q_ptr) { + const size_t m = compute_desc->_trans_a == oneapi::mkl::transpose::nontrans + ? a_desc->_rows + : a_desc->_cols; + const size_t n = d_desc->_cols; + const size_t k = compute_desc->_trans_b == oneapi::mkl::transpose::nontrans + ? b_desc->_rows + : b_desc->_cols; + const library_data_t a_type = a_desc->_type; + const library_data_t b_type = b_desc->_type; + const library_data_t c_type = c_desc->_type; + const library_data_t d_type = d_desc->_type; + const library_data_t scale_type = compute_desc->_scale_type; + + if (!q_ptr) + q_ptr = &::dpct::cs::get_default_queue(); + handle->init(q_ptr); + bool vector_alpha = false; + bool device_alpha = false; + if (compute_desc->_pointer_mode == pointer_mode_t::device_vector || + compute_desc->_pointer_mode == + pointer_mode_t::alpha_device_vector_beta_zero || + compute_desc->_pointer_mode == + pointer_mode_t::alpha_device_vector_beta_host) { + vector_alpha = true; + device_alpha = true; + } else if (compute_desc->_pointer_mode == pointer_mode_t::device) { + device_alpha = true; + } + + bool beta_is_zero = true; + if (beta != nullptr) { + int beta_value = detail::type_dispatch( + compute_desc->_scale_type, beta, q_ptr); + if (beta_value != 0) { + beta_is_zero = false; + if (beta_value != 1) + throw std::runtime_error( + "dpct::blas_gemm::experimental::matmul() does " + "not support non-zero and non-one beta currently."); + } + } + + if (compute_desc->_epilogue != epilogue_t::nop && + compute_desc->_epilogue != epilogue_t::relu && + compute_desc->_epilogue != epilogue_t::bias && + compute_desc->_epilogue != epilogue_t::gelu && + compute_desc->_epilogue != epilogue_t::gelu_bias && + compute_desc->_epilogue != epilogue_t::gelu_aux && + compute_desc->_epilogue != epilogue_t::gelu_aux_bias && + compute_desc->_epilogue != epilogue_t::dgelu && + compute_desc->_epilogue != epilogue_t::bgradb) { + throw std::runtime_error( + "dpct::blas_gemm::experimental::matmul() only " + "supports relu, bias, gelu, gelu_bias, gelu_aux, " + "gelu_aux_bias, dgelu and bgradb epilogue currently."); + } + + if (!(compute_desc->_scale_type == library_data_t::real_int32 && + a_desc->_type == library_data_t::real_int8 && + b_desc->_type == library_data_t::real_int8 && + c_desc->_type == library_data_t::real_int32) && + !(compute_desc->_scale_type == library_data_t::real_float && + a_desc->_type == library_data_t::real_int8 && + b_desc->_type == library_data_t::real_int8 && + c_desc->_type == library_data_t::real_int8) && + !(compute_desc->_scale_type == library_data_t::real_float && + a_desc->_type == library_data_t::real_int8 && + b_desc->_type == library_data_t::real_int8 && + c_desc->_type == library_data_t::real_int32) && + !(compute_desc->_scale_type == library_data_t::real_float && + a_desc->_type == library_data_t::real_float && + b_desc->_type == library_data_t::real_float && + c_desc->_type == library_data_t::real_float)) { + throw std::runtime_error( + "dpct::blas_gemm::experimental::matmul() only supports data type " + "combinataions:\n scale_type==int32 && a_type==int8 && b_type==int8" + "&& c_type==int32,\n scale_type==float && a_type==int8 && " + "b_type==int8 && c_type==int8,\n scale_type==float && a_type==int8" + "&& b_type==int8 && c_type==int32 or\n scale_type==float && " + "a_type==float" + "&& b_type==float && c_type==float."); + } + + if (a_desc->_batch_count != b_desc->_batch_count || + a_desc->_batch_count != c_desc->_batch_count || + a_desc->_batch_count != d_desc->_batch_count) { + throw std::runtime_error("dpct::blas_gemm::experimental::matmul() does not " + "support different batch count."); + } + +#ifdef DPCT_USM_LEVEL_NONE + if (a_desc->_batch_count != 1) { + throw std::runtime_error( + "dpct::blas_gemm::experimental::matmul() only supports " + "batch mode with USM enabled."); + } +#endif + + size_t a_elm_size = dpct::detail::library_data_size[static_cast( + a_desc->_type)] / + 8; + size_t b_elm_size = dpct::detail::library_data_size[static_cast( + b_desc->_type)] / + 8; + size_t c_elm_size = dpct::detail::library_data_size[static_cast( + c_desc->_type)] / + 8; + size_t d_elm_size = dpct::detail::library_data_size[static_cast( + d_desc->_type)] / + 8; + + static const auto matmul_single = + [](descriptor_ptr handle, matmul_desc_ptr compute_desc, const void *alpha, + const void *a, matrix_layout_ptr a_desc, const void *b, + matrix_layout_ptr b_desc, const void *beta, const void *c, + matrix_layout_ptr c_desc, void *d, matrix_layout_ptr d_desc, + ::dpct::cs::queue_ptr q_ptr, const size_t m, const size_t n, + const size_t k, const library_data_t a_type, + const library_data_t b_type, const library_data_t c_type, + const library_data_t d_type, const library_data_t scale_type, + bool vector_alpha, bool device_alpha, bool beta_is_zero, + size_t a_elm_size, size_t b_elm_size, size_t c_elm_size, + size_t d_elm_size) -> sycl::event { + // For non-col_major matrix, convert it to col_major. + const void *new_a = a; + const void *new_b = b; + const void *new_c = c; + void *new_d = d; + bool new_b_allocated = false; + bool new_c_allocated = false; + bool new_d_allocated = false; + size_t new_lda = a_desc->_ld, new_ldb = b_desc->_ld, new_ldc = c_desc->_ld, + new_ldd = d_desc->_ld; + std::vector transform_events; + + if (a_desc->_order != order_t::col) + new_lda = a_desc->_rows; + new_a = ::dpct::cs::malloc(a_elm_size * a_desc->_cols * new_lda, *q_ptr); + sycl::event e_init; + if (a_desc->_order != order_t::col) + e_init = detail::type_dispatch( + a_desc->_type, q_ptr, a_desc->_rows, a_desc->_cols, a_desc->_ld, + a_desc->_order, (const std::int8_t *)a, new_lda, order_t::col, + (std::int8_t *)new_a, std::vector{}); + else + e_init = ::dpct::cs::memcpy( + *q_ptr, (void *)new_a, a, a_elm_size * a_desc->_cols * new_lda, + ::dpct::cs::memcpy_direction::device_to_device); + + // alpha = alpha * scale_a * scale_b + sycl::event e_scale_new_a = detail::scale_new_a( + q_ptr, m, k, (void *)new_a, a_type, alpha, scale_type, vector_alpha, + device_alpha, compute_desc->_a_scale_pointer, + compute_desc->_b_scale_pointer, {e_init}); + + transform_events.push_back(e_scale_new_a); + + if (b_desc->_order != order_t::col) { + new_ldb = b_desc->_rows; + new_b = ::dpct::cs::malloc(b_elm_size * b_desc->_cols * new_ldb, *q_ptr); + new_b_allocated = true; + sycl::event e = detail::type_dispatch( + b_desc->_type, q_ptr, b_desc->_rows, b_desc->_cols, b_desc->_ld, + b_desc->_order, b, new_ldb, order_t::col, const_cast(new_b), + std::vector{}); + transform_events.push_back(e); + } + + if (!beta_is_zero && c_desc->_order != order_t::col) { + new_ldc = c_desc->_rows; + new_c = ::dpct::cs::malloc(c_elm_size * c_desc->_cols * new_ldc, *q_ptr); + new_c_allocated = true; + sycl::event e = detail::type_dispatch( + c_desc->_type, q_ptr, c_desc->_rows, c_desc->_cols, c_desc->_ld, + c_desc->_order, c, new_ldc, order_t::col, const_cast(new_c), + std::vector{}); + transform_events.push_back(e); + } + + if (d_desc->_order != order_t::col) { + new_ldd = d_desc->_rows; + new_d = ::dpct::cs::malloc(d_elm_size * d_desc->_cols * new_ldd, *q_ptr); + new_d_allocated = true; + } + + // start to call oneDNN matmul primitive + // a,d are col_major, b is row_major + const ::dnnl::memory::dim M = m; + const ::dnnl::memory::dim N = n; + const ::dnnl::memory::dim K = k; + + ::dnnl::memory::dims src_dims = {M, K}; + ::dnnl::memory::dims weights_dims = {K, N}; + ::dnnl::memory::dims bias_dims = {M, N}; + ::dnnl::memory::dims dst_dims = {M, N}; + + const ::dnnl::memory::dims src_strides = + compute_desc->_trans_a == oneapi::mkl::transpose::nontrans + ? ::dnnl::memory::dims{1, static_cast(new_lda)} + : ::dnnl::memory::dims{static_cast(new_lda), 1}; + const ::dnnl::memory::dims weights_strides = + compute_desc->_trans_b == oneapi::mkl::transpose::nontrans + ? ::dnnl::memory::dims{1, static_cast(new_ldb)} + : ::dnnl::memory::dims{static_cast(new_ldb), 1}; + const ::dnnl::memory::dims bias_strides = + ::dnnl::memory::dims{1, static_cast(new_ldc)}; + const ::dnnl::memory::dims dst_strides = + ::dnnl::memory::dims{1, static_cast(new_ldd)}; + + auto src_md = ::dnnl::memory::desc( + src_dims, dpct::dnnl::memory_desc_ext::to_dnnl_data_type(a_type), + src_strides); + auto weights_md = ::dnnl::memory::desc( + weights_dims, dpct::dnnl::memory_desc_ext::to_dnnl_data_type(b_type), + weights_strides); + auto bias_md = ::dnnl::memory::desc( + bias_dims, dpct::dnnl::memory_desc_ext::to_dnnl_data_type(c_type), + bias_strides); + auto dst_md = ::dnnl::memory::desc( + dst_dims, dpct::dnnl::memory_desc_ext::to_dnnl_data_type(d_type), + dst_strides); + + auto *src_mem = + new ::dnnl::memory(src_md, handle->get_engine(), DNNL_MEMORY_NONE); + auto *weights_mem = + new ::dnnl::memory(weights_md, handle->get_engine(), DNNL_MEMORY_NONE); + auto *bias_mem = + new ::dnnl::memory(bias_md, handle->get_engine(), DNNL_MEMORY_NONE); + auto *dst_mem = + new ::dnnl::memory(dst_md, handle->get_engine(), DNNL_MEMORY_NONE); + +#ifdef DPCT_USM_LEVEL_NONE + detail::type_dispatch(a_type, src_mem, new_a); + detail::type_dispatch(b_type, weights_mem, new_b); + if (!beta_is_zero) + detail::type_dispatch(c_type, bias_mem, new_c); + detail::type_dispatch(d_type, dst_mem, new_d); +#else + src_mem->set_data_handle(const_cast(new_a)); + weights_mem->set_data_handle(const_cast(new_b)); + if (!beta_is_zero) + bias_mem->set_data_handle(const_cast(new_c)); + dst_mem->set_data_handle(new_d); +#endif + + std::unordered_map matmul_args; + matmul_args.insert({DNNL_ARG_SRC, *src_mem}); + matmul_args.insert({DNNL_ARG_WEIGHTS, *weights_mem}); + matmul_args.insert({DNNL_ARG_DST, *dst_mem}); + ::dnnl::primitive_attr matmul_attr; + + ::dnnl::post_ops matmul_ops; + if (!beta_is_zero) { + matmul_ops.append_binary(::dnnl::algorithm::binary_add, bias_md); + matmul_args.insert({DNNL_ARG_ATTR_MULTIPLE_POST_OP(matmul_ops.len() - 1) | + DNNL_ARG_SRC_1, + *bias_mem}); + } + + ::dnnl::memory *po_bias_mem = nullptr; + auto po_bias_md = + ::dnnl::memory::desc(::dnnl::memory::dims{M, 1}, + dpct::dnnl::memory_desc_ext::to_dnnl_data_type( + compute_desc->_bias_data_type), + ::dnnl::memory::dims{1, M}); + if (compute_desc->_epilogue == epilogue_t::bias || + compute_desc->_epilogue == epilogue_t::gelu_bias || + compute_desc->_epilogue == epilogue_t::gelu_aux_bias) { + po_bias_mem = new ::dnnl::memory(po_bias_md, handle->get_engine(), + DNNL_MEMORY_NONE); +#ifdef DPCT_USM_LEVEL_NONE + detail::type_dispatch( + compute_desc->_bias_data_type, po_bias_mem, + compute_desc->_bias_pointer); +#else + po_bias_mem->set_data_handle(compute_desc->_bias_pointer); +#endif + } + + ::dnnl::memory *po_bias_bgradb_mem = nullptr; + auto po_bias_bgradb_md = ::dnnl::memory::desc( + compute_desc->_trans_b == oneapi::mkl::transpose::nontrans + ? ::dnnl::memory::dims{N, 1} + : ::dnnl::memory::dims{1, N}, + dpct::dnnl::memory_desc_ext::to_dnnl_data_type( + compute_desc->_bias_data_type), + compute_desc->_trans_b == oneapi::mkl::transpose::nontrans + ? ::dnnl::memory::dims{1, N} + : ::dnnl::memory::dims{N, 1}); + if (compute_desc->_epilogue == epilogue_t::bgradb) { + po_bias_bgradb_mem = new ::dnnl::memory( + po_bias_bgradb_md, handle->get_engine(), DNNL_MEMORY_NONE); +#ifdef DPCT_USM_LEVEL_NONE + detail::type_dispatch( + compute_desc->_bias_data_type, po_bias_bgradb_mem, + compute_desc->_bias_pointer); +#else + po_bias_bgradb_mem->set_data_handle(compute_desc->_bias_pointer); +#endif + } + + ::dnnl::memory *po_aux_mem = nullptr; + auto po_aux_md = ::dnnl::memory::desc( + ::dnnl::memory::dims{M, N}, + dpct::dnnl::memory_desc_ext::to_dnnl_data_type( + compute_desc->_epilogue_aux_data_type), + ::dnnl::memory::dims{ + 1, static_cast(compute_desc->_epilogue_aux_ld)}); + if (compute_desc->_epilogue == epilogue_t::dgelu) { + po_aux_mem = + new ::dnnl::memory(po_aux_md, handle->get_engine(), DNNL_MEMORY_NONE); +#ifdef DPCT_USM_LEVEL_NONE + detail::type_dispatch( + compute_desc->_epilogue_aux_data_type, po_aux_mem, + compute_desc->_epilogue_aux_pointer); +#else + po_aux_mem->set_data_handle(compute_desc->_epilogue_aux_pointer); +#endif + } + + switch (compute_desc->_epilogue) { + case epilogue_t::relu: + matmul_ops.append_eltwise(::dnnl::algorithm::eltwise_relu, 0.f, 0.f); + break; + case epilogue_t::bias: + case epilogue_t::gelu_bias: { + matmul_ops.append_binary(::dnnl::algorithm::binary_add, po_bias_md); + matmul_args.insert({DNNL_ARG_ATTR_MULTIPLE_POST_OP(matmul_ops.len() - 1) | + DNNL_ARG_SRC_1, + *po_bias_mem}); + if (compute_desc->_epilogue == epilogue_t::gelu_bias) + matmul_ops.append_eltwise(::dnnl::algorithm::eltwise_gelu_tanh, 0.f, + 0.f); + break; + } + case epilogue_t::gelu: + matmul_ops.append_eltwise(::dnnl::algorithm::eltwise_gelu_tanh, 0.f, 0.f); + break; + default: + break; + } + + matmul_attr.set_post_ops(matmul_ops); + + auto matmul_pd = ::dnnl::matmul::primitive_desc( + handle->get_engine(), src_md, weights_md, dst_md, matmul_attr); + auto matmul_prim = ::dnnl::matmul(matmul_pd); + sycl::event matmul_prim_event = + ::dnnl::sycl_interop::execute(matmul_prim, handle->get_engine_stream(), + matmul_args, transform_events); + + // post-op implemented by separate primitives + sycl::event post_op_prim_event; + if (compute_desc->_epilogue == epilogue_t::gelu_aux || + compute_desc->_epilogue == epilogue_t::gelu_aux_bias) { + sycl::event prev_event = matmul_prim_event; + if (compute_desc->_epilogue == epilogue_t::gelu_aux_bias) { + auto po_bias_pd = ::dnnl::binary::primitive_desc( + handle->get_engine(), ::dnnl::algorithm::binary_add, dst_md, + po_bias_md, dst_md); + auto po_bias_prim = ::dnnl::binary(po_bias_pd); + std::unordered_map po_bias_args; + po_bias_args.insert({DNNL_ARG_SRC_0, *dst_mem}); + po_bias_args.insert({DNNL_ARG_SRC_1, *po_bias_mem}); + po_bias_args.insert({DNNL_ARG_DST, *dst_mem}); + sycl::event prev_event = ::dnnl::sycl_interop::execute( + po_bias_prim, handle->get_engine_stream(), po_bias_args, + {matmul_prim_event}); + } + size_t size_of_element = + dpct::detail::library_data_size[static_cast( + d_desc->_type)] / + 8; + sycl::event copy_e = dpct::blas::matrix_mem_copy_async( + compute_desc->_epilogue_aux_pointer, new_d, + compute_desc->_epilogue_aux_ld, d_desc->_ld, m, n, size_of_element, + ::dpct::cs::memcpy_direction::automatic, *q_ptr, {prev_event}); + + auto gelu_pd = ::dnnl::eltwise_forward::primitive_desc( + handle->get_engine(), ::dnnl::prop_kind::forward_training, + ::dnnl::algorithm::eltwise_gelu_tanh, dst_md, dst_md); + auto gelu_prim = ::dnnl::eltwise_forward(gelu_pd); + std::unordered_map gelu_args; + gelu_args.insert({DNNL_ARG_SRC, *dst_mem}); + gelu_args.insert({DNNL_ARG_DST, *dst_mem}); + post_op_prim_event = ::dnnl::sycl_interop::execute( + gelu_prim, handle->get_engine_stream(), gelu_args, {copy_e}); + } else if (compute_desc->_epilogue == epilogue_t::dgelu) { + auto gelu_pd = ::dnnl::eltwise_forward::primitive_desc( + handle->get_engine(), ::dnnl::prop_kind::forward_training, + ::dnnl::algorithm::eltwise_gelu_tanh, po_aux_md, po_aux_md); + auto dgelu_pd = ::dnnl::eltwise_backward::primitive_desc( + handle->get_engine(), ::dnnl::algorithm::eltwise_gelu_tanh, dst_md, + dst_md, po_aux_md, gelu_pd); + auto dgelu_prim = ::dnnl::eltwise_backward(dgelu_pd); + std::unordered_map dgelu_args; + dgelu_args.insert({DNNL_ARG_SRC, *po_aux_mem}); + dgelu_args.insert({DNNL_ARG_DIFF_DST, *dst_mem}); + dgelu_args.insert({DNNL_ARG_DIFF_SRC, *dst_mem}); + post_op_prim_event = + ::dnnl::sycl_interop::execute(dgelu_prim, handle->get_engine_stream(), + dgelu_args, {matmul_prim_event}); + } else if (compute_desc->_epilogue == epilogue_t::bgradb) { + auto reduction_pd = ::dnnl::reduction::primitive_desc( + handle->get_engine(), ::dnnl::algorithm::reduction_sum, weights_md, + po_bias_bgradb_md, 0.f, 0.f); + auto reduction_prim = ::dnnl::reduction(reduction_pd); + std::unordered_map reduction_args; + reduction_args.insert({DNNL_ARG_SRC, *weights_mem}); + reduction_args.insert({DNNL_ARG_DST, *po_bias_bgradb_mem}); + post_op_prim_event = ::dnnl::sycl_interop::execute( + reduction_prim, handle->get_engine_stream(), reduction_args, + {matmul_prim_event}); + } + +#ifndef DPCT_USM_LEVEL_NONE + // WA to avoid incorrect result on CPU with USM + if (q_ptr->get_device().is_cpu()) { + matmul_prim_event.wait(); + post_op_prim_event.wait(); + } +#endif + // end of calling oneDNN + + sycl::event absmax_d_event; + if (auto absmax_ptr = compute_desc->_absmax_d_pointer) { + absmax_d_event = detail::type_dispatch( + d_desc->_type, absmax_ptr, new_d, new_ldd, d_desc->_rows, + d_desc->_cols, q_ptr, + std::vector{matmul_prim_event, post_op_prim_event}); + } + + sycl::event scale_d_event; + if (auto d_scale_ptr = compute_desc->_d_scale_pointer) { + scale_d_event = detail::type_dispatch( + d_desc->_type, d_scale_ptr, new_d, new_ldd, d_desc->_rows, + d_desc->_cols, q_ptr, compute_desc->_scale_type, + std::vector{matmul_prim_event, absmax_d_event, + post_op_prim_event}); + } + + sycl::event transform_d_event; + if (d_desc->_order != order_t::col) { + transform_d_event = detail::type_dispatch( + d_desc->_type, q_ptr, d_desc->_rows, d_desc->_cols, new_ldd, + order_t::col, new_d, d_desc->_ld, d_desc->_order, d, + std::vector{matmul_prim_event, absmax_d_event, + post_op_prim_event}); + } + + sycl::event free_event = q_ptr->submit([&](sycl::handler &cgh) { + cgh.depends_on({transform_d_event, matmul_prim_event, absmax_d_event, + post_op_prim_event}); + cgh.host_task([=] { + delete src_mem; + delete weights_mem; + delete bias_mem; + delete dst_mem; + if (po_bias_mem) + delete po_bias_mem; + if (po_bias_bgradb_mem) + delete po_bias_bgradb_mem; + if (po_aux_mem) + delete po_aux_mem; + ::dpct::cs::free((void *)new_a, *q_ptr); + if (new_b_allocated) + ::dpct::cs::free((void *)new_b, *q_ptr); + if (new_c_allocated) + ::dpct::cs::free((void *)new_c, *q_ptr); + if (new_d_allocated) + ::dpct::cs::free((void *)new_d, *q_ptr); + }); + }); + return free_event; + }; + + std::vector events; + const std::byte *offsetted_a = static_cast(a); + const std::byte *offsetted_b = static_cast(b); + const std::byte *offsetted_c = static_cast(c); + std::byte *offsetted_d = static_cast(d); + for (std::uint64_t i = 0; i < a_desc->_batch_count; i++) { + sycl::event e = matmul_single( + handle, compute_desc, alpha, offsetted_a, a_desc, offsetted_b, b_desc, + beta, offsetted_c, c_desc, offsetted_d, d_desc, q_ptr, m, n, k, a_type, + b_type, c_type, d_type, scale_type, vector_alpha, device_alpha, + beta_is_zero, a_elm_size, b_elm_size, c_elm_size, d_elm_size); + events.push_back(e); + + offsetted_a += a_elm_size * a_desc->_strided_batch_offset; + offsetted_b += b_elm_size * b_desc->_strided_batch_offset; + offsetted_c += c_elm_size * c_desc->_strided_batch_offset; + offsetted_d += d_elm_size * d_desc->_strided_batch_offset; + } + + return q_ptr->single_task(events, [] {}); +} + +class transform_desc_t { +public: + enum class attribute { scale_type, pointer_mode, trans_a, trans_b }; + + transform_desc_t(library_data_t scale_type) : _scale_type(scale_type) {} + void set_attribute(attribute attr, const void *mem) { + get_set_attr(attr, const_cast(mem)); + } + void get_attribute(attribute attr, void *mem) { + get_set_attr(attr, mem); + } + +private: + template void get_set_attr(attribute attr, void *mem) { +#define CASE(tag) \ + case attribute::tag: \ + if constexpr (is_set) { \ + _##tag = *static_cast(mem); \ + } else { \ + *static_cast(mem) = _##tag; \ + } \ + break; + switch (attr) { + CASE(scale_type) + CASE(pointer_mode) + CASE(trans_a) + CASE(trans_b) + } +#undef CASE + } + + library_data_t _scale_type; + pointer_mode_t _pointer_mode = pointer_mode_t::host; + oneapi::mkl::transpose _trans_a = oneapi::mkl::transpose::nontrans; + oneapi::mkl::transpose _trans_b = oneapi::mkl::transpose::nontrans; + + friend sycl::event + matrix_transform(transform_desc_ptr transform_desc, const void *alpha, + const void *a, matrix_layout_ptr a_desc, const void *beta, + const void *b, matrix_layout_ptr b_desc, void *c, + matrix_layout_ptr c_desc, ::dpct::cs::queue_ptr q_ptr); +}; + +/// This function does operation: +/// C = alpha*transformation(A) + beta*transformation(B). +/// The "transformation" includes matrix transpose and layout conversion. +/// Currently it only supports alpha==1 && beta==0. +/// \param [in] transform_desc Describe the transformation. +/// \param [in] alpha Scaling factor alpha. +/// \param [in] a Input matrix A. +/// \param [in] a_desc Describe the matrix A. +/// \param [in] beta Scaling factor beta. +/// \param [in] b Input matrix B. +/// \param [in] b_desc Describe the matrix B. +/// \param [out] c Output matrix C. +/// \param [in] c_desc Describe the matrix C. +/// \param [in] q_ptr The queue where the routine should be executed. +inline sycl::event matrix_transform(transform_desc_ptr transform_desc, + const void *alpha, const void *a, + matrix_layout_ptr a_desc, const void *beta, + const void *b, matrix_layout_ptr b_desc, + void *c, matrix_layout_ptr c_desc, + ::dpct::cs::queue_ptr q_ptr) { + if (!q_ptr) + q_ptr = &::dpct::cs::get_default_queue(); + + if (transform_desc->_pointer_mode != pointer_mode_t::host) { + throw std::runtime_error( + "dpct::blas_gemm::experimental::matrix_transform() " + "only supports pointer_mode_t::host as pointer_mode currently."); + } + if (transform_desc->_scale_type != library_data_t::real_float) { + throw std::runtime_error( + "dpct::blas_gemm::experimental::matrix_transform() " + "only supports library_data_t::real_float as scale_type currently."); + } + + if (alpha != nullptr) { + if (1.0f != *reinterpret_cast(alpha)) + throw std::runtime_error( + "dpct::blas_gemm::experimental::matrix_transform() does not " + "support non-one alpha currently."); + } + + if (beta != nullptr) { + if (0.0f != *reinterpret_cast(beta)) + throw std::runtime_error( + "dpct::blas_gemm::experimental::matrix_transform() does not " + "support non-zero beta currently."); + } + + if (b != nullptr) { + throw std::runtime_error( + "dpct::blas_gemm::experimental::matrix_transform() does not " + "support matrix B currently."); + } + + if ((a_desc->_type != library_data_t::real_int8 || + c_desc->_type != library_data_t::real_int8) && + (a_desc->_type != library_data_t::real_int32 || + c_desc->_type != library_data_t::real_int32)) { + throw std::runtime_error( + "dpct::blas_gemm::experimental::matrix_transform() only supports " + "combinations of data types: a_type==real_int8&&c_type==real_int8, " + "a_type==real_int32&&c_type==real_int32."); + } + + return detail::type_dispatch( + a_desc->_type, q_ptr, a_desc->_rows, a_desc->_cols, a_desc->_ld, + a_desc->_order, a, c_desc->_ld, c_desc->_order, c, + std::vector{}); +} +} // namespace dpct::blas_gemm::experimental +#endif // __DPCT_BLAS_GEMM_UTILS_HPP__ diff --git a/sycl/include/dpct/blas_utils.hpp b/sycl/include/dpct/blas_utils.hpp new file mode 100644 index 0000000000..4a58aad334 --- /dev/null +++ b/sycl/include/dpct/blas_utils.hpp @@ -0,0 +1,2599 @@ +//==---- blas_utils.hpp----------------------------*- C++ -*----------------==// +// +// Copyright (C) Intel Corporation +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// See https://llvm.org/LICENSE.txt for license information. +// +//===----------------------------------------------------------------------===// + +#ifndef __DPCT_BLAS_UTILS_HPP__ +#define __DPCT_BLAS_UTILS_HPP__ + +#include "compat_service.hpp" +#include "lib_common_utils.hpp" + +#include "detail/blas_utils_detail.hpp" + +namespace dpct { +namespace blas { + +/// Parameter input/output properties are: +/// in: input parameter +/// out: output parameter +/// in_out: input and output parameter +enum class parameter_inout_prop { in, out, in_out }; + +/// parameter_wrapper_t is a class to wrap the parameter to fit the oneMKL +/// interface. +/// E.g., +/// \code +/// void foo(sycl::queue q, int *res) { +/// // Declare the wrapper. +/// parameter_wrapper_t +/// res_wrapper(q, res); +/// // parameter_wrapper_t::get_ptr() is passed as a parameter. The result is +/// // saved into the wrapper. +/// oneapi::mkl::...(q, ..., res_wrapper.get_ptr()); +/// // The action to copy the result in the wrapper to the original variable +/// // is submitted when destructing the wrapper automatically. +/// } +/// \endcode +/// \tparam target_t Data type to fit oneMKL parameter data type. E.g., +/// get_ptr() returns \tparam target_t data type and can be used as oneMKL +/// parameter with USM memory configuration. +/// \tparam source_t Data type of the original parameter. +/// \tparam inout_prop The input/output property of the parameter. +template +class parameter_wrapper_t + : public detail::parameter_wrapper_base_t { + static_assert(inout_prop == parameter_inout_prop::out && + "Only parameter_inout_prop::out is supported if " + "target_t and source_t are not same."); + using base_t = detail::parameter_wrapper_base_t; + using base_t::_q; + using base_t::_source; + using base_t::_source_attribute; + using base_t::_target; + +public: + /// Constructor. Malloc the wrapper memory. + /// \param q The queue used for internal malloc and memcpy + /// \param source The original parameter + parameter_wrapper_t(sycl::queue q, source_t *source) : base_t(q, source, 1) {} + /// Destructor. Copy back content from wrapper memory to original memory + ~parameter_wrapper_t() { +#ifdef DPCT_USM_LEVEL_NONE + if (_source_attribute == + ::dpct::cs::detail::pointer_access_attribute::device_only) { + _q.submit([&](sycl::handler &cgh) { + auto from_acc = dpct::get_buffer(_target) + .template get_access(cgh); + auto to_acc = dpct::get_buffer(_source) + .template get_access(cgh); + cgh.single_task<::dpct::cs::kernel_name< + class parameter_wrapper_copyback, target_t, source_t>>( + [=]() { to_acc[0] = static_cast(from_acc[0]); }); + }); + } else { + source_t temp = + static_cast(dpct::get_host_ptr(_target)[0]); + *_source = temp; + } +#else + if (_source_attribute == + ::dpct::cs::detail::pointer_access_attribute::device_only) { + _q.template single_task<::dpct::cs::kernel_name< + class parameter_wrapper_copyback, target_t, source_t>>( + [t = _target, s = _source]() { *s = static_cast(*t); }); + } else { + target_t temp; + _q.memcpy(&temp, _target, sizeof(target_t)).wait(); + *_source = static_cast(temp); + } +#endif + } + /// Get the target memory of the wrapper to represent the wrapped variable + target_t *get_ptr() { return _target; } +}; + +/// parameter_wrapper_t is a class to wrap the parameter to fit the oneMKL +/// interface. +/// E.g., +/// \code +/// void foo(sycl::queue q, int *params, int ele_num) { +/// // Declare the wrapper. +/// parameter_wrapper_t params_wrapper( +/// q, params, ele_num); +/// // parameter_wrapper_t::get_ptr() is passed as a parameter. The result is +/// // saved into the wrapper. +/// oneapi::mkl::...(q, ..., params_wrapper.get_ptr()); +/// // The action to copy the result in the wrapper to the original variable +/// // is submitted when destructing the wrapper automatically. +/// } +/// \endcode +/// \tparam target_t Data type to fit oneMKL parameter data type. E.g., +/// get_ptr() returns \tparam target_t data type and can be used as oneMKL +/// parameter with USM memory configuration. +/// \tparam inout_prop The input/output property of the parameter. +template +class parameter_wrapper_t + : public detail::parameter_wrapper_base_t { + using base_t = detail::parameter_wrapper_base_t; + using base_t::_ele_num; + using base_t::_need_free; + using base_t::_q; + using base_t::_source; + using base_t::_source_attribute; + using base_t::_target; + +public: + /// Constructor. Malloc the wrapper memory. + /// \param q The queue used for internal malloc and memcpy + /// \param source The original parameter + /// \param ele_num Element number in \p source + parameter_wrapper_t(sycl::queue q, target_t *source, size_t ele_num = 1) + : base_t(q, source, ele_num) { + if constexpr (inout_prop != parameter_inout_prop::out) { + if (_need_free) { + ::dpct::cs::memcpy(_q, _target, _source, sizeof(target_t) * _ele_num, + ::dpct::cs::memcpy_direction::automatic); + } + } + } + + /// Constructor. Only vaild for parameter_inout_prop::in. + /// \param q The queue used for internal malloc and memcpy + /// \param source The original parameter + /// \param ele_num Element number in \p source + template + parameter_wrapper_t( + sycl::queue q, const target_t *source, size_t ele_num = 1, + typename std::enable_if::type * = 0) + : parameter_wrapper_t(q, const_cast(source), ele_num) {} + + /// Destructor. Copy back content from wrapper memory to original memory + ~parameter_wrapper_t() { + if constexpr (inout_prop != parameter_inout_prop::in) { + if (_need_free) { + sycl::event e = ::dpct::cs::memcpy( + _q, _source, _target, sizeof(target_t) * _ele_num, + ::dpct::cs::memcpy_direction::automatic); + (void)e; + if (_source_attribute == + ::dpct::cs::detail::pointer_access_attribute::host_only) + e.wait(); + } + } + } + /// Get the target memory of the wrapper to represent the wrapped variable + target_t *get_ptr() { return _target; } +}; + +using wrapper_int_to_int64_out = + parameter_wrapper_t; +using wrapper_int64_out = + parameter_wrapper_t; +using wrapper_float_out = + parameter_wrapper_t; +using wrapper_double_out = + parameter_wrapper_t; +using wrapper_float2_out = + parameter_wrapper_t; +using wrapper_double2_out = parameter_wrapper_t; +using wrapper_float_inout = + parameter_wrapper_t; +using wrapper_double_inout = + parameter_wrapper_t; +using wrapper_float2_inout = parameter_wrapper_t; +using wrapper_double2_inout = parameter_wrapper_t; +using wrapper_float_in = + parameter_wrapper_t; +using wrapper_double_in = + parameter_wrapper_t; + +/// Copy matrix data synchronously. The default leading dimension is column. +/// \param [out] to_ptr A pointer points to the destination location. +/// \param [in] from_ptr A pointer points to the source location. +/// \param [in] to_ld The leading dimension the destination matrix. +/// \param [in] from_ld The leading dimension the source matrix. +/// \param [in] rows The number of rows of the source matrix. +/// \param [in] cols The number of columns of the source matrix. +/// \param [in] elem_size The element size in bytes. +/// \param [in] direction The direction of the data copy. +/// \param [in] queue The queue where the routine should be executed. +/// \param [in] deps A list of events to wait for before starting copy. +inline void +matrix_mem_copy_sync(void *to_ptr, const void *from_ptr, std::int64_t to_ld, + std::int64_t from_ld, std::int64_t rows, std::int64_t cols, + std::int64_t elem_size, + ::dpct::cs::memcpy_direction direction = + ::dpct::cs::memcpy_direction::automatic, + sycl::queue &queue = ::dpct::cs::get_default_queue(), + const std::vector &deps = {}) { + if (to_ptr == from_ptr && to_ld == from_ld) + return; + if (to_ld == from_ld) { + size_t copy_size = elem_size * ((cols - 1) * (size_t)to_ld + rows); + ::dpct::cs::memcpy(queue, to_ptr, from_ptr, copy_size, direction, deps) + .wait(); + } else { + sycl::event::wait(::dpct::cs::memcpy( + queue, to_ptr, from_ptr, elem_size * to_ld, elem_size * from_ld, + elem_size * rows, cols, direction, deps)); + } +} +/// Copy matrix data asynchronously. The default leading dimension is column. +/// \return Output event to wait on to ensure copy is complete. +/// \param [out] to_ptr A pointer points to the destination location. +/// \param [in] from_ptr A pointer points to the source location. +/// \param [in] to_ld The leading dimension the destination matrix. +/// \param [in] from_ld The leading dimension the source matrix. +/// \param [in] rows The number of rows of the source matrix. +/// \param [in] cols The number of columns of the source matrix. +/// \param [in] elem_size The element size in bytes. +/// \param [in] direction The direction of the data copy. +/// \param [in] queue The queue where the routine should be executed. +/// \param [in] deps A list of events to wait for before starting copy. +inline sycl::event +matrix_mem_copy_async(void *to_ptr, const void *from_ptr, std::int64_t to_ld, + std::int64_t from_ld, std::int64_t rows, + std::int64_t cols, std::int64_t elem_size, + ::dpct::cs::memcpy_direction direction = + ::dpct::cs::memcpy_direction::automatic, + sycl::queue &queue = ::dpct::cs::get_default_queue(), + const std::vector &deps = {}) { + if (to_ptr == from_ptr && to_ld == from_ld) + return sycl::event(); + if (to_ld == from_ld) { + size_t copy_size = elem_size * ((cols - 1) * (size_t)to_ld + rows); + return ::dpct::cs::memcpy(queue, to_ptr, from_ptr, copy_size, direction, + deps); + } + auto events = ::dpct::cs::memcpy(queue, to_ptr, from_ptr, elem_size * to_ld, + elem_size * from_ld, elem_size * rows, cols, + direction, deps); + return queue.single_task(events, [] {}); +} + +/// Copy matrix data. The default leading dimension is column. +/// \param [out] to_ptr A pointer points to the destination location. +/// \param [in] from_ptr A pointer points to the source location. +/// \param [in] to_ld The leading dimension the destination matrix. +/// \param [in] from_ld The leading dimension the source matrix. +/// \param [in] rows The number of rows of the source matrix. +/// \param [in] cols The number of columns of the source matrix. +/// \param [in] elem_size The element size in bytes. +/// \param [in] direction The direction of the data copy. +/// \param [in] queue The queue where the routine should be executed. +/// \param [in] async If this argument is true, the return of the function +/// does NOT guarantee the copy is completed. +inline void +matrix_mem_copy(void *to_ptr, const void *from_ptr, std::int64_t to_ld, + std::int64_t from_ld, std::int64_t rows, std::int64_t cols, + std::int64_t elem_size, + ::dpct::cs::memcpy_direction direction = + ::dpct::cs::memcpy_direction::automatic, + sycl::queue &queue = ::dpct::cs::get_default_queue(), + bool async = false) { + if (async) + matrix_mem_copy_async(to_ptr, from_ptr, to_ld, from_ld, rows, cols, + elem_size, direction, queue); + else + matrix_mem_copy_sync(to_ptr, from_ptr, to_ld, from_ld, rows, cols, + elem_size, direction, queue); +} + +enum class math_mode : int { + mm_default, + mm_tf32, +}; + +using ::dpct::compute_type; + +class descriptor { +public: + void set_queue(::dpct::cs::queue_ptr q_ptr) noexcept { _queue_ptr = q_ptr; } + sycl::queue &get_queue() noexcept { return *_queue_ptr; } + void set_math_mode(math_mode mm) noexcept { _mm = mm; } + math_mode get_math_mode() const noexcept { return _mm; } + static inline void set_saved_queue(::dpct::cs::queue_ptr q_ptr) noexcept { + _saved_queue_ptr = q_ptr; + } + static inline sycl::queue &get_saved_queue() noexcept { + return *_saved_queue_ptr; + } + +private: + ::dpct::cs::queue_ptr _queue_ptr = &::dpct::cs::get_default_queue(); + math_mode _mm = math_mode::mm_default; + static inline ::dpct::cs::queue_ptr _saved_queue_ptr = + &::dpct::cs::get_default_queue(); +}; + +using descriptor_ptr = descriptor *; +} // namespace blas + +/// Get the value of \p s. +/// Copy the data to host synchronously, then return the data. +/// \param [in] p The pointer points the data. +/// \param [in] q The queue where the memory copy should be executed. +template inline T get_value(const T *s, sycl::queue &q) { + return detail::get_value(s, q); +} +template +inline std::complex get_value(const sycl::vec *s, sycl::queue &q) { + return detail::get_value(s, q); +} + +inline oneapi::mkl::transpose get_transpose(int t) { + if (t == 0) { + return oneapi::mkl::transpose::nontrans; + } else if (t == 1) { + return oneapi::mkl::transpose::trans; + } else { + return oneapi::mkl::transpose::conjtrans; + } +} + +/// Computes the LU factorizations of a batch of general matrices. +/// \param [in] exec_queue The queue where the routine should be executed. +/// \param [in] n The order of the matrices. +/// \param [in, out] a Array of pointers to matrices. These matrices will be +/// overwritten by lower triangulars with unit diagonal elements and upper +/// triangulars. +/// \param [in] lda The leading dimension of the matrices. +/// \param [out] ipiv An array stores the pivot indices. If \p ipiv is nullptr, +/// non-pivoting LU factorization is computed. +/// \param [out] info An array stores the error information. +/// \param [in] batch_size The size of the batch. +template +inline void getrf_batch_wrapper(sycl::queue &exec_queue, int n, T *a[], int lda, + int *ipiv, int *info, int batch_size) { + if (ipiv == nullptr) { + detail::getrfnp_batch_wrapper(exec_queue, n, a, lda, info, batch_size); + return; + } + using Ty = typename ::dpct::detail::lib_data_traits_t; + // Set the info array value to 0 + ::dpct::cs::fill(exec_queue, info, 0, + sizeof(int) * batch_size); +#ifdef DPCT_USM_LEVEL_NONE + std::int64_t stride_a = n * lda; + std::int64_t stride_ipiv = n; + std::int64_t scratchpad_size = + oneapi::mkl::lapack::getrf_batch_scratchpad_size( + exec_queue, n, n, lda, stride_a, stride_ipiv, batch_size); + + T *a_buffer_ptr; + a_buffer_ptr = (T *)::dpct::cs::malloc(stride_a * batch_size * sizeof(T)); + + T **host_a = (T **)std::malloc(batch_size * sizeof(T *)); + dpct_memcpy(host_a, a, batch_size * sizeof(T *)); + for (std::int64_t i = 0; i < batch_size; ++i) + dpct_memcpy(a_buffer_ptr + i * stride_a, host_a[i], n * lda * sizeof(T)); + + { + sycl::buffer ipiv_buf( + sycl::range<1>(batch_size * stride_ipiv)); + sycl::buffer scratchpad{sycl::range<1>(scratchpad_size)}; + auto a_buffer = get_buffer(a_buffer_ptr); + oneapi::mkl::lapack::getrf_batch(exec_queue, n, n, a_buffer, lda, stride_a, + ipiv_buf, stride_ipiv, batch_size, + scratchpad, scratchpad_size); + + auto to_buffer = get_buffer(ipiv); + exec_queue.submit([&](sycl::handler &cgh) { + auto from_acc = ipiv_buf.get_access(cgh); + auto to_acc = to_buffer.get_access(cgh); + cgh.parallel_for< + ::dpct::cs::kernel_name>( + sycl::range<2>(batch_size, n), [=](sycl::id<2> id) { + to_acc[id.get(0) * n + id.get(1)] = + static_cast(from_acc[id.get(0) * stride_ipiv + id.get(1)]); + }); + }); + } + + // Copy back to the original buffers + std::vector events; + for (std::int64_t i = 0; i < batch_size; ++i) + events.push_back(::dpct::cs::memcpy( + exec_queue, host_a[i], a_buffer_ptr + i * stride_a, n * lda * sizeof(T), + ::dpct::cs::memcpy_direction::automatic)); + + std::vector ptrs{host_a}; + std::thread mem_free_thread( + [=](std::vector pointers_array, + std::vector events_array) { + sycl::event::wait(events_array); + for (auto p : pointers_array) + std::free(p); + }, + ptrs, events); + mem_free_thread.detach(); +#else + std::int64_t *m_int64 = new std::int64_t; + std::int64_t *n_int64 = new std::int64_t; + std::int64_t *lda_int64 = new std::int64_t; + std::int64_t *group_sizes = new std::int64_t; + *m_int64 = n; + *n_int64 = n; + *lda_int64 = lda; + *group_sizes = batch_size; + std::int64_t scratchpad_size = + oneapi::mkl::lapack::getrf_batch_scratchpad_size( + exec_queue, m_int64, n_int64, lda_int64, 1, group_sizes); + + Ty *scratchpad = sycl::malloc_device(scratchpad_size, exec_queue); + std::int64_t *ipiv_int64 = + sycl::malloc_device(batch_size * n, exec_queue); + std::int64_t **ipiv_int64_ptr = + sycl::malloc_shared(batch_size, exec_queue); + T **a_shared = sycl::malloc_shared(batch_size, exec_queue); + exec_queue.memcpy(a_shared, a, batch_size * sizeof(T *)).wait(); + for (std::int64_t i = 0; i < batch_size; ++i) + ipiv_int64_ptr[i] = ipiv_int64 + n * i; + + oneapi::mkl::lapack::getrf_batch(exec_queue, m_int64, n_int64, + (Ty **)a_shared, lda_int64, ipiv_int64_ptr, + 1, group_sizes, scratchpad, scratchpad_size); + + sycl::event e = exec_queue.submit([&](sycl::handler &cgh) { + cgh.parallel_for< + ::dpct::cs::kernel_name>( + sycl::range<1>(batch_size * n), [=](sycl::id<1> idx) { + ipiv[idx] = static_cast(ipiv_int64[idx]); + }); + }); + + exec_queue.submit([&](sycl::handler &cgh) { + cgh.depends_on(e); + cgh.host_task([=] { + delete m_int64; + delete n_int64; + delete lda_int64; + delete group_sizes; + }); + }); + std::vector ptrs{scratchpad, ipiv_int64, ipiv_int64_ptr, a_shared}; + ::dpct::cs::enqueue_free(ptrs, {e}, exec_queue); +#endif +} + +/// Solves a system of linear equations with a batch of LU-factored square +/// coefficient matrices, with multiple right-hand sides. +/// \param [in] exec_queue The queue where the routine should be executed. +/// \param [in] trans Indicates the form of the linear equations. +/// \param [in] n The order of the matrices. +/// \param [in] nrhs The number of right hand sides. +/// \param [in] a Array of pointers to matrices. +/// \param [in] lda The leading dimension of the matrices in \p a. +/// \param [in] ipiv An array stores the pivots. +/// \param [in, out] b Array of pointers to matrices, whose columns are +/// the right-hand sides for the systems of equations. +/// \param [in] ldb The leading dimension of the matrices in \p b. +/// \param [out] info A value stores the error information. +/// \param [in] batch_size The size of the batch. +template +inline void getrs_batch_wrapper(sycl::queue &exec_queue, + oneapi::mkl::transpose trans, int n, int nrhs, + const T *a[], int lda, const int *ipiv, T *b[], + int ldb, int *info, int batch_size) { + using Ty = typename ::dpct::detail::lib_data_traits_t; + // Set the info value to 0 + *info = 0; +#ifdef DPCT_USM_LEVEL_NONE + std::int64_t stride_a = n * lda; + std::int64_t stride_b = nrhs * ldb; + std::int64_t stride_ipiv = n; + std::int64_t scratchpad_size = + oneapi::mkl::lapack::getrs_batch_scratchpad_size( + exec_queue, trans, n, nrhs, lda, stride_a, stride_ipiv, ldb, stride_b, + batch_size); + + T *a_buffer_ptr, *b_buffer_ptr; + a_buffer_ptr = (T *)::dpct::cs::malloc(stride_a * batch_size * sizeof(T)); + b_buffer_ptr = (T *)::dpct::cs::malloc(stride_b * batch_size * sizeof(T)); + + T **host_a = (T **)std::malloc(batch_size * sizeof(T *)); + T **host_b = (T **)std::malloc(batch_size * sizeof(T *)); + dpct_memcpy(host_a, a, batch_size * sizeof(T *)); + dpct_memcpy(host_b, b, batch_size * sizeof(T *)); + for (std::int64_t i = 0; i < batch_size; ++i) { + dpct_memcpy(a_buffer_ptr + i * stride_a, host_a[i], n * lda * sizeof(T)); + dpct_memcpy(b_buffer_ptr + i * stride_b, host_b[i], nrhs * ldb * sizeof(T)); + } + + { + auto a_buffer = get_buffer(a_buffer_ptr); + auto b_buffer = get_buffer(b_buffer_ptr); + sycl::buffer scratchpad{sycl::range<1>(scratchpad_size)}; + sycl::buffer ipiv_buf( + sycl::range<1>(batch_size * stride_ipiv)); + auto from_buf = get_buffer(ipiv); + exec_queue.submit([&](sycl::handler &cgh) { + auto from_acc = from_buf.get_access(cgh); + auto to_acc = ipiv_buf.get_access(cgh); + cgh.parallel_for< + ::dpct::cs::kernel_name>( + sycl::range<2>(batch_size, n), [=](sycl::id<2> id) { + to_acc[id.get(0) * stride_ipiv + id.get(1)] = + static_cast(from_acc[id.get(0) * n + id.get(1)]); + }); + }); + + oneapi::mkl::lapack::getrs_batch(exec_queue, trans, n, nrhs, a_buffer, lda, + stride_a, ipiv_buf, stride_ipiv, b_buffer, + ldb, stride_b, batch_size, scratchpad, + scratchpad_size); + } + + // Copy back to the original buffers + std::vector events; + for (std::int64_t i = 0; i < batch_size; ++i) + events.push_back(::dpct::cs::memcpy( + exec_queue, host_b[i], b_buffer_ptr + i * stride_b, + nrhs * ldb * sizeof(T), ::dpct::cs::memcpy_direction::automatic)); + std::vector ptrs{host_a, host_b}; + std::thread mem_free_thread( + [=](std::vector pointers_array, + std::vector events_array) { + sycl::event::wait(events_array); + for (auto p : pointers_array) + std::free(p); + }, + ptrs, events); + mem_free_thread.detach(); +#else + std::int64_t *n_int64 = new std::int64_t; + std::int64_t *nrhs_int64 = new std::int64_t; + std::int64_t *lda_int64 = new std::int64_t; + std::int64_t *ldb_int64 = new std::int64_t; + std::int64_t *group_sizes = new std::int64_t; + oneapi::mkl::transpose *trans_array = new oneapi::mkl::transpose; + *n_int64 = n; + *nrhs_int64 = nrhs; + *lda_int64 = lda; + *ldb_int64 = ldb; + *group_sizes = batch_size; + *trans_array = trans; + std::int64_t scratchpad_size = + oneapi::mkl::lapack::getrs_batch_scratchpad_size( + exec_queue, trans_array, n_int64, nrhs_int64, lda_int64, ldb_int64, 1, + group_sizes); + + Ty *scratchpad = sycl::malloc_device(scratchpad_size, exec_queue); + std::int64_t *ipiv_int64 = + sycl::malloc_device(batch_size * n, exec_queue); + std::int64_t **ipiv_int64_ptr = + sycl::malloc_shared(batch_size, exec_queue); + T **a_shared = sycl::malloc_shared(batch_size, exec_queue); + T **b_shared = sycl::malloc_shared(batch_size, exec_queue); + exec_queue.memcpy(a_shared, a, batch_size * sizeof(T *)); + exec_queue.memcpy(b_shared, b, batch_size * sizeof(T *)); + + exec_queue + .submit([&](sycl::handler &cgh) { + cgh.parallel_for< + ::dpct::cs::kernel_name>( + sycl::range<1>(batch_size * n), [=](sycl::id<1> idx) { + ipiv_int64[idx] = static_cast(ipiv[idx]); + }); + }) + .wait(); + + for (std::int64_t i = 0; i < batch_size; ++i) + ipiv_int64_ptr[i] = ipiv_int64 + n * i; + + sycl::event e = oneapi::mkl::lapack::getrs_batch( + exec_queue, trans_array, n_int64, nrhs_int64, (Ty **)a_shared, lda_int64, + ipiv_int64_ptr, (Ty **)b_shared, ldb_int64, 1, group_sizes, scratchpad, + scratchpad_size); + + exec_queue.submit([&](sycl::handler &cgh) { + cgh.depends_on(e); + cgh.host_task([=] { + delete n_int64; + delete nrhs_int64; + delete lda_int64; + delete ldb_int64; + delete group_sizes; + delete trans_array; + }); + }); + std::vector ptrs{scratchpad, ipiv_int64_ptr, ipiv_int64, a_shared, + b_shared}; + ::dpct::cs::enqueue_free(ptrs, {e}, exec_queue); +#endif +} + +/// Computes the inverses of a batch of LU-factored matrices. +/// \param [in] exec_queue The queue where the routine should be executed. +/// \param [in] n The order of the matrices. +/// \param [in] a Array of pointers to matrices. +/// \param [in] lda The leading dimension of the matrices in \p a. +/// \param [in] ipiv An array stores the pivots. +/// \param [out] b Array of pointers to inverse matrices. +/// \param [in] ldb The leading dimension of the matrices in \p b. +/// \param [out] info An array stores the error information. +/// \param [in] batch_size The size of the batch. +template +inline void getri_batch_wrapper(sycl::queue &exec_queue, int n, const T *a[], + int lda, int *ipiv, T *b[], int ldb, int *info, + int batch_size) { + using Ty = typename ::dpct::detail::lib_data_traits_t; + // Set the info array value to 0 + ::dpct::cs::fill(exec_queue, info, 0, + sizeof(int) * batch_size); +#ifdef DPCT_USM_LEVEL_NONE + std::int64_t stride_b = n * ldb; + std::int64_t stride_ipiv = n; + std::int64_t scratchpad_size = + oneapi::mkl::lapack::getri_batch_scratchpad_size( + exec_queue, n, ldb, stride_b, stride_ipiv, batch_size); + + T *b_buffer_ptr; + b_buffer_ptr = (T *)::dpct::cs::malloc(stride_b * batch_size * sizeof(T)); + + T **host_a = (T **)std::malloc(batch_size * sizeof(T *)); + T **host_b = (T **)std::malloc(batch_size * sizeof(T *)); + dpct_memcpy(host_a, a, batch_size * sizeof(T *)); + dpct_memcpy(host_b, b, batch_size * sizeof(T *)); + + for (std::int64_t i = 0; i < batch_size; ++i) { + // Need to create a copy of input matrices "a" to keep them unchanged. + // Matrices "b" (copy of matrices "a") will be used as input and output + // parameter in oneapi::mkl::lapack::getri_batch call. + blas::matrix_mem_copy(b_buffer_ptr + i * stride_b, host_a[i], ldb, lda, n, + n, sizeof(T), dpct::device_to_device, exec_queue); + } + + { + auto b_buffer = get_buffer(b_buffer_ptr); + sycl::buffer scratchpad{sycl::range<1>(scratchpad_size)}; + sycl::buffer ipiv_buf( + sycl::range<1>(batch_size * stride_ipiv)); + auto from_buf = get_buffer(ipiv); + exec_queue.submit([&](sycl::handler &cgh) { + auto from_acc = from_buf.get_access(cgh); + auto to_acc = ipiv_buf.get_access(cgh); + cgh.parallel_for< + ::dpct::cs::kernel_name>( + sycl::range<2>(batch_size, n), [=](sycl::id<2> id) { + to_acc[id.get(0) * stride_ipiv + id.get(1)] = + static_cast(from_acc[id.get(0) * n + id.get(1)]); + }); + }); + + oneapi::mkl::lapack::getri_batch(exec_queue, n, b_buffer, ldb, stride_b, + ipiv_buf, stride_ipiv, batch_size, + scratchpad, scratchpad_size); + } + + // Copy back to the original buffers + std::vector events; + for (std::int64_t i = 0; i < batch_size; ++i) + events.push_back(::dpct::cs::memcpy( + exec_queue, host_b[i], b_buffer_ptr + i * stride_b, n * ldb * sizeof(T), + ::dpct::cs::memcpy_direction::automatic)); + std::vector ptrs{host_a, host_b}; + std::thread mem_free_thread( + [=](std::vector pointers_array, + std::vector events_array) { + sycl::event::wait(events_array); + for (auto p : pointers_array) + std::free(p); + }, + ptrs, events); + mem_free_thread.detach(); +#else + std::int64_t *n_int64 = new std::int64_t; + std::int64_t *ldb_int64 = new std::int64_t; + std::int64_t *group_sizes = new std::int64_t; + *n_int64 = n; + *ldb_int64 = ldb; + *group_sizes = batch_size; + std::int64_t scratchpad_size = + oneapi::mkl::lapack::getri_batch_scratchpad_size( + exec_queue, n_int64, ldb_int64, 1, group_sizes); + + Ty *scratchpad = sycl::malloc_device(scratchpad_size, exec_queue); + std::int64_t *ipiv_int64 = + sycl::malloc_device(batch_size * n, exec_queue); + std::int64_t **ipiv_int64_ptr = + sycl::malloc_shared(batch_size, exec_queue); + + exec_queue.submit([&](sycl::handler &cgh) { + cgh.parallel_for< + ::dpct::cs::kernel_name>( + sycl::range<1>(batch_size * n), [=](sycl::id<1> idx) { + ipiv_int64[idx] = static_cast(ipiv[idx]); + }); + }); + + T **a_shared = sycl::malloc_shared(batch_size, exec_queue); + T **b_shared = sycl::malloc_shared(batch_size, exec_queue); + exec_queue.memcpy(a_shared, a, batch_size * sizeof(T *)); + exec_queue.memcpy(b_shared, b, batch_size * sizeof(T *)).wait(); + for (std::int64_t i = 0; i < batch_size; ++i) { + ipiv_int64_ptr[i] = ipiv_int64 + n * i; + // Need to create a copy of input matrices "a" to keep them unchanged. + // Matrices "b" (copy of matrices "a") will be used as input and output + // parameter in oneapi::mkl::lapack::getri_batch call. + blas::matrix_mem_copy(b_shared[i], a_shared[i], ldb, lda, n, n, sizeof(T), + ::dpct::cs::memcpy_direction::device_to_device, + exec_queue); + } + + sycl::event e = oneapi::mkl::lapack::getri_batch( + exec_queue, n_int64, (Ty **)b_shared, ldb_int64, ipiv_int64_ptr, 1, + group_sizes, scratchpad, scratchpad_size); + + exec_queue.submit([&](sycl::handler &cgh) { + cgh.depends_on(e); + cgh.host_task([=] { + delete n_int64; + delete ldb_int64; + delete group_sizes; + }); + }); + std::vector ptrs{scratchpad, ipiv_int64_ptr, ipiv_int64, a_shared, + b_shared}; + ::dpct::cs::enqueue_free(ptrs, {e}, exec_queue); +#endif +} + +/// Computes the QR factorizations of a batch of general matrices. +/// \param [in] exec_queue The queue where the routine should be executed. +/// \param [in] m The number of rows in the matrices. +/// \param [in] n The number of columns in the matrices. +/// \param [in, out] a Array of pointers to matrices. These +/// matrices will be overwritten by the factorization data. +/// \param [in] lda The leading dimension of the matrices in \p a. +/// \param [out] tau An array stores the scalars. +/// \param [out] info A value stores the error information. +/// \param [in] batch_size The size of the batch. +template +inline void geqrf_batch_wrapper(sycl::queue exec_queue, int m, int n, T *a[], + int lda, T *tau[], int *info, int batch_size) { + using Ty = typename ::dpct::detail::lib_data_traits_t; + // Set the info value to 0 + *info = 0; +#ifdef DPCT_USM_LEVEL_NONE + std::int64_t stride_a = n * lda; + std::int64_t stride_tau = (std::max)(1, (std::min)(m, n)); + std::int64_t scratchpad_size = + oneapi::mkl::lapack::geqrf_batch_scratchpad_size( + exec_queue, m, n, lda, stride_a, stride_tau, batch_size); + + T *a_buffer_ptr, *tau_buffer_ptr; + a_buffer_ptr = (T *)::dpct::cs::malloc(stride_a * batch_size * sizeof(T)); + tau_buffer_ptr = (T *)::dpct::cs::malloc(stride_tau * batch_size * sizeof(T)); + + T **host_a = (T **)std::malloc(batch_size * sizeof(T *)); + T **host_tau = (T **)std::malloc(batch_size * sizeof(T *)); + dpct_memcpy(host_a, a, batch_size * sizeof(T *)); + dpct_memcpy(host_tau, tau, batch_size * sizeof(T *)); + + for (std::int64_t i = 0; i < batch_size; ++i) + dpct_memcpy(a_buffer_ptr + i * stride_a, host_a[i], n * lda * sizeof(T)); + { + auto a_buffer = get_buffer(a_buffer_ptr); + auto tau_buffer = get_buffer(tau_buffer_ptr); + sycl::buffer scratchpad{sycl::range<1>(scratchpad_size)}; + oneapi::mkl::lapack::geqrf_batch(exec_queue, m, n, a_buffer, lda, stride_a, + tau_buffer, stride_tau, batch_size, + scratchpad, scratchpad_size); + } + + // Copy back to the original buffers + std::vector events_a; + std::vector events_tau; + for (std::int64_t i = 0; i < batch_size; ++i) { + events_a.push_back(::dpct::cs::memcpy( + exec_queue, host_a[i], a_buffer_ptr + i * stride_a, n * lda * sizeof(T), + ::dpct::cs::memcpy_direction::automatic)); + events_tau.push_back(::dpct::cs::memcpy( + exec_queue, host_tau[i], tau_buffer_ptr + i * stride_tau, + (std::max)(1, (std::min)(m, n)) * sizeof(T), + ::dpct::cs::memcpy_direction::automatic)); + } + std::vector ptr_a{host_a}; + std::vector ptr_tau{host_tau}; + std::thread mem_free_thread_a( + [=](std::vector pointers_array, + std::vector events_array) { + sycl::event::wait(events_array); + for (auto p : pointers_array) + std::free(p); + }, + ptr_a, events_a); + std::thread mem_free_thread_tau( + [=](std::vector pointers_array, + std::vector events_array) { + sycl::event::wait(events_array); + for (auto p : pointers_array) + std::free(p); + }, + ptr_tau, events_tau); + mem_free_thread_a.detach(); + mem_free_thread_tau.detach(); +#else + std::int64_t *m_int64 = new std::int64_t; + std::int64_t *n_int64 = new std::int64_t; + std::int64_t *lda_int64 = new std::int64_t; + std::int64_t *group_sizes = new std::int64_t; + *m_int64 = n; + *n_int64 = n; + *lda_int64 = lda; + *group_sizes = batch_size; + std::int64_t scratchpad_size = + oneapi::mkl::lapack::geqrf_batch_scratchpad_size( + exec_queue, m_int64, n_int64, lda_int64, 1, group_sizes); + + Ty *scratchpad = sycl::malloc_device(scratchpad_size, exec_queue); + T **a_shared = sycl::malloc_shared(batch_size, exec_queue); + T **tau_shared = sycl::malloc_shared(batch_size, exec_queue); + exec_queue.memcpy(a_shared, a, batch_size * sizeof(T *)); + exec_queue.memcpy(tau_shared, tau, batch_size * sizeof(T *)).wait(); + + sycl::event e = oneapi::mkl::lapack::geqrf_batch( + exec_queue, m_int64, n_int64, (Ty **)a_shared, lda_int64, + (Ty **)tau_shared, 1, group_sizes, scratchpad, scratchpad_size); + + exec_queue.submit([&](sycl::handler &cgh) { + cgh.depends_on(e); + cgh.host_task([=] { + delete m_int64; + delete n_int64; + delete lda_int64; + delete group_sizes; + }); + }); + std::vector ptrs{scratchpad, a_shared, tau_shared}; + ::dpct::cs::enqueue_free(ptrs, {e}, exec_queue); +#endif +} + +namespace blas { +#ifdef __INTEL_MKL__ +template +inline oneapi::mkl::blas::compute_mode +deduce_compute_mode(std::optional ct, math_mode mm) { + using Ty = typename ::dpct::detail::lib_data_traits_t; + if (ct) { + switch (ct.value()) { + case compute_type::f16_standard: + case compute_type::f32_standard: + case compute_type::f64_standard: + case compute_type::i32_standard: + return oneapi::mkl::blas::compute_mode::standard; + case compute_type::f32: + if constexpr (std::is_same_v> || + std::is_same_v>) + return oneapi::mkl::blas::compute_mode::complex_3m; + break; + case compute_type::f32_fast_bf16: + return oneapi::mkl::blas::compute_mode::float_to_bf16; + case compute_type::f32_fast_tf32: + return oneapi::mkl::blas::compute_mode::float_to_tf32; + default: + [[fallthrough]]; + } + } + if (mm == math_mode::mm_tf32) + return oneapi::mkl::blas::compute_mode::float_to_tf32; + return oneapi::mkl::blas::compute_mode::unset; +} + +inline oneapi::mkl::blas::compute_mode +deduce_compute_mode(std::optional ct, math_mode mm, + bool is_complex) { + if (is_complex) + return deduce_compute_mode>(ct, mm); + return deduce_compute_mode(ct, mm); +} +#endif + +/// Computes matrix-matrix product with general matrices. +/// \param [in] desc_ptr Descriptor. +/// \param [in] a_trans Specifies the operation applied to A. +/// \param [in] b_trans Specifies the operation applied to B. +/// \param [in] m Specifies the number of rows of the matrix op(A) and of the matrix C. +/// \param [in] n Specifies the number of columns of the matrix op(B) and of the matrix C. +/// \param [in] k Specifies the number of columns of the matrix op(A) and the number of rows of the matrix op(B). +/// \param [in] alpha Scaling factor for the matrix-matrix product. +/// \param [in] a Input matrix A. +/// \param [in] a_type Data type of the matrix A. +/// \param [in] lda Leading dimension of A. +/// \param [in] b Input matrix B. +/// \param [in] b_type Data type of the matrix B. +/// \param [in] ldb Leading dimension of B. +/// \param [in] beta Scaling factor for matrix C. +/// \param [in, out] c Input/Output matrix C. +/// \param [in] c_type Data type of the matrix C. +/// \param [in] ldc Leading dimension of C. +/// \param [in] ct Compute type. +inline void gemm(descriptor_ptr desc_ptr, oneapi::mkl::transpose a_trans, + oneapi::mkl::transpose b_trans, std::int64_t m, std::int64_t n, + std::int64_t k, const void *alpha, const void *a, + library_data_t a_type, std::int64_t lda, const void *b, + library_data_t b_type, std::int64_t ldb, const void *beta, + void *c, library_data_t c_type, std::int64_t ldc, + std::variant ct) { +#ifndef __INTEL_MKL__ + throw std::runtime_error("The oneAPI Math Kernel Library (oneMKL) Interfaces " + "Project does not support this API."); +#else + sycl::queue q = desc_ptr->get_queue(); + oneapi::mkl::blas::compute_mode cm = oneapi::mkl::blas::compute_mode::unset; + library_data_t scaling_type; + if (auto ct_p = std::get_if(&ct)) { + cm = deduce_compute_mode(*ct_p, desc_ptr->get_math_mode(), + a_type == library_data_t::complex_float || + a_type == library_data_t::complex_double); + scaling_type = detail::compute_type_to_library_data_t(*ct_p); + } else { + cm = deduce_compute_mode(std::nullopt, desc_ptr->get_math_mode(), + a_type == library_data_t::complex_float || + a_type == library_data_t::complex_double); + scaling_type = std::get(ct); + } + + if (scaling_type == library_data_t::real_float && + c_type == library_data_t::complex_float) { + scaling_type = library_data_t::complex_float; + } else if (scaling_type == library_data_t::real_double && + c_type == library_data_t::complex_double) { + scaling_type = library_data_t::complex_double; + } + + std::uint64_t key = dpct::detail::get_type_combination_id( + a_type, b_type, c_type, scaling_type); + switch (key) { + case dpct::detail::get_type_combination_id( + library_data_t::real_float, library_data_t::real_float, + library_data_t::real_float, library_data_t::real_float): { + dpct::detail::gemm_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, cm); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_double, library_data_t::real_double, + library_data_t::real_double, library_data_t::real_double): { + dpct::detail::gemm_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, cm); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::complex_float, library_data_t::complex_float, + library_data_t::complex_float, library_data_t::complex_float): { + dpct::detail::gemm_impl, std::complex, + std::complex, std::complex>( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, cm); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::complex_double, library_data_t::complex_double, + library_data_t::complex_double, library_data_t::complex_double): { + dpct::detail::gemm_impl, std::complex, + std::complex, std::complex>( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, cm); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_half, library_data_t::real_half, + library_data_t::real_half, library_data_t::real_half): { + dpct::detail::gemm_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, cm); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_bfloat16, library_data_t::real_bfloat16, + library_data_t::real_float, library_data_t::real_float): { + dpct::detail::gemm_impl(q, a_trans, b_trans, m, n, k, alpha, a, lda, + b, ldb, beta, c, ldc, cm); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_half, library_data_t::real_half, + library_data_t::real_float, library_data_t::real_float): { + dpct::detail::gemm_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, cm); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_half, library_data_t::real_half, + library_data_t::real_half, library_data_t::real_float): { + float alpha_value = + dpct::get_value(reinterpret_cast(alpha), q); + float beta_value = + dpct::get_value(reinterpret_cast(beta), q); + sycl::half alpha_half(alpha_value); + sycl::half beta_half(beta_value); + dpct::detail::gemm_impl( + q, a_trans, b_trans, m, n, k, &alpha_half, a, lda, b, ldb, &beta_half, + c, ldc, cm); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_int8, library_data_t::real_int8, + library_data_t::real_float, library_data_t::real_float): { + dpct::detail::gemm_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, cm); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_bfloat16, library_data_t::real_bfloat16, + library_data_t::real_bfloat16, library_data_t::real_float): { + dpct::detail::gemm_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, cm); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_int8, library_data_t::real_int8, + library_data_t::real_int32, library_data_t::real_int32): { + float alpha_float = + dpct::get_value(reinterpret_cast(alpha), q); + float beta_float = + dpct::get_value(reinterpret_cast(beta), q); + dpct::detail::gemm_impl( + q, a_trans, b_trans, m, n, k, &alpha_float, a, lda, b, ldb, &beta_float, + c, ldc, cm); + break; + } + default: + throw std::runtime_error("the combination of data type is unsupported"); + } +#endif +} + +/// Computes a batch of matrix-matrix product with general matrices. +/// \param [in] desc_ptr Descriptor. +/// \param [in] a_trans Specifies the operation applied to A. +/// \param [in] b_trans Specifies the operation applied to B. +/// \param [in] m Specifies the number of rows of the matrix op(A) and of the matrix C. +/// \param [in] n Specifies the number of columns of the matrix op(B) and of the matrix C. +/// \param [in] k Specifies the number of columns of the matrix op(A) and the number of rows of the matrix op(B). +/// \param [in] alpha Scaling factor for the matrix-matrix product. +/// \param [in] a Input matrix A. +/// \param [in] a_type Data type of the matrix A. +/// \param [in] lda Leading dimension of A. +/// \param [in] b Input matrix B. +/// \param [in] b_type Data type of the matrix B. +/// \param [in] ldb Leading dimension of B. +/// \param [in] beta Scaling factor for matrix C. +/// \param [in, out] c Input/Output matrix C. +/// \param [in] c_type Data type of the matrix C. +/// \param [in] ldc Leading dimension of C. +/// \param [in] batch_size Specifies the number of matrix multiply operations to perform. +/// \param [in] scaling_type Data type of the scaling factors. +/// \param [in] ct Compute type. +inline void gemm_batch(descriptor_ptr desc_ptr, oneapi::mkl::transpose a_trans, + oneapi::mkl::transpose b_trans, std::int64_t m, + std::int64_t n, std::int64_t k, const void *alpha, + const void *a[], library_data_t a_type, std::int64_t lda, + const void *b[], library_data_t b_type, std::int64_t ldb, + const void *beta, void *c[], library_data_t c_type, + std::int64_t ldc, std::int64_t batch_size, + std::variant ct) { +#ifdef DPCT_USM_LEVEL_NONE + throw std::runtime_error("this API is unsupported when USM level is none"); +#else + sycl::queue q = desc_ptr->get_queue(); +#ifdef __INTEL_MKL__ + oneapi::mkl::blas::compute_mode cm = oneapi::mkl::blas::compute_mode::unset; +#endif + library_data_t scaling_type; + if (auto ct_p = std::get_if(&ct)) { +#ifdef __INTEL_MKL__ + cm = deduce_compute_mode(*ct_p, desc_ptr->get_math_mode(), + a_type == library_data_t::complex_float || + a_type == library_data_t::complex_double); +#endif + scaling_type = detail::compute_type_to_library_data_t(*ct_p); + } else { +#ifdef __INTEL_MKL__ + cm = deduce_compute_mode(std::nullopt, desc_ptr->get_math_mode(), + a_type == library_data_t::complex_float || + a_type == library_data_t::complex_double); +#endif + scaling_type = std::get(ct); + } + + if (scaling_type == library_data_t::real_float && + c_type == library_data_t::complex_float) { + scaling_type = library_data_t::complex_float; + } else if (scaling_type == library_data_t::real_double && + c_type == library_data_t::complex_double) { + scaling_type = library_data_t::complex_double; + } + + std::uint64_t key = dpct::detail::get_type_combination_id( + a_type, b_type, c_type, scaling_type); + switch (key) { + case dpct::detail::get_type_combination_id( + library_data_t::real_float, library_data_t::real_float, + library_data_t::real_float, library_data_t::real_float): { + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, + batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_double, library_data_t::real_double, + library_data_t::real_double, library_data_t::real_double): { + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, + batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::complex_float, library_data_t::complex_float, + library_data_t::complex_float, library_data_t::complex_float): { + dpct::detail::gemm_batch_impl, std::complex, + std::complex, std::complex>( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, + batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::complex_double, library_data_t::complex_double, + library_data_t::complex_double, library_data_t::complex_double): { + dpct::detail::gemm_batch_impl, std::complex, + std::complex, std::complex>( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, + batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_half, library_data_t::real_half, + library_data_t::real_half, library_data_t::real_half): { + dpct::detail::gemm_batch_impl(q, a_trans, b_trans, m, n, k, + alpha, a, lda, b, ldb, beta, c, + ldc, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } +#ifdef __INTEL_MKL__ + case dpct::detail::get_type_combination_id( + library_data_t::real_bfloat16, library_data_t::real_bfloat16, + library_data_t::real_bfloat16, library_data_t::real_float): { + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, + batch_size, cm); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_bfloat16, library_data_t::real_bfloat16, + library_data_t::real_float, library_data_t::real_float): { + dpct::detail::gemm_batch_impl(q, a_trans, b_trans, m, n, k, + alpha, a, lda, b, ldb, beta, c, + ldc, batch_size, cm); + break; + } +#endif + case dpct::detail::get_type_combination_id( + library_data_t::real_int8, library_data_t::real_int8, + library_data_t::real_int32, library_data_t::real_int32): { + float alpha_float = + dpct::get_value(reinterpret_cast(alpha), q); + float beta_float = + dpct::get_value(reinterpret_cast(beta), q); + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, &alpha_float, a, lda, b, ldb, &beta_float, + c, ldc, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_int8, library_data_t::real_int8, + library_data_t::real_float, library_data_t::real_float): { + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, + batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_half, library_data_t::real_half, + library_data_t::real_float, library_data_t::real_float): { + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, + batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_half, library_data_t::real_half, + library_data_t::real_half, library_data_t::real_float): { + float alpha_value = + dpct::get_value(reinterpret_cast(alpha), q); + float beta_value = + dpct::get_value(reinterpret_cast(beta), q); + sycl::half alpha_half(alpha_value); + sycl::half beta_half(beta_value); + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, &alpha_half, a, lda, b, ldb, &beta_half, + c, ldc, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + default: + throw std::runtime_error("the combination of data type is unsupported"); + } +#endif +} + +/// Computes a batch of matrix-matrix product with general matrices. +/// \param [in] desc_ptr Descriptor. +/// \param [in] a_trans Specifies the operation applied to A. +/// \param [in] b_trans Specifies the operation applied to B. +/// \param [in] m Specifies the number of rows of the matrix op(A) and of the matrix C. +/// \param [in] n Specifies the number of columns of the matrix op(B) and of the matrix C. +/// \param [in] k Specifies the number of columns of the matrix op(A) and the number of rows of the matrix op(B). +/// \param [in] alpha Scaling factor for the matrix-matrix product. +/// \param [in] a Input matrix A. +/// \param [in] a_type Data type of the matrix A. +/// \param [in] lda Leading dimension of A. +/// \param [in] stride_a Stride between the different A matrices. +/// \param [in] b Input matrix B. +/// \param [in] b_type Data type of the matrix B. +/// \param [in] ldb Leading dimension of B. +/// \param [in] stride_b Stride between the different B matrices. +/// \param [in] beta Scaling factor for matrix C. +/// \param [in, out] c Input/Output matrix C. +/// \param [in] c_type Data type of the matrix C. +/// \param [in] ldc Leading dimension of C. +/// \param [in] stride_c Stride between the different C matrices. +/// \param [in] batch_size Specifies the number of matrix multiply operations to perform. +/// \param [in] ct Compute type. +inline void gemm_batch(descriptor_ptr desc_ptr, oneapi::mkl::transpose a_trans, + oneapi::mkl::transpose b_trans, std::int64_t m, + std::int64_t n, std::int64_t k, const void *alpha, + const void *a, library_data_t a_type, std::int64_t lda, + long long int stride_a, const void *b, + library_data_t b_type, std::int64_t ldb, + long long int stride_b, const void *beta, void *c, + library_data_t c_type, std::int64_t ldc, + long long int stride_c, std::int64_t batch_size, + std::variant ct) { + sycl::queue q = desc_ptr->get_queue(); +#ifdef __INTEL_MKL__ + oneapi::mkl::blas::compute_mode cm = oneapi::mkl::blas::compute_mode::unset; +#endif + library_data_t scaling_type; + if (auto ct_p = std::get_if(&ct)) { +#ifdef __INTEL_MKL__ + cm = deduce_compute_mode(*ct_p, desc_ptr->get_math_mode(), + a_type == library_data_t::complex_float || + a_type == library_data_t::complex_double); +#endif + scaling_type = detail::compute_type_to_library_data_t(*ct_p); + } else { +#ifdef __INTEL_MKL__ + cm = deduce_compute_mode(std::nullopt, desc_ptr->get_math_mode(), + a_type == library_data_t::complex_float || + a_type == library_data_t::complex_double); +#endif + scaling_type = std::get(ct); + } + + if (scaling_type == library_data_t::real_float && + c_type == library_data_t::complex_float) { + scaling_type = library_data_t::complex_float; + } else if (scaling_type == library_data_t::real_double && + c_type == library_data_t::complex_double) { + scaling_type = library_data_t::complex_double; + } + + std::uint64_t key = dpct::detail::get_type_combination_id( + a_type, b_type, c_type, scaling_type); + switch (key) { + case dpct::detail::get_type_combination_id( + library_data_t::real_float, library_data_t::real_float, + library_data_t::real_float, library_data_t::real_float): { + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, stride_a, b, ldb, stride_b, + beta, c, ldc, stride_c, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_double, library_data_t::real_double, + library_data_t::real_double, library_data_t::real_double): { + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, stride_a, b, ldb, stride_b, + beta, c, ldc, stride_c, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::complex_float, library_data_t::complex_float, + library_data_t::complex_float, library_data_t::complex_float): { + dpct::detail::gemm_batch_impl, std::complex, + std::complex, std::complex>( + q, a_trans, b_trans, m, n, k, alpha, a, lda, stride_a, b, ldb, stride_b, + beta, c, ldc, stride_c, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::complex_double, library_data_t::complex_double, + library_data_t::complex_double, library_data_t::complex_double): { + dpct::detail::gemm_batch_impl, std::complex, + std::complex, std::complex>( + q, a_trans, b_trans, m, n, k, alpha, a, lda, stride_a, b, ldb, stride_b, + beta, c, ldc, stride_c, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_half, library_data_t::real_half, + library_data_t::real_half, library_data_t::real_half): { + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, stride_a, b, ldb, stride_b, + beta, c, ldc, stride_c, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } +#ifdef __INTEL_MKL__ + case dpct::detail::get_type_combination_id( + library_data_t::real_bfloat16, library_data_t::real_bfloat16, + library_data_t::real_bfloat16, library_data_t::real_float): { + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, stride_a, b, ldb, stride_b, + beta, c, ldc, stride_c, batch_size, cm); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_bfloat16, library_data_t::real_bfloat16, + library_data_t::real_float, library_data_t::real_float): { + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, stride_a, b, ldb, stride_b, + beta, c, ldc, stride_c, batch_size, cm); + break; + } +#endif + case dpct::detail::get_type_combination_id( + library_data_t::real_int8, library_data_t::real_int8, + library_data_t::real_int32, library_data_t::real_int32): { + float alpha_float = + dpct::get_value(reinterpret_cast(alpha), q); + float beta_float = + dpct::get_value(reinterpret_cast(beta), q); + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, &alpha_float, a, lda, stride_a, b, ldb, + stride_b, &beta_float, c, ldc, stride_c, + batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_int8, library_data_t::real_int8, + library_data_t::real_float, library_data_t::real_float): { + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, stride_a, b, ldb, stride_b, + beta, c, ldc, stride_c, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_half, library_data_t::real_half, + library_data_t::real_float, library_data_t::real_float): { + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, alpha, a, lda, stride_a, b, ldb, stride_b, + beta, c, ldc, stride_c, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id( + library_data_t::real_half, library_data_t::real_half, + library_data_t::real_half, library_data_t::real_float): { + float alpha_value = + dpct::get_value(reinterpret_cast(alpha), q); + float beta_value = + dpct::get_value(reinterpret_cast(beta), q); + sycl::half alpha_half(alpha_value); + sycl::half beta_half(beta_value); + dpct::detail::gemm_batch_impl( + q, a_trans, b_trans, m, n, k, &alpha_half, a, lda, stride_a, b, ldb, + stride_b, &beta_half, c, ldc, stride_c, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + default: + throw std::runtime_error("the combination of data type is unsupported"); + } +} + +/// Performs a symmetric/hermitian rank-k update. +/// \tparam is_hermitian True means current matrix is hermitian. +/// \param [in] desc_ptr Descriptor. +/// \param [in] uplo Specifies whether matrix c is upper or lower triangular. +/// \param [in] trans Specifies op(a), the transposition operation applied to +/// matrix a. +/// \param [in] n Number of rows and columns of matrix c. +/// \param [in] k Number of columns of matrix op(a). +/// \param [in] alpha Scaling factor for the rank-k update. +/// \param [in] a Input matrix a. +/// \param [in] a_type Data type of the matrix a. +/// \param [in] lda Leading dimension of the matrix a. +/// \param [in] beta Scaling factor for the rank-k update. +/// \param [in, out] c Input/Output matrix c. +/// \param [in] c_type Data type of the matrix c. +/// \param [in] ldc Leading dimension of the matrix c. +/// \param [in] ct Compute type. +template +inline void syherk(descriptor_ptr desc_ptr, oneapi::mkl::uplo uplo, + oneapi::mkl::transpose trans, std::int64_t n, std::int64_t k, + const void *alpha, const void *a, library_data_t a_type, + std::int64_t lda, const void *beta, void *c, + library_data_t c_type, std::int64_t ldc, + std::variant ct) { + sycl::queue q = desc_ptr->get_queue(); +#ifdef __INTEL_MKL__ + oneapi::mkl::blas::compute_mode cm = oneapi::mkl::blas::compute_mode::unset; + if (auto ct_p = std::get_if(&ct)) { + cm = deduce_compute_mode(*ct_p, desc_ptr->get_math_mode(), + a_type == library_data_t::complex_float || + a_type == library_data_t::complex_double); + } else { + cm = deduce_compute_mode(std::nullopt, desc_ptr->get_math_mode(), + a_type == library_data_t::complex_float || + a_type == library_data_t::complex_double); + } +#endif + std::uint64_t key = dpct::detail::get_type_combination_id(a_type, c_type); + if (!is_hermitian && + dpct::detail::get_type_combination_id( + library_data_t::real_float, library_data_t::real_float) == key) { + dpct::detail::syherk_impl(q, uplo, trans, n, k, alpha, a, lda, + beta, c, ldc DPCT_COMPUTE_MODE_ARG); + } else if (!is_hermitian && dpct::detail::get_type_combination_id( + library_data_t::real_double, + library_data_t::real_double) == key) { + dpct::detail::syherk_impl(q, uplo, trans, n, k, alpha, a, + lda, beta, c, + ldc DPCT_COMPUTE_MODE_ARG); + } else if (dpct::detail::get_type_combination_id( + library_data_t::complex_float, + library_data_t::complex_float) == key) { + dpct::detail::syherk_impl>( + q, uplo, trans, n, k, alpha, a, lda, beta, c, + ldc DPCT_COMPUTE_MODE_ARG); + } else if (dpct::detail::get_type_combination_id( + library_data_t::complex_double, + library_data_t::complex_double) == key) { + dpct::detail::syherk_impl>( + q, uplo, trans, n, k, alpha, a, lda, beta, c, + ldc DPCT_COMPUTE_MODE_ARG); + } else { + throw std::runtime_error("the combination of data type is unsupported"); + } +} + +/// This routines perform a special rank-k update of a symmetric matrix C by +/// general matrices A and B. +/// \param [in] desc_ptr Descriptor. +/// \param [in] uplo Specifies whether C's data is stored in its upper or lower triangle. +/// \param [in] trans Specifies the operation to apply. +/// \param [in] n The number of rows and columns in C. +/// \param [in] k The inner dimension of matrix multiplications. +/// \param [in] alpha Scaling factor for the rank-k update. +/// \param [in] a Input matrix A. +/// \param [in] lda Leading dimension of A. +/// \param [in] b Input matrix B. +/// \param [in] ldb Leading dimension of B. +/// \param [in] beta Scaling factor for the rank-k update. +/// \param [in, out] c Input/Output matrix C. +/// \param [in] ldc Leading dimension of C. +template +inline void syrk(descriptor_ptr desc_ptr, oneapi::mkl::uplo uplo, + oneapi::mkl::transpose trans, std::int64_t n, std::int64_t k, + const T *alpha, const T *a, std::int64_t lda, const T *b, + std::int64_t ldb, const T *beta, T *c, std::int64_t ldc) { + sycl::queue q = desc_ptr->get_queue(); +#ifdef __INTEL_MKL__ + auto cm = deduce_compute_mode(std::nullopt, desc_ptr->get_math_mode()); +#endif + dpct::detail::rk_impl(q, uplo, trans, n, k, alpha, a, lda, b, + ldb, beta, c, ldc DPCT_COMPUTE_MODE_ARG); +} + +/// This routines perform a special rank-k update of a Hermitian matrix C by +/// general matrices A and B. +/// \param [in] desc_ptr Descriptor. +/// \param [in] uplo Specifies whether C's data is stored in its upper or lower triangle. +/// \param [in] trans Specifies the operation to apply. +/// \param [in] n The number of rows and columns in C. +/// \param [in] k The inner dimension of matrix multiplications. +/// \param [in] alpha Scaling factor for the rank-k update. +/// \param [in] a Input matrix A. +/// \param [in] lda Leading dimension of A. +/// \param [in] b Input matrix B. +/// \param [in] ldb Leading dimension of B. +/// \param [in] beta Scaling factor for the rank-k update. +/// \param [in, out] c Input/Output matrix C. +/// \param [in] ldc Leading dimension of C. +template +inline void herk(descriptor_ptr desc_ptr, oneapi::mkl::uplo uplo, + oneapi::mkl::transpose trans, std::int64_t n, std::int64_t k, + const T *alpha, const T *a, std::int64_t lda, const T *b, + std::int64_t ldb, const Tbeta *beta, T *c, std::int64_t ldc) { + sycl::queue q = desc_ptr->get_queue(); +#ifdef __INTEL_MKL__ + auto cm = deduce_compute_mode(std::nullopt, desc_ptr->get_math_mode()); +#endif + dpct::detail::rk_impl(q, uplo, trans, n, k, alpha, a, lda, b, + ldb, beta, c, + ldc DPCT_COMPUTE_MODE_ARG); +} + +/// This routine performs a group of trsm operations. Each trsm solves an +/// equation of the form op(A) * X = alpha * B or X * op(A) = alpha * B. +/// \param [in] desc_ptr Descriptor. +/// \param [in] left_right Specifies A multiplies X on the left or on the right. +/// \param [in] upper_lower Specifies A is upper or lower triangular. +/// \param [in] trans Specifies the operation applied to A. +/// \param [in] unit_diag Specifies whether A is unit triangular. +/// \param [in] m Number of rows of the B matrices. +/// \param [in] n Number of columns of the B matrices. +/// \param [in] alpha Scaling factor for the solutions. +/// \param [in] a Input matrices A. +/// \param [in] a_type Data type of the matrices A. +/// \param [in] lda Leading dimension of the matrices A. +/// \param [in, out] b Input and output matrices B. +/// \param [in] b_type Data type of the matrices B. +/// \param [in] ldb Leading dimension of the matrices B. +/// \param [in] batch_size Specifies the number of trsm operations to perform. +/// \param [in] ct Compute type. +inline void trsm_batch(descriptor_ptr desc_ptr, oneapi::mkl::side left_right, + oneapi::mkl::uplo upper_lower, + oneapi::mkl::transpose trans, + oneapi::mkl::diag unit_diag, std::int64_t m, + std::int64_t n, const void *alpha, const void **a, + library_data_t a_type, std::int64_t lda, void **b, + library_data_t b_type, std::int64_t ldb, + std::int64_t batch_size, + std::variant ct) { +#ifdef DPCT_USM_LEVEL_NONE + throw std::runtime_error("this API is unsupported when USM level is none"); +#else + sycl::queue q = desc_ptr->get_queue(); +#ifdef __INTEL_MKL__ + oneapi::mkl::blas::compute_mode cm = oneapi::mkl::blas::compute_mode::unset; + if (auto ct_p = std::get_if(&ct)) + cm = deduce_compute_mode(*ct_p, desc_ptr->get_math_mode(), + a_type == library_data_t::complex_float || + a_type == library_data_t::complex_double); + else + cm = deduce_compute_mode(std::nullopt, desc_ptr->get_math_mode(), + a_type == library_data_t::complex_float || + a_type == library_data_t::complex_double); +#endif + std::uint64_t key = dpct::detail::get_type_combination_id(a_type, b_type); + switch (key) { + case dpct::detail::get_type_combination_id(library_data_t::real_float, + library_data_t::real_float): { + dpct::detail::trsm_batch_impl( + q, left_right, upper_lower, trans, unit_diag, m, n, alpha, a, lda, b, + ldb, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id(library_data_t::real_double, + library_data_t::real_double): { + dpct::detail::trsm_batch_impl( + q, left_right, upper_lower, trans, unit_diag, m, n, alpha, a, lda, b, + ldb, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id(library_data_t::complex_float, + library_data_t::complex_float): { + dpct::detail::trsm_batch_impl, std::complex, + std::complex>( + q, left_right, upper_lower, trans, unit_diag, m, n, alpha, a, lda, b, + ldb, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + case dpct::detail::get_type_combination_id(library_data_t::complex_double, + library_data_t::complex_double): { + dpct::detail::trsm_batch_impl, std::complex, + std::complex>( + q, left_right, upper_lower, trans, unit_diag, m, n, alpha, a, lda, b, + ldb, batch_size DPCT_COMPUTE_MODE_ARG); + break; + } + default: + throw std::runtime_error("the combination of data type is unsupported"); + } +#endif +} + +/// Computes a triangular matrix-general matrix product. +/// \param [in] desc_ptr Descriptor. +/// \param [in] left_right Specifies A is on the left or right side of the +/// multiplication. +/// \param [in] upper_lower Specifies A is upper or lower triangular. +/// \param [in] trans Specifies the operation applied to A. +/// \param [in] unit_diag Specifies whether A is unit triangular. +/// \param [in] m Number of rows of B. +/// \param [in] n Number of columns of B. +/// \param [in] alpha Scaling factor for the matrix-matrix product. +/// \param [in] a Input matrices A. +/// \param [in] lda Leading dimension of the matrices A. +/// \param [in] b Input matrices B. +/// \param [in] ldb Leading dimension of the matrices B. +/// \param [out] c Output matrices C. +/// \param [in] ldc Leading dimension of the matrices C. +template +inline void trmm(descriptor_ptr desc_ptr, oneapi::mkl::side left_right, + oneapi::mkl::uplo upper_lower, oneapi::mkl::transpose trans, + oneapi::mkl::diag unit_diag, std::int64_t m, std::int64_t n, + const T *alpha, const T *a, std::int64_t lda, const T *b, + std::int64_t ldb, T *c, std::int64_t ldc) { + using Ty = typename ::dpct::detail::lib_data_traits_t; + sycl::queue q = desc_ptr->get_queue(); +#ifdef __INTEL_MKL__ + auto cm = deduce_compute_mode(std::nullopt, desc_ptr->get_math_mode()); +#endif + auto alpha_val = dpct::get_value(alpha, q); + if (b != c) { + dpct::blas::matrix_mem_copy(c, b, ldc, ldb, m, n, sizeof(Ty), + ::dpct::cs::memcpy_direction::device_to_device, + q); + } + auto data_a = dpct::detail::get_memory(a); + auto data_c = dpct::detail::get_memory(c); + oneapi::mkl::blas::column_major::trmm(q, left_right, upper_lower, trans, + unit_diag, m, n, alpha_val, data_a, lda, + data_c, ldc DPCT_COMPUTE_MODE_ARG); +} + +/// Computes the Euclidean norm of a vector. +/// \param [in] desc_ptr Descriptor. +/// \param [in] n Number of elements in vector x. +/// \param [in] x Input vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [out] result The result scalar. +/// \param [in] result_type Data type of the result. +inline void nrm2(descriptor_ptr desc_ptr, std::int64_t n, const void *x, + library_data_t x_type, std::int64_t incx, void *result, + library_data_t result_type) { + sycl::queue q = desc_ptr->get_queue(); + std::uint64_t key = + ::dpct::detail::get_type_combination_id(x_type, result_type); + switch (key) { + case ::dpct::detail::get_type_combination_id(library_data_t::real_float, + library_data_t::real_float): { + ::dpct::detail::nrm2_impl(q, n, x, incx, result); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::real_double, + library_data_t::real_double): { + ::dpct::detail::nrm2_impl(q, n, x, incx, result); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::complex_float, + library_data_t::real_float): { + ::dpct::detail::nrm2_impl, float>(q, n, x, incx, + result); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::complex_double, + library_data_t::real_double): { + ::dpct::detail::nrm2_impl, double>(q, n, x, incx, + result); + break; + } +#ifdef __INTEL_MKL__ + case ::dpct::detail::get_type_combination_id(library_data_t::real_half, + library_data_t::real_half): { + ::dpct::detail::nrm2_impl(q, n, x, incx, result); + break; + } +#endif + default: + throw std::runtime_error("the combination of data type is unsupported"); + } +} + +/// Computes the dot product of two vectors. +/// \param [in] desc_ptr Descriptor. +/// \param [in] n Number of elements in vector x. +/// \param [in] x Input vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [in] y Input vector y. +/// \param [in] y_type Data type of the vector y. +/// \param [in] incy Stride of vector y. +/// \param [out] result The result scalar. +/// \param [in] result_type Data type of the result. +inline void dot(descriptor_ptr desc_ptr, std::int64_t n, const void *x, + library_data_t x_type, std::int64_t incx, const void *y, + library_data_t y_type, std::int64_t incy, void *result, + library_data_t result_type) { + sycl::queue q = desc_ptr->get_queue(); + ::dpct::detail::dotuc(q, n, x, x_type, incx, y, y_type, incy, result, + result_type); +} + +/// Computes the dot product of two vectors, conjugating the first vector. +/// \param [in] desc_ptr Descriptor. +/// \param [in] n Number of elements in vector x. +/// \param [in] x Input vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [in] y Input vector y. +/// \param [in] y_type Data type of the vector y. +/// \param [in] incy Stride of vector y. +/// \param [out] result The result scalar. +/// \param [in] result_type Data type of the result. +inline void dotc(descriptor_ptr desc_ptr, std::int64_t n, const void *x, + library_data_t x_type, std::int64_t incx, const void *y, + library_data_t y_type, std::int64_t incy, void *result, + library_data_t result_type) { + sycl::queue q = desc_ptr->get_queue(); + ::dpct::detail::dotuc(q, n, x, x_type, incx, y, y_type, incy, result, + result_type); +} + +/// Computes the product of a vector by a scalar. +/// \param [in] desc_ptr Descriptor. +/// \param [in] n Number of elements in vector x. +/// \param [in] alpha The scale factor alpha. +/// \param [in] alpha_type The data type of alpha. +/// \param [in, out] x Input/Output vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +inline void scal(descriptor_ptr desc_ptr, std::int64_t n, const void *alpha, + library_data_t alpha_type, void *x, library_data_t x_type, + std::int64_t incx) { + sycl::queue q = desc_ptr->get_queue(); + std::uint64_t key = ::dpct::detail::get_type_combination_id(x_type); + switch (key) { + case ::dpct::detail::get_type_combination_id(library_data_t::real_float): { + ::dpct::detail::scal_impl(q, n, alpha, x, incx); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::real_double): { + ::dpct::detail::scal_impl(q, n, alpha, x, incx); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::complex_float): { + ::dpct::detail::scal_impl, std::complex>( + q, n, alpha, x, incx); + break; + } + case ::dpct::detail::get_type_combination_id( + library_data_t::complex_double): { + ::dpct::detail::scal_impl, std::complex>( + q, n, alpha, x, incx); + break; + } +#ifdef __INTEL_MKL__ + case ::dpct::detail::get_type_combination_id(library_data_t::real_half): { + float alpha_value = + dpct::get_value(reinterpret_cast(alpha), q); + sycl::half alaph_half(alpha_value); + ::dpct::detail::scal_impl(q, n, &alaph_half, x, + incx); + break; + } +#endif + default: + throw std::runtime_error("the combination of data type is unsupported"); + } +} + +/// Computes a vector-scalar product and adds the result to a vector. +/// \param [in] desc_ptr Descriptor. +/// \param [in] n Number of elements in vector x. +/// \param [in] alpha The scale factor alpha. +/// \param [in] alpha_type The data type of alpha. +/// \param [in] x Input vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [in, out] y Input/Output vector y. +/// \param [in] y_type Data type of the vector y. +/// \param [in] incy Stride of vector y. +inline void axpy(descriptor_ptr desc_ptr, std::int64_t n, const void *alpha, + library_data_t alpha_type, const void *x, + library_data_t x_type, std::int64_t incx, void *y, + library_data_t y_type, std::int64_t incy) { + sycl::queue q = desc_ptr->get_queue(); + std::uint64_t key = + ::dpct::detail::get_type_combination_id(x_type, alpha_type); + switch (key) { + case ::dpct::detail::get_type_combination_id(library_data_t::real_float, + library_data_t::real_float): { + ::dpct::detail::axpy_impl(q, n, alpha, x, incx, y, incy); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::real_double, + library_data_t::real_double): { + ::dpct::detail::axpy_impl(q, n, alpha, x, incx, y, incy); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::complex_float, + library_data_t::complex_float): { + ::dpct::detail::axpy_impl, std::complex>( + q, n, alpha, x, incx, y, incy); + break; + } + case ::dpct::detail::get_type_combination_id( + library_data_t::complex_double, library_data_t::complex_double): { + ::dpct::detail::axpy_impl, std::complex>( + q, n, alpha, x, incx, y, incy); + break; + } +#ifdef __INTEL_MKL__ + case ::dpct::detail::get_type_combination_id(library_data_t::real_half, + library_data_t::real_float): { + float alpha_value = + dpct::get_value(reinterpret_cast(alpha), q); + sycl::half alaph_half(alpha_value); + ::dpct::detail::axpy_impl(q, n, &alaph_half, x, + incx, y, incy); + break; + } +#endif + default: + throw std::runtime_error("the combination of data type is unsupported"); + } +} + +/// Performs rotation of points in the plane. +/// \param [in] desc_ptr Descriptor. +/// \param [in] n Number of elements in vector x. +/// \param [in, out] x Input/Output vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [in, out] y Input/Output vector y. +/// \param [in] y_type Data type of the vector y. +/// \param [in] incy Stride of vector y. +/// \param [in] c Scaling factor. +/// \param [in] s Scaling factor. +/// \param [in] cs_type Data type of the scaling factors. +inline void rot(descriptor_ptr desc_ptr, std::int64_t n, void *x, + library_data_t x_type, std::int64_t incx, void *y, + library_data_t y_type, std::int64_t incy, const void *c, + const void *s, library_data_t cs_type) { + sycl::queue q = desc_ptr->get_queue(); + std::uint64_t key = ::dpct::detail::get_type_combination_id(x_type, cs_type); + switch (key) { + case ::dpct::detail::get_type_combination_id(library_data_t::real_float, + library_data_t::real_float): { + ::dpct::detail::rot_impl(q, n, x, incx, y, incy, c, s); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::real_double, + library_data_t::real_double): { + ::dpct::detail::rot_impl(q, n, x, incx, y, incy, c, + s); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::complex_float, + library_data_t::real_float): { + ::dpct::detail::rot_impl, float, float>(q, n, x, incx, + y, incy, c, s); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::complex_double, + library_data_t::real_double): { + ::dpct::detail::rot_impl, double, double>( + q, n, x, incx, y, incy, c, s); + break; + } +#ifdef __INTEL_MKL__ + case ::dpct::detail::get_type_combination_id(library_data_t::complex_float, + library_data_t::complex_float): { + ::dpct::detail::rot_impl, float, std::complex>( + q, n, x, incx, y, incy, c, s); + break; + } + case ::dpct::detail::get_type_combination_id( + library_data_t::complex_double, library_data_t::complex_double): { + ::dpct::detail::rot_impl, double, + std::complex>(q, n, x, incx, y, incy, c, + s); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::real_half, + library_data_t::real_half): { + ::dpct::detail::rot_impl(q, n, x, incx, + y, incy, c, s); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::real_bfloat16, + library_data_t::real_bfloat16): { + ::dpct::detail::rot_impl(q, n, x, incx, y, incy, c, + s); + break; + } +#endif + default: + throw std::runtime_error("the combination of data type is unsupported"); + } +} + +/// Performs modified Givens rotation of points in the plane. +/// \param [in] desc_ptr Descriptor. +/// \param [in] n Number of elements in vector x. +/// \param [in, out] x Input/Output vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [in, out] y Input/Output vector y. +/// \param [in] y_type Data type of the vector y. +/// \param [in] incy Stride of vector y. +/// \param [in] param Array of 5 parameters. +/// \param [in] param_type Data type of \p param. +inline void rotm(descriptor_ptr desc_ptr, std::int64_t n, void *x, + library_data_t x_type, int64_t incx, void *y, + library_data_t y_type, int64_t incy, const void *param, + library_data_t param_type) { + sycl::queue q = desc_ptr->get_queue(); + std::uint64_t key = + ::dpct::detail::get_type_combination_id(x_type, param_type); + switch (key) { + case ::dpct::detail::get_type_combination_id(library_data_t::real_float, + library_data_t::real_float, + library_data_t::real_float): { + ::dpct::detail::rotm_impl(q, n, x, incx, y, incy, + param); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::real_double, + library_data_t::real_double, + library_data_t::real_double): { + ::dpct::detail::rotm_impl(q, n, x, incx, y, incy, + param); + break; + } + default: + throw std::runtime_error("the combination of data type is unsupported"); + } +} + +/// Copies a vector to another vector. +/// \param [in] desc_ptr Descriptor. +/// \param [in] n Number of elements in vector x. +/// \param [in] x Input vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [out] y Output vector y. +/// \param [in] y_type Data type of the vector y. +/// \param [in] incy Stride of vector y. +inline void copy(descriptor_ptr desc_ptr, std::int64_t n, const void *x, + library_data_t x_type, std::int64_t incx, void *y, + library_data_t y_type, std::int64_t incy) { + sycl::queue q = desc_ptr->get_queue(); + std::uint64_t key = ::dpct::detail::get_type_combination_id(x_type, y_type); + switch (key) { + case ::dpct::detail::get_type_combination_id(library_data_t::real_float, + library_data_t::real_float): { + ::dpct::detail::copy_impl(q, n, x, incx, y, incy); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::real_double, + library_data_t::real_double): { + ::dpct::detail::copy_impl(q, n, x, incx, y, incy); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::complex_float, + library_data_t::complex_float): { + ::dpct::detail::copy_impl, std::complex>( + q, n, x, incx, y, incy); + break; + } + case ::dpct::detail::get_type_combination_id( + library_data_t::complex_double, library_data_t::complex_double): { + ::dpct::detail::copy_impl, std::complex>( + q, n, x, incx, y, incy); + break; + } + default: + throw std::runtime_error("the combination of data type is unsupported"); + } +} + +/// Swaps a vector with another vector. +/// \param [in] desc_ptr Descriptor. +/// \param [in] n Number of elements in vector x. +/// \param [in, out] x Input/Output vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [in, out] y Input/Output vector y. +/// \param [in] y_type Data type of the vector y. +/// \param [in] incy Stride of vector y. +inline void swap(descriptor_ptr desc_ptr, std::int64_t n, void *x, + library_data_t x_type, std::int64_t incx, void *y, + library_data_t y_type, std::int64_t incy) { + sycl::queue q = desc_ptr->get_queue(); + std::uint64_t key = ::dpct::detail::get_type_combination_id(x_type, y_type); + switch (key) { + case ::dpct::detail::get_type_combination_id(library_data_t::real_float, + library_data_t::real_float): { + ::dpct::detail::swap_impl(q, n, x, incx, y, incy); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::real_double, + library_data_t::real_double): { + ::dpct::detail::swap_impl(q, n, x, incx, y, incy); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::complex_float, + library_data_t::complex_float): { + ::dpct::detail::swap_impl, std::complex>( + q, n, x, incx, y, incy); + break; + } + case ::dpct::detail::get_type_combination_id( + library_data_t::complex_double, library_data_t::complex_double): { + ::dpct::detail::swap_impl, std::complex>( + q, n, x, incx, y, incy); + break; + } + default: + throw std::runtime_error("the combination of data type is unsupported"); + } +} + +/// Computes the sum of magnitudes of the vector elements. +/// \param [in] desc_ptr Descriptor. +/// \param [in] n Number of elements in vector x. +/// \param [in] x Input vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [out] result The scalar result. +/// \param [in] result_type Data type of \p result. +inline void asum(descriptor_ptr desc_ptr, std::int64_t n, const void *x, + library_data_t x_type, std::int64_t incx, void *result, + library_data_t result_type) { + sycl::queue q = desc_ptr->get_queue(); + std::uint64_t key = + ::dpct::detail::get_type_combination_id(x_type, result_type); + switch (key) { + case ::dpct::detail::get_type_combination_id(library_data_t::real_float, + library_data_t::real_float): { + ::dpct::detail::asum_impl(q, n, x, incx, result); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::real_double, + library_data_t::real_double): { + ::dpct::detail::asum_impl(q, n, x, incx, result); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::complex_float, + library_data_t::real_float): { + ::dpct::detail::asum_impl, float>(q, n, x, incx, + result); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::complex_double, + library_data_t::real_double): { + ::dpct::detail::asum_impl, double>(q, n, x, incx, + result); + break; + } + default: + throw std::runtime_error("the combination of data type is unsupported"); + } +} + +/// Finds the index of the element with the largest absolute value in a vector. +/// \param [in] desc_ptr Descriptor. +/// \param [in] n Number of elements in vector x. +/// \param [in] x Input vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [out] result The index of the maximal element. +inline void iamax(descriptor_ptr desc_ptr, std::int64_t n, const void *x, + library_data_t x_type, std::int64_t incx, + std::int64_t *result) { + sycl::queue q = desc_ptr->get_queue(); + std::uint64_t key = ::dpct::detail::get_type_combination_id(x_type); + switch (key) { + case ::dpct::detail::get_type_combination_id(library_data_t::real_float): { + ::dpct::detail::iamax_impl(q, n, x, incx, result); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::real_double): { + ::dpct::detail::iamax_impl(q, n, x, incx, result); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::complex_float): { + ::dpct::detail::iamax_impl>(q, n, x, incx, result); + break; + } + case ::dpct::detail::get_type_combination_id( + library_data_t::complex_double): { + ::dpct::detail::iamax_impl>(q, n, x, incx, result); + break; + } + default: + throw std::runtime_error("the combination of data type is unsupported"); + } +} + +/// Finds the index of the element with the smallest absolute value in a vector. +/// \param [in] desc_ptr Descriptor. +/// \param [in] n Number of elements in vector x. +/// \param [in] x Input vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [out] result The index of the minimum element. +inline void iamin(descriptor_ptr desc_ptr, std::int64_t n, const void *x, + library_data_t x_type, std::int64_t incx, + std::int64_t *result) { + sycl::queue q = desc_ptr->get_queue(); + std::uint64_t key = ::dpct::detail::get_type_combination_id(x_type); + switch (key) { + case ::dpct::detail::get_type_combination_id(library_data_t::real_float): { + ::dpct::detail::iamin_impl(q, n, x, incx, result); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::real_double): { + ::dpct::detail::iamin_impl(q, n, x, incx, result); + break; + } + case ::dpct::detail::get_type_combination_id(library_data_t::complex_float): { + ::dpct::detail::iamin_impl>(q, n, x, incx, result); + break; + } + case ::dpct::detail::get_type_combination_id( + library_data_t::complex_double): { + ::dpct::detail::iamin_impl>(q, n, x, incx, result); + break; + } + default: + throw std::runtime_error("the combination of data type is unsupported"); + } +} + +/// Finds the index of the element with the largest absolute value in a vector. +/// \param [in] desc_ptr Descriptor. +/// \param [in] n Number of elements in vector x. +/// \param [in] x Input vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [out] result The index of the maximal element. +inline void iamax(descriptor_ptr desc_ptr, std::int64_t n, const void *x, + library_data_t x_type, std::int64_t incx, int *result) { + dpct::blas::wrapper_int_to_int64_out wrapper(desc_ptr->get_queue(), result); + iamax(desc_ptr, n, x, x_type, incx, wrapper.get_ptr()); +} + +/// Finds the index of the element with the smallest absolute value in a vector. +/// \param [in] desc_ptr Descriptor. +/// \param [in] n Number of elements in vector x. +/// \param [in] x Input vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [out] result The index of the minimum element. +inline void iamin(descriptor_ptr desc_ptr, std::int64_t n, const void *x, + library_data_t x_type, std::int64_t incx, int *result) { + dpct::blas::wrapper_int_to_int64_out wrapper(desc_ptr->get_queue(), result); + iamin(desc_ptr, n, x, x_type, incx, wrapper.get_ptr()); +} + +/// Finds the least squares solutions for a batch of overdetermined linear +/// systems. Uses the QR factorization to solve a grouped batch of linear +/// systems with full rank matrices. +/// \param [in] desc_ptr Descriptor. +/// \param [in] trans Operation applied to \p a. +/// \param [in] m The number of rows of \p a. +/// \param [in] n The number of columns of \p a. +/// \param [in] nrhs The number of columns of \p b. +/// \param [in, out] a Array of pointers to matrices. +/// \param [in] lda The leading dimension of \p a. +/// \param [in, out] b Array of pointers to matrices. +/// \param [in] ldb The leading dimension of \p b. +/// \param [out] info Set to 0 if no error. +/// \param [out] dev_info Optional. If it is not NULL : dev_info[i]==0 means the +/// i-th problem is successful; dev_info[i]!=0 means dev_info[i] is the first +/// zero diagonal element of the i-th \p a . +/// \param [in] batch_size The size of the batch. +template +inline sycl::event +gels_batch_wrapper(descriptor_ptr desc_ptr, oneapi::mkl::transpose trans, int m, + int n, int nrhs, T *const a[], int lda, T *const b[], + int ldb, int *info, int *dev_info, int batch_size) { +#if defined(DPCT_USM_LEVEL_NONE) || !defined(__INTEL_MKL__) +#if defined(DPCT_USM_LEVEL_NONE) + throw std::runtime_error("this API is unsupported when USM level is none"); +#else + throw std::runtime_error("The oneAPI Math Kernel Library (oneMKL) Interfaces " + "Project does not support this API."); +#endif +#else + using Ty = typename ::dpct::detail::lib_data_traits_t; + sycl::queue exec_queue = desc_ptr->get_queue(); + struct matrix_info_t { + oneapi::mkl::transpose trans_info; + std::int64_t m_info; + std::int64_t n_info; + std::int64_t nrhs_info; + std::int64_t lda_info; + std::int64_t ldb_info; + std::int64_t group_size_info; + }; + matrix_info_t *matrix_info = + (matrix_info_t *)std::malloc(sizeof(matrix_info_t)); + matrix_info->trans_info = trans; + matrix_info->m_info = m; + matrix_info->n_info = n; + matrix_info->nrhs_info = nrhs; + matrix_info->lda_info = lda; + matrix_info->ldb_info = ldb; + matrix_info->group_size_info = batch_size; + std::int64_t scratchpad_size = + oneapi::mkl::lapack::gels_batch_scratchpad_size( + exec_queue, &(matrix_info->trans_info), &(matrix_info->m_info), + &(matrix_info->n_info), &(matrix_info->nrhs_info), + &(matrix_info->lda_info), &(matrix_info->ldb_info), 1, + &(matrix_info->group_size_info)); + Ty *scratchpad = sycl::malloc_device(scratchpad_size, exec_queue); + + *info = 0; + if (dev_info) + exec_queue.memset(dev_info, 0, batch_size * sizeof(int)); + static const std::vector empty_events{}; + static const std::string api_name = "oneapi::mkl::lapack::gels_batch"; + sycl::event e = ::dpct::detail::catch_batch_error_f( + nullptr, api_name, exec_queue, info, dev_info, batch_size, + oneapi::mkl::lapack::gels_batch, exec_queue, &(matrix_info->trans_info), + &(matrix_info->m_info), &(matrix_info->n_info), &(matrix_info->nrhs_info), + (Ty **)a, &(matrix_info->lda_info), (Ty **)b, &(matrix_info->ldb_info), + (std::int64_t)1, &(matrix_info->group_size_info), (Ty *)scratchpad, + (std::int64_t)scratchpad_size, empty_events); + + return exec_queue.submit([&](sycl::handler &cgh) { + cgh.host_task([=]() mutable { + ::dpct::detail::catch_batch_error( + nullptr, api_name, exec_queue, info, dev_info, + matrix_info->group_size_info, + [](sycl::event _e) { + _e.wait_and_throw(); + return 0; + }, + e); + std::free(matrix_info); + sycl::free(scratchpad, exec_queue); + }); + }); +#endif +} +} // namespace blas + +/// Computes matrix-matrix product with general matrices. +/// \param [in] q The queue where the routine should be executed. +/// \param [in] a_trans Specifies the operation applied to A. +/// \param [in] b_trans Specifies the operation applied to B. +/// \param [in] m Specifies the number of rows of the matrix op(A) and of the matrix C. +/// \param [in] n Specifies the number of columns of the matrix op(B) and of the matrix C. +/// \param [in] k Specifies the number of columns of the matrix op(A) and the number of rows of the matrix op(B). +/// \param [in] alpha Scaling factor for the matrix-matrix product. +/// \param [in] a Input matrix A. +/// \param [in] a_type Data type of the matrix A. +/// \param [in] lda Leading dimension of A. +/// \param [in] b Input matrix B. +/// \param [in] b_type Data type of the matrix B. +/// \param [in] ldb Leading dimension of B. +/// \param [in] beta Scaling factor for matrix C. +/// \param [in, out] c Input/Output matrix C. +/// \param [in] c_type Data type of the matrix C. +/// \param [in] ldc Leading dimension of C. +/// \param [in] scaling_type Data type of the scaling factors. +[[deprecated("Please use dpct::blas::gemm(...) instead.")]] inline void +gemm(sycl::queue &q, oneapi::mkl::transpose a_trans, + oneapi::mkl::transpose b_trans, int m, int n, int k, const void *alpha, + const void *a, library_data_t a_type, int lda, const void *b, + library_data_t b_type, int ldb, const void *beta, void *c, + library_data_t c_type, int ldc, library_data_t scaling_type) { + blas::descriptor desc; + desc.set_queue(&q); + blas::gemm(&desc, a_trans, b_trans, m, n, k, alpha, a, a_type, lda, b, b_type, + ldb, beta, c, c_type, ldc, scaling_type); +} + +/// Computes a batch of matrix-matrix product with general matrices. +/// \param [in] q The queue where the routine should be executed. +/// \param [in] a_trans Specifies the operation applied to A. +/// \param [in] b_trans Specifies the operation applied to B. +/// \param [in] m Specifies the number of rows of the matrix op(A) and of the matrix C. +/// \param [in] n Specifies the number of columns of the matrix op(B) and of the matrix C. +/// \param [in] k Specifies the number of columns of the matrix op(A) and the number of rows of the matrix op(B). +/// \param [in] alpha Scaling factor for the matrix-matrix product. +/// \param [in] a Input matrix A. +/// \param [in] a_type Data type of the matrix A. +/// \param [in] lda Leading dimension of A. +/// \param [in] b Input matrix B. +/// \param [in] b_type Data type of the matrix B. +/// \param [in] ldb Leading dimension of B. +/// \param [in] beta Scaling factor for matrix C. +/// \param [in, out] c Input/Output matrix C. +/// \param [in] c_type Data type of the matrix C. +/// \param [in] ldc Leading dimension of C. +/// \param [in] batch_size Specifies the number of matrix multiply operations to perform. +/// \param [in] scaling_type Data type of the scaling factors. +[[deprecated("Please use dpct::blas::gemm_batch(...) instead.")]] inline void +gemm_batch(sycl::queue &q, oneapi::mkl::transpose a_trans, + oneapi::mkl::transpose b_trans, int m, int n, int k, + const void *alpha, const void *a[], library_data_t a_type, int lda, + const void *b[], library_data_t b_type, int ldb, const void *beta, + void *c[], library_data_t c_type, int ldc, int batch_size, + library_data_t scaling_type) { + blas::descriptor desc; + desc.set_queue(&q); + blas::gemm_batch(&desc, a_trans, b_trans, m, n, k, alpha, a, a_type, lda, b, + b_type, ldb, beta, c, c_type, ldc, batch_size, scaling_type); +} + +/// Computes a batch of matrix-matrix product with general matrices. +/// \param [in] q The queue where the routine should be executed. +/// \param [in] a_trans Specifies the operation applied to A. +/// \param [in] b_trans Specifies the operation applied to B. +/// \param [in] m Specifies the number of rows of the matrix op(A) and of the matrix C. +/// \param [in] n Specifies the number of columns of the matrix op(B) and of the matrix C. +/// \param [in] k Specifies the number of columns of the matrix op(A) and the number of rows of the matrix op(B). +/// \param [in] alpha Scaling factor for the matrix-matrix product. +/// \param [in] a Input matrix A. +/// \param [in] a_type Data type of the matrix A. +/// \param [in] lda Leading dimension of A. +/// \param [in] stride_a Stride between the different A matrices. +/// \param [in] b Input matrix B. +/// \param [in] b_type Data type of the matrix B. +/// \param [in] ldb Leading dimension of B. +/// \param [in] stride_b Stride between the different B matrices. +/// \param [in] beta Scaling factor for matrix C. +/// \param [in, out] c Input/Output matrix C. +/// \param [in] c_type Data type of the matrix C. +/// \param [in] ldc Leading dimension of C. +/// \param [in] stride_c Stride between the different C matrices. +/// \param [in] batch_size Specifies the number of matrix multiply operations to perform. +/// \param [in] scaling_type Data type of the scaling factors. +[[deprecated("Please use dpct::blas::gemm_batch(...) instead.")]] inline void +gemm_batch(sycl::queue &q, oneapi::mkl::transpose a_trans, + oneapi::mkl::transpose b_trans, int m, int n, int k, + const void *alpha, const void *a, library_data_t a_type, int lda, + long long int stride_a, const void *b, library_data_t b_type, + int ldb, long long int stride_b, const void *beta, void *c, + library_data_t c_type, int ldc, long long int stride_c, + int batch_size, library_data_t scaling_type) { + blas::descriptor desc; + desc.set_queue(&q); + blas::gemm_batch(&desc, a_trans, b_trans, m, n, k, alpha, a, a_type, lda, + stride_a, b, b_type, ldb, stride_b, beta, c, c_type, ldc, + stride_c, batch_size, scaling_type); +} + +/// This routines perform a special rank-k update of a symmetric matrix C by +/// general matrices A and B. +/// \param [in] q The queue where the routine should be executed. +/// \param [in] uplo Specifies whether C's data is stored in its upper or lower triangle. +/// \param [in] trans Specifies the operation to apply. +/// \param [in] n The number of rows and columns in C. +/// \param [in] k The inner dimension of matrix multiplications. +/// \param [in] alpha Scaling factor for the rank-k update. +/// \param [in] a Input matrix A. +/// \param [in] lda Leading dimension of A. +/// \param [in] b Input matrix B. +/// \param [in] ldb Leading dimension of B. +/// \param [in] beta Scaling factor for the rank-k update. +/// \param [in, out] c Input/Output matrix C. +/// \param [in] ldc Leading dimension of C. +template +[[deprecated("Please use dpct::blas::syrk(...) instead.")]] inline void +syrk(sycl::queue &q, oneapi::mkl::uplo uplo, oneapi::mkl::transpose trans, + int n, int k, const T *alpha, const T *a, int lda, const T *b, int ldb, + const T *beta, T *c, int ldc) { + blas::descriptor desc; + desc.set_queue(&q); + blas::syrk(&desc, uplo, trans, n, k, alpha, a, lda, b, ldb, beta, c, ldc); +} + +/// This routines perform a special rank-k update of a Hermitian matrix C by +/// general matrices A and B. +/// \param [in] q The queue where the routine should be executed. +/// \param [in] uplo Specifies whether C's data is stored in its upper or lower triangle. +/// \param [in] trans Specifies the operation to apply. +/// \param [in] n The number of rows and columns in C. +/// \param [in] k The inner dimension of matrix multiplications. +/// \param [in] alpha Scaling factor for the rank-k update. +/// \param [in] a Input matrix A. +/// \param [in] lda Leading dimension of A. +/// \param [in] b Input matrix B. +/// \param [in] ldb Leading dimension of B. +/// \param [in] beta Scaling factor for the rank-k update. +/// \param [in, out] c Input/Output matrix C. +/// \param [in] ldc Leading dimension of C. +template +[[deprecated("Please use dpct::blas::herk(...) instead.")]] inline void +herk(sycl::queue &q, oneapi::mkl::uplo uplo, oneapi::mkl::transpose trans, + int n, int k, const T *alpha, const T *a, int lda, const T *b, int ldb, + const Tbeta *beta, T *c, int ldc) { + blas::descriptor desc; + desc.set_queue(&q); + blas::herk(&desc, uplo, trans, n, k, alpha, a, lda, b, ldb, beta, c, ldc); +} + +/// This routine performs a group of trsm operations. Each trsm solves an +/// equation of the form op(A) * X = alpha * B or X * op(A) = alpha * B. +/// \param [in] q The queue where the routine should be executed. +/// \param [in] left_right Specifies A multiplies X on the left or on the right. +/// \param [in] upper_lower Specifies A is upper or lower triangular. +/// \param [in] trans Specifies the operation applied to A. +/// \param [in] unit_diag Specifies whether A is unit triangular. +/// \param [in] m Number of rows of the B matrices. +/// \param [in] n Number of columns of the B matrices. +/// \param [in] alpha Scaling factor for the solutions. +/// \param [in] a Input matrices A. +/// \param [in] a_type Data type of the matrices A. +/// \param [in] lda Leading dimension of the matrices A. +/// \param [in, out] b Input and output matrices B. +/// \param [in] b_type Data type of the matrices B. +/// \param [in] ldb Leading dimension of the matrices B. +/// \param [in] batch_size Specifies the number of trsm operations to perform. +/// \param [in] scaling_type Data type of the scaling factors. +[[deprecated("Please use dpct::blas::trsm_batch(...) instead.")]] inline void +trsm_batch(sycl::queue &q, oneapi::mkl::side left_right, + oneapi::mkl::uplo upper_lower, oneapi::mkl::transpose trans, + oneapi::mkl::diag unit_diag, int m, int n, const void *alpha, + const void **a, library_data_t a_type, int lda, void **b, + library_data_t b_type, int ldb, int batch_size, + library_data_t scaling_type) { + blas::descriptor desc; + desc.set_queue(&q); + blas::trsm_batch(&desc, left_right, upper_lower, trans, unit_diag, m, n, + alpha, a, a_type, lda, b, b_type, ldb, batch_size, + scaling_type); +} + +/// Computes a triangular matrix-general matrix product. +/// \param [in] q The queue where the routine should be executed. +/// \param [in] left_right Specifies A is on the left or right side of the +/// multiplication. +/// \param [in] upper_lower Specifies A is upper or lower triangular. +/// \param [in] trans Specifies the operation applied to A. +/// \param [in] unit_diag Specifies whether A is unit triangular. +/// \param [in] m Number of rows of B. +/// \param [in] n Number of columns of B. +/// \param [in] alpha Scaling factor for the matrix-matrix product. +/// \param [in] a Input matrices A. +/// \param [in] lda Leading dimension of the matrices A. +/// \param [in] b Input matrices B. +/// \param [in] ldb Leading dimension of the matrices B. +/// \param [out] c Output matrices C. +/// \param [in] ldc Leading dimension of the matrices C. +template +[[deprecated("Please use dpct::blas::trmm(...) instead.")]] inline void +trmm(sycl::queue &q, oneapi::mkl::side left_right, + oneapi::mkl::uplo upper_lower, oneapi::mkl::transpose trans, + oneapi::mkl::diag unit_diag, int m, int n, const T *alpha, const T *a, + int lda, const T *b, int ldb, T *c, int ldc) { + blas::descriptor desc; + desc.set_queue(&q); + blas::trmm(&desc, left_right, upper_lower, trans, unit_diag, m, n, alpha, + a, lda, b, ldb, c, ldc); +} + +/// Computes the Euclidean norm of a vector. +/// \param [in] q The queue where the routine should be executed. +/// \param [in] n Number of elements in vector x. +/// \param [in] x Input vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [out] result The result scalar. +/// \param [in] result_type Data type of the result. +[[deprecated("Please use dpct::blas::nrm2(...) instead.")]] inline void +nrm2(sycl::queue &q, int n, const void *x, library_data_t x_type, int incx, + void *result, library_data_t result_type) { + blas::descriptor desc; + desc.set_queue(&q); + blas::nrm2(&desc, n, x, x_type, incx, result, result_type); +} + +/// Computes the dot product of two vectors. +/// \param [in] q The queue where the routine should be executed. +/// \param [in] n Number of elements in vector x. +/// \param [in] x Input vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [in] y Input vector y. +/// \param [in] y_type Data type of the vector y. +/// \param [in] incy Stride of vector y. +/// \param [out] result The result scalar. +/// \param [in] result_type Data type of the result. +[[deprecated("Please use dpct::blas::dot(...) instead.")]] inline void +dot(sycl::queue &q, int n, const void *x, library_data_t x_type, int incx, + const void *y, library_data_t y_type, int incy, void *result, + library_data_t result_type) { + blas::descriptor desc; + desc.set_queue(&q); + blas::dot(&desc, n, x, x_type, incx, y, y_type, incy, result, result_type); +} + +/// Computes the dot product of two vectors, conjugating the first vector. +/// \param [in] q The queue where the routine should be executed. +/// \param [in] n Number of elements in vector x. +/// \param [in] x Input vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [in] y Input vector y. +/// \param [in] y_type Data type of the vector y. +/// \param [in] incy Stride of vector y. +/// \param [out] result The result scalar. +/// \param [in] result_type Data type of the result. +[[deprecated("Please use dpct::blas::dotc(...) instead.")]] inline void +dotc(sycl::queue &q, int n, const void *x, library_data_t x_type, int incx, + const void *y, library_data_t y_type, int incy, void *result, + library_data_t result_type) { + blas::descriptor desc; + desc.set_queue(&q); + blas::dotc(&desc, n, x, x_type, incx, y, y_type, incy, result, result_type); +} + +/// Computes the product of a vector by a scalar. +/// \param [in] q The queue where the routine should be executed. +/// \param [in] n Number of elements in vector x. +/// \param [in] alpha The scale factor alpha. +/// \param [in] alpha_type The data type of alpha. +/// \param [in, out] x Input/Output vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +[[deprecated("Please use dpct::blas::scal(...) instead.")]] inline void +scal(sycl::queue &q, int n, const void *alpha, library_data_t alpha_type, + void *x, library_data_t x_type, int incx) { + blas::descriptor desc; + desc.set_queue(&q); + blas::scal(&desc, n, alpha, alpha_type, x, x_type, incx); +} + +/// Computes a vector-scalar product and adds the result to a vector. +/// \param [in] q The queue where the routine should be executed. +/// \param [in] n Number of elements in vector x. +/// \param [in] alpha The scale factor alpha. +/// \param [in] alpha_type The data type of alpha. +/// \param [in] x Input vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [in, out] y Input/Output vector y. +/// \param [in] y_type Data type of the vector y. +/// \param [in] incy Stride of vector y. +[[deprecated("Please use dpct::blas::axpy(...) instead.")]] inline void +axpy(sycl::queue &q, int n, const void *alpha, library_data_t alpha_type, + const void *x, library_data_t x_type, int incx, void *y, + library_data_t y_type, int incy) { + blas::descriptor desc; + desc.set_queue(&q); + blas::axpy(&desc, n, alpha, alpha_type, x, x_type, incx, y, y_type, incy); +} + +/// Performs rotation of points in the plane. +/// \param [in] q The queue where the routine should be executed. +/// \param [in] n Number of elements in vector x. +/// \param [in, out] x Input/Output vector x. +/// \param [in] x_type Data type of the vector x. +/// \param [in] incx Stride of vector x. +/// \param [in, out] y Input/Output vector y. +/// \param [in] y_type Data type of the vector y. +/// \param [in] incy Stride of vector y. +/// \param [in] c Scaling factor. +/// \param [in] s Scaling factor. +/// \param [in] cs_type Data type of the scaling factors. +[[deprecated("Please use dpct::blas::rot(...) instead.")]] inline void +rot(sycl::queue &q, int n, void *x, library_data_t x_type, int incx, void *y, + library_data_t y_type, int incy, const void *c, const void *s, + library_data_t cs_type) { + blas::descriptor desc; + desc.set_queue(&q); + blas::rot(&desc, n, x, x_type, incx, y, y_type, incy, c, s, cs_type); +} +} // namespace dpct +#undef DPCT_COMPUTE_MODE_ARG +#undef DPCT_COMPUTE_MODE_PARAM +#endif // __DPCT_BLAS_UTILS_HPP__ diff --git a/sycl/include/dpct/ccl_utils.hpp b/sycl/include/dpct/ccl_utils.hpp new file mode 100644 index 0000000000..b965567900 --- /dev/null +++ b/sycl/include/dpct/ccl_utils.hpp @@ -0,0 +1,266 @@ +//==---- ccl_utils.hpp----------------------------*- C++ -*----------------==// +// +// Copyright (C) Intel Corporation +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// See https://llvm.org/LICENSE.txt for license information. +// +//===----------------------------------------------------------------------===// + +#ifndef __DPCT_CCL_UTILS_HPP__ +#define __DPCT_CCL_UTILS_HPP__ + +#include "compat_service.hpp" + +#include + +#include +#include + +#include "detail/ccl_utils_detail.hpp" + +namespace dpct { +namespace ccl { + +/// Get concatenated library version as an integer. +static inline int get_version() { + oneapi::ccl::init(); + auto ver = oneapi::ccl::get_library_version(); + return ver.major * 10000 + ver.minor * 100 + ver.update; +} + +/// Create main kvs and return its address. +static inline oneapi::ccl::kvs::address_type create_kvs_address() { + oneapi::ccl::init(); + auto ptr = oneapi::ccl::create_main_kvs(); + auto addr = ptr->get_address(); + detail::get_kvs(addr) = ptr; + return addr; +} + +/// Get stored kvs with /p addr if exist. Otherwise, create kvs with /p addr. +static inline std::shared_ptr +create_kvs(const oneapi::ccl::kvs::address_type &addr) { + oneapi::ccl::init(); + auto &ptr = detail::get_kvs(addr); + if (!ptr) + ptr = oneapi::ccl::create_kvs(addr); + return ptr; +} + +/// dpct communicator extension +class communicator_wrapper : public dpct::ccl::detail::ccl_init_helper { +public: + communicator_wrapper( + int size, int rank, oneapi::ccl::kvs::address_type id, + const oneapi::ccl::comm_attr &attr = oneapi::ccl::default_comm_attr) + : _device_comm(oneapi::ccl::create_device( + static_cast(::dpct::cs::get_current_device()))), + _context_comm( + oneapi::ccl::create_context(::dpct::cs::get_default_context())), + _comm(oneapi::ccl::create_communicator( + size, rank, _device_comm, _context_comm, dpct::ccl::create_kvs(id), + attr)) { + _queue_init = false; + _ccl_stream_ptr = nullptr; + } + + ~communicator_wrapper() { + delete _ccl_stream_ptr; + }; + + /// Return the rank in a oneapi::ccl::communicator + /// \returns The rank corresponding to communicator object + int rank() const { + return _comm.rank(); + } + + /// Retrieves the number of rank in oneapi::ccl::communicator + /// \returns The number of the ranks + int size() const { + return _comm.size(); + } + + /// Return underlying native device, which was used in oneapi::ccl::communicator + sycl::device get_device() const { + return _comm.get_device().get_native(); + } + + /// \brief allreduce is a collective communication operation that performs the global reduction operation + /// on values from all ranks of communicator and distributes the result back to all ranks. + /// \param sendbuff the buffer with @c count elements of @c dtype that stores local data to be reduced + /// \param recvbuff [out] the buffer to store reduced result, must have the same dimension as @c sendbuff + /// \param count the number of elements of type @c dtype in @c sendbuff and @c recvbuff + /// \param dtype the datatype of elements in @c sendbuff and @c recvbuff + /// \param rtype the type of the reduction operation to be applied + /// \param queue_ptr a sycl::queue ptr associated with the operation + /// \return @ref void + void allreduce(const void *sendbuff, void *recvbuff, size_t count, + oneapi::ccl::datatype dtype, oneapi::ccl::reduction rtype, + sycl::queue *queue_ptr) { + call_func_wrapper( + [=](const oneapi::ccl::stream &stream) { + return oneapi::ccl::allreduce(sendbuff, recvbuff, count, dtype, rtype, + _comm, stream); + }, + queue_ptr); + } + + /// \brief reduce is a collective communication operation that performs the + /// global reduction operation on values from all ranks of the communicator + /// and returns the result to the root rank. + /// \param sendbuff the buffer with @c count elements of @c dtype that stores + /// local data to be reduced + /// \param recvbuff [out] the buffer to store reduced result, + /// must have the same dimension as @c sendbuff + /// \param count the number of elements of type @c dtype in @c sendbuff and @c recvbuff + /// \param dtype the datatype of elements in @c sendbuff and @c recvbuff + /// \param root the rank that gets the result of reduction + /// \param rtype the type of the reduction operation to be applied + /// \param queue_ptr a sycl::queue ptr associated with the operation + /// \return @ref void + void reduce(const void *sendbuff, void *recvbuff, size_t count, + oneapi::ccl::datatype dtype, oneapi::ccl::reduction rtype, + int root, sycl::queue *queue_ptr) { + call_func_wrapper( + [=](const oneapi::ccl::stream &stream) { + return oneapi::ccl::reduce(sendbuff, recvbuff, count, dtype, rtype, + root, _comm, stream); + }, + queue_ptr); + } + + /// \brief broadcast is a collective communication operation that broadcasts data + /// from one rank of communicator (denoted as root) to all other ranks. + /// Only support in-place operation + /// \param sendbuff the buffer with @c count elements of @c dtype that stores + /// local data to be reduced + /// \param recvbuff [out] the buffer to store reduced result + /// \param count the number of elements of type @c dtype in @c buf + /// \param dtype thedatatype of elements in @c buf + /// \param root the rank that broadcasts @c buf + /// \param queue_ptr a sycl::queue ptr associated with the operation + /// \return @ref void + void broadcast(void *sendbuff, void *recvbuff, size_t count, + oneapi::ccl::datatype dtype, int root, + sycl::queue *queue_ptr) { + if (sendbuff != recvbuff) { + throw std::runtime_error( + "oneCCL broadcast only support in-place operation. " + "sendbuff and recvbuff must be same."); + return; + } + call_func_wrapper( + [=](const oneapi::ccl::stream &stream) { + return oneapi::ccl::broadcast(recvbuff, count, dtype, root, _comm, + stream); + }, + queue_ptr); + } + + /// \brief reduce_scatter is a collective communication operation that performs the global reduction operation + /// on values from all ranks of the communicator and scatters the result in blocks back to all ranks. + /// \param sendbuff the buffer with @c count elements of @c dtype that stores local data to be reduced + /// \param recvbuff [out] the buffer to store reduced result, must have the same dimension as @c sendbuff + /// \param recv_count the number of elements of type @c dtype in receive block + /// \param dtype the datatype of elements in @c sendbuff and @c recvbuff + /// \param rtype the type of the reduction operation to be applied + /// \param queue_ptr a sycl::queue ptr associated with the operation + /// \return @ref void + void reduce_scatter(const void *sendbuff, void *recvbuff, size_t recv_count, + oneapi::ccl::datatype dtype, oneapi::ccl::reduction rtype, + sycl::queue *queue_ptr) { + call_func_wrapper( + [=](const oneapi::ccl::stream &stream) { + return oneapi::ccl::reduce_scatter(sendbuff, recvbuff, recv_count, + dtype, rtype, _comm, stream); + }, + queue_ptr); + } + + /// \brief send is a pt2pt communication operation that sends data from one rank of communicator. + /// \param sendbuff the buffer with @c count elements of @c dtype serves as send buffer for root + /// \param count the number of elements of type @c dtype in @c sendbuff + /// \param dtype the datatype of elements in @c sendbuff + /// \param peer the rank that receives @c sendbuff + /// \param queue_ptr a sycl::queue ptr associated with the operation + /// \return @ref void + void send(void *sendbuff, size_t count, oneapi::ccl::datatype dtype, int peer, + sycl::queue *queue_ptr) { + call_func_wrapper( + [=](const oneapi::ccl::stream &stream) { + return oneapi::ccl::send(sendbuff, count, dtype, peer, _comm, stream); + }, + queue_ptr); + } + + /// \brief recv is a pt2pt communication operation that sends data from one rank of communicator. + /// \param recvbuff the buffer with @c count elements of @c dtype serves as receive buffer + /// \param count the number of elements of type @c dtype in @c recvbuff + /// \param dtype the datatype of elements in @c recvbuff + /// \param peer the rank that receives @c recvbuff + /// \param queue_ptr a sycl::queue ptr associated with the operation + /// \return @ref void + void recv(void *recvbuff, size_t count, oneapi::ccl::datatype dtype, int peer, + sycl::queue *queue_ptr) { + call_func_wrapper( + [=](const oneapi::ccl::stream &stream) { + return oneapi::ccl::recv(recvbuff, count, dtype, peer, _comm, stream); + }, + queue_ptr); + } + +private: + oneapi::ccl::device _device_comm; + oneapi::ccl::context _context_comm; + oneapi::ccl::communicator _comm; + sycl::queue _queue; + bool _queue_init; + oneapi::ccl::stream *_ccl_stream_ptr; + + template + void call_func_wrapper(Fn func, sycl::queue *qptr) { + if (_queue_init && *qptr != _queue) { + call_func_async(func, qptr); + } else { + if(!_queue_init) { + _queue = *qptr; + _queue_init = true; + _ccl_stream_ptr = new oneapi::ccl::stream(oneapi::ccl::create_stream(_queue)); + } + std::invoke(func, *_ccl_stream_ptr); + } + } + + class call_func_async { + sycl::queue *_q_ptr; + struct call_async_impl { + oneapi::ccl::stream _ccl_stream_impl; + oneapi::ccl::event _ccl_event_impl; + template + explicit call_async_impl(Fn func, sycl::queue *qptr) + : _ccl_stream_impl(oneapi::ccl::create_stream(*qptr)), + _ccl_event_impl(std::invoke(func, _ccl_stream_impl)) {} + }; + call_async_impl *_imp; + + public: + template + explicit call_func_async(Fn func, sycl::queue *qptr) + : _q_ptr(qptr), + _imp(new call_async_impl(func, qptr)) {} + ~call_func_async() { + _q_ptr->submit([&](sycl::handler &cgh) + { cgh.host_task([=] + { + _imp->_ccl_event_impl.wait(); + delete _imp; }); }); + } + }; +}; + +typedef dpct::ccl::communicator_wrapper *comm_ptr; + +} // namespace ccl +} // namespace dpct + +#endif // __DPCT_CCL_UTILS_HPP__ \ No newline at end of file diff --git a/sycl/include/dpct/codepin/codepin.hpp b/sycl/include/dpct/codepin/codepin.hpp new file mode 100644 index 0000000000..59ccd65319 --- /dev/null +++ b/sycl/include/dpct/codepin/codepin.hpp @@ -0,0 +1,383 @@ +//==---- codepin.hpp -------------------------*- C++ -*------------------==// +// +// Copyright (C) Intel Corporation +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// See https://llvm.org/LICENSE.txt for license information. +// +//===-------------------------------------------------------------------===// +#ifndef __DPCT_CODEPIN_HPP__ +#define __DPCT_CODEPIN_HPP__ + +#include "serialization/basic.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Random seed for data sampling. +#ifndef CODEPIN_RAND_SEED +#define CODEPIN_RAND_SEED 0 +#endif + +// Data size threshold to trigger data sampling. +// array/pointer size larger than the threshold will be sampled. +#ifndef CODEPIN_SAMPLING_THRESHOLD +#define CODEPIN_SAMPLING_THRESHOLD 20 +#endif + +// Sampling percent, interval: [0, 100] +// 0: No data will be logged. +// 100: all data will be logged. +#ifndef CODEPIN_SAMPLING_PERCENT +#define CODEPIN_SAMPLING_PERCENT 1 +#endif + +#define CODEPIN_TO_STR(x) CODEPIN_STR(x) +#define CODEPIN_STR(x) #x +#pragma message( \ + "CodePin data sampling feature is enabled for data dump. As follow list 3 configs for data sampling:") +#pragma message("CODEPIN_RAND_SEED: " CODEPIN_TO_STR(CODEPIN_RAND_SEED)) +#pragma message("CODEPIN_SAMPLING_THRESHOLD: " CODEPIN_TO_STR(CODEPIN_SAMPLING_THRESHOLD)) +#pragma message("CODEPIN_SAMPLING_PERCENT: " CODEPIN_TO_STR(CODEPIN_SAMPLING_PERCENT)) +#pragma message( \ + "Define the macros in the build command to change sampling configs. Also refer to codepin.hpp for definitions and default value of the macros.") + +namespace dpct { +namespace experimental { +namespace codepin { + +inline static std::map api_index; +inline static std::map event_map; +inline static bool rand_seed_setup = false; +namespace detail { + +inline static std::unordered_set ptr_unique; + +class logger { +public: + logger(const std::string &dump_file) + : opf(dump_file), json_ss(opf), arr(json_ss) { + auto top_obj = arr.object(); + top_obj.key("CodePin Random Seed"); + top_obj.value(CODEPIN_RAND_SEED); + top_obj.key("CodePin Sampling Threshold"); + top_obj.value(CODEPIN_SAMPLING_THRESHOLD); + top_obj.key("CodePin Sampling Percent"); + top_obj.value(CODEPIN_SAMPLING_PERCENT); + } + ~logger() {} + + detail::json_stringstream &get_stringstream() { + return this->json_ss; + } + + template + void print_CP(const std::string &cp_id, std::string device_name, + size_t free_byte, size_t total_byte, float elapse_time, + queue_t queue, Args... args) { + ptr_unique.clear(); + auto obj = arr.object(); + obj.key("ID"); + obj.value(cp_id); + obj.key("Device Name"); + obj.value(device_name); + obj.key("Device ID"); +#ifdef __NVCC__ + int device_id; + cudaGetDevice(&device_id); + obj.value(device_id); +#else + obj.value((int)dpct::get_current_device_id()); +#endif + obj.key("Stream Address"); + obj.value((void *)queue); + obj.key("Free Device Memory"); + obj.value(free_byte); + obj.key("Total Device Memory"); + obj.value(total_byte); + obj.key("Elapse Time(ms)"); + obj.value(elapse_time); + obj.key("CheckPoint"); + auto cp_obj = + obj.value(); + print_args(cp_obj, queue, 0, args...); + } + + void print_args(json_stringstream::json_obj &obj, queue_t queue, + int index = 0) {} + + template + void print_args(json_stringstream::json_obj &obj, queue_t queue, int index, + std::string_view arg_name, First &arg, RestArgs... args) { + obj.key(arg_name); + { + auto type_obj = + obj.value(); + detail::data_ser::print_type_name(type_obj); + type_obj.key("Address"); + print_address(type_obj, arg); + type_obj.key("Index"); + type_obj.value(index); + type_obj.key("Data"); + detail::data_ser::dump(json_ss, arg, queue); + } + print_args(obj, queue, index + 1, args...); + } + template + void print_address(json_stringstream::json_obj &obj, ArgT arg) { + if constexpr (std::is_pointer::value) { + obj.value((void *)arg); + } else { + obj.value((void *)&arg); + } + } + +private: + std::ofstream opf; + detail::json_stringstream json_ss; + detail::json_stringstream::json_array arr; +}; + +#ifdef __NVCC__ +inline std::string data_file_prefix = "CodePin_CUDA_"; +#else +inline std::string data_file_prefix = "CodePin_SYCL_"; +#endif + +inline std::string get_data_file_name(std::string_view data_file_prefix) { + std::time_t now_time = std::time(nullptr); + std::stringstream strs; + strs << data_file_prefix + << std::put_time(std::localtime(&now_time), "%Y-%m-%d_%H-%M-%S") + << ".json"; + return strs.str(); +} + +inline logger log(get_data_file_name(data_file_prefix)); + +inline std::map &get_ptr_size_map() { + static std::map ptr_size_map; + return ptr_size_map; +} + +inline uint32_t get_ptr_size_in_bytes(void *ptr) { + const std::map &ptr_size_map = get_ptr_size_map(); + const auto &it = ptr_size_map.find(ptr); + return (it != ptr_size_map.end()) ? it->second : 0; +} + +template +class data_ser { +public: + static void dump(detail::json_stringstream &ss, T* value, + queue_t queue) { + using PointeeType = std::remove_cv_t>; + PointeeType *non_const_value = const_cast(value); + if (ptr_unique.find(non_const_value) != ptr_unique.end()) { + return; + } + ptr_unique.insert(non_const_value); + int size = get_ptr_size_in_bytes(non_const_value); + size = size == 0 ? 1 : size / sizeof(*value); + PointeeType *dump_addr = non_const_value; + bool is_dev = is_dev_ptr(non_const_value); + if (is_dev) { + PointeeType *h_data = new PointeeType[size]; +#ifdef __NVCC__ + cudaMemcpyAsync(h_data, value, size * sizeof(PointeeType), + cudaMemcpyDeviceToHost, queue); + cudaStreamSynchronize(queue); +#else + queue->memcpy((void *)h_data, (void *)value, size * sizeof(PointeeType)) + .wait(); +#endif + dump_addr = h_data; + } + auto arr = ss.array(); + for (int i = 0; i < size; ++i) { + if (size > CODEPIN_SAMPLING_THRESHOLD && i != 0) { + float r = static_cast(rand()) / static_cast(RAND_MAX); + if (r > (float)CODEPIN_SAMPLING_PERCENT/(float)100) + continue; + } + auto obj = arr.object(); + detail::data_ser::print_type_name(obj); + obj.key("Data"); + detail::data_ser::dump( + ss, *(dump_addr + i), queue); + } + if(is_dev) + delete[] dump_addr; + } + static void print_type_name( + detail::json_stringstream::json_obj &obj) { + obj.key("Type"); + obj.value("Pointer"); + } +}; + +template +class data_ser::value>::type> { +public: + static void dump(detail::json_stringstream &ss, T value, + queue_t queue) { + auto arr = ss.array(); + size_t size = sizeof(T) / sizeof(value[0]); + for (size_t i = 0; i < size; ++i) { + if (size > CODEPIN_SAMPLING_THRESHOLD && i != 0) { + float r = static_cast(rand()) / static_cast(RAND_MAX); + if (r > (float)CODEPIN_SAMPLING_PERCENT/(float)100) + continue; + } + auto obj = arr.object(); + detail::data_ser< + std::remove_extent_t>::print_type_name(obj); + obj.key("Data"); + detail::data_ser>::dump( + ss, value[i], queue); + } + } + static void print_type_name( + detail::json_stringstream::json_obj &obj) { + obj.key("Type"); + obj.value("Array"); + } +}; + +template +void gen_log_API_CP(const std::string &cp_id, std::string device_name, + size_t free_byte, size_t total_byte, float elapse_time, + queue_t queue, Args... args) { + if (!rand_seed_setup) { + srand(CODEPIN_RAND_SEED); + rand_seed_setup = true; + std::cout << "CodePin data sampling is enabled for data dump. As follow list 3 " + "configs for data sampling:" + << std::endl; + std::cout << "CODEPIN_RAND_SEED: " << CODEPIN_RAND_SEED << std::endl; + std::cout << "CODEPIN_SAMPLING_THRESHOLD: " << CODEPIN_SAMPLING_THRESHOLD + << std::endl; + std::cout << "CODEPIN_SAMPLING_PERCENT: " << CODEPIN_SAMPLING_PERCENT + << std::endl; + } + log.print_CP(cp_id, device_name, free_byte, total_byte, elapse_time, queue, + args...); +} +} // namespace detail + +#ifdef __NVCC__ +inline void synchronize(cudaStream_t stream) { cudaStreamSynchronize(stream); } +#else +inline void synchronize(sycl::queue *q) { q->wait(); } +#endif + +/// Generate API check point prolog. +/// \param cp_id The UID of the function call. +/// \param queue The sycl queue to synchronize the command execution. +/// \param args The var name string and variable value pair list. +template +void gen_prolog_API_CP(const std::string &cp_id, + queue_t queue, Args&&... args) { + synchronize(queue); + std::string prolog_tag = cp_id + ":" + "prolog"; + if (api_index.find(cp_id) == api_index.end()) { + api_index[cp_id] = 0; + } else { + api_index[cp_id]++; + } + std::string event_id = + cp_id + ":" + std::to_string(api_index[cp_id]); + size_t free_byte, total_byte; +#ifdef __NVCC__ + int device; + cudaGetDevice(&device); + cudaDeviceProp deviceProp; + cudaGetDeviceProperties(&deviceProp, device); + std::string device_name(deviceProp.name); + cudaMemGetInfo(&free_byte, &total_byte); + cudaEvent_t event; + cudaEventCreate(&event); + cudaEventRecord(event, queue); + event_map[event_id] = event; +#else + dpct::get_current_device().get_memory_info(free_byte, total_byte); + std::string device_name = dpct::get_current_device().get_info(); +#ifdef DPCT_PROFILING_ENABLED + sycl::event event = queue->ext_oneapi_submit_barrier(); + event_map[event_id] = event; +#endif //DPCT_PROFILING_ENABLED +#endif + + detail::gen_log_API_CP(prolog_tag, device_name, free_byte, total_byte, 0.0f, + queue, args...); +} + +/// Generate API check point epilog. +/// \param cp_id The UID of the function call. +/// \param stream The sycl queue to synchronize the command execution. +/// \param args The var name string and variable value pair list. +template +void gen_epilog_API_CP(const std::string &cp_id, + queue_t queue, Args&&... args) { + synchronize(queue); + std::string epilog_tag = cp_id + ":" + "epilog"; + std::string event_id = + cp_id + ":" + std::to_string(api_index[cp_id]); + size_t free_byte, total_byte; + float kernel_elapsed_time = 0.0f; +#ifdef __NVCC__ + int device; + cudaGetDevice(&device); + cudaDeviceProp deviceProp; + cudaGetDeviceProperties(&deviceProp, device); + std::string device_name(deviceProp.name); + cudaMemGetInfo(&free_byte, &total_byte); + cudaEvent_t event; + cudaEventCreate(&event); + cudaEventRecord(event, queue); + auto pre_event = event_map[event_id]; + event_map.erase(event_id); + cudaEventSynchronize(event); + cudaEventElapsedTime(&kernel_elapsed_time, pre_event, event); +#else +#ifdef DPCT_PROFILING_ENABLED + sycl::event event = queue->ext_oneapi_submit_barrier(); + auto pre_event = event_map[event_id]; + event_map.erase(event_id); + event.wait_and_throw(); + kernel_elapsed_time = + (event.get_profiling_info() - + pre_event + .get_profiling_info()) / + 1000000.0f; +#endif //DPCT_PROFILING_ENABLED + std::string device_name = dpct::get_current_device().get_info(); + dpct::get_current_device().get_memory_info(free_byte, total_byte); +#endif + detail::gen_log_API_CP(epilog_tag, device_name, free_byte, total_byte, + kernel_elapsed_time, queue, args...); +} + +inline std::map &get_ptr_size_map() { + return detail::get_ptr_size_map(); +} +} // namespace codepin +} // namespace experimental +} // namespace dpct + +namespace dpctexp = dpct::experimental; + + +#endif // End of __DPCT_CODEPIN_HPP__ diff --git a/sycl/include/dpct/codepin/serialization/basic.hpp b/sycl/include/dpct/codepin/serialization/basic.hpp new file mode 100644 index 0000000000..6be886bfc2 --- /dev/null +++ b/sycl/include/dpct/codepin/serialization/basic.hpp @@ -0,0 +1,501 @@ +//==----------- basic.hpp ----------------------------*-C++-*-------------==// +// +// Copyright (C) Intel Corporation +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// See https://llvm.org/LICENSE.txt for license information. +// +//===--------------------------------------------------------------------===// +#ifndef __DPCT_CODEPIN_SER_BASIC_HPP__ +#define __DPCT_CODEPIN_SER_BASIC_HPP__ + +#if defined(__linux__) +#include +#endif +#include +#include +#include +#include +#ifdef __NVCC__ +#include +#include +#include +#else +#include +#include +#endif + +namespace dpct { +namespace experimental { +namespace codepin { + +#ifdef __NVCC__ +typedef cudaStream_t queue_t; +#else +typedef dpct::queue_ptr queue_t; +#endif + +#ifdef __NVCC__ +typedef cudaEvent_t event_t; +#else +typedef sycl::event event_t; +#endif + +namespace detail { + +inline bool is_dev_ptr(void *p) { +#ifdef __NVCC__ + cudaPointerAttributes attr; + cudaPointerGetAttributes(&attr, p); + if (attr.type == cudaMemoryTypeDevice) + return true; + return false; +#else + dpct::pointer_attributes attributes; + attributes.init(p); + if (attributes.get_device_pointer() != nullptr) + return true; + return false; +#endif +} + +class json_stringstream { + public: + json_stringstream(std::ofstream &ofst) : os(ofst) { + if (!ofst.is_open()) { + throw std::runtime_error("Error while openning file: "); + } + } + +private: + std::string indent; + const size_t tab_length = 2; + std::ofstream &os; + +#if defined(__linux__) + const std::string newline = "\n"; +#elif defined(_WIN64) + const std::string newline = "\r\n"; +#else +#error Only support windows and Linux. +#endif + + +public: + class json_obj { + bool isFirst = true; + json_stringstream &js; + private: + friend class json_stringstream; + json_obj(json_stringstream &json_ss) : js(json_ss) { + js.os << "{" << js.newline; + js.indent.append(js.tab_length, ' '); + js.os << js.indent; + } + + public: + template T value(); + + void key(std::string_view key) { + if (!isFirst){ + js.os << "," << js.newline << js.indent; + } else { + isFirst = false; + } + js.os << "\"" << key << "\": "; + }; + + void value(std::string_view value) { js.os << "\"" << value << "\""; }; + void value(float value) { js.os << "\"" << value << "\""; }; + void value(size_t value) { js.os << "\"" << value << "\""; }; + void value(int value) { js.os << "\"" << value << "\""; }; + void value(void *value) { js.os << "\"" << value << "\""; }; + ~json_obj() { + js.indent.resize(js.indent.size() - js.tab_length); + js.os << js.newline; + js.os << js.indent; + js.os << "}"; + } + }; + + class json_array { + bool isFirst = true; + json_stringstream &js; + public: + json_array(json_stringstream &json_ss) : js(json_ss) { + if(!(js.os)) + return; + js.os << "[" << js.newline; + js.indent.append(js.tab_length, ' '); + js.os << js.indent; + } + + json_obj object() { + if(!isFirst){ + js.os << "," << js.newline << js.indent; + } else { + isFirst = false; + } + return json_obj(js); + } + + template + void member(const MemberT &t) { + if(!isFirst){ + js.os << "," << js.newline << js.indent; + } else { + isFirst = false; + } + js.os << t; + } + + ~json_array() { + js.indent.resize(js.indent.size() - js.tab_length); + js.os << js.newline; + js.os << js.indent; + js.os << "]"; + } + }; + + template >>> + json_stringstream &operator<<(T &&value) { + os << std::forward(value); + return *this; + } + + json_obj object(){ + return json_obj(*this); + } + json_array array(){ + return json_array(*this); + } +}; + +template <> +inline json_stringstream::json_obj +json_stringstream::json_obj::value() { + return js.object(); +} + +template inline std::string demangle_name() { + std::string ret_str = ""; +#if defined(__linux__) + int s; + auto mangle_name = typeid(T).name(); + auto demangle_name = abi::__cxa_demangle(mangle_name, NULL, NULL, &s); + if (s != 0) { + ret_str = "CODEPIN:ERROR:0: Unable to demangle symbol " + + std::string(mangle_name) + "."; + } else { + ret_str = demangle_name; + std::free(demangle_name); + } +#else + ret_str = typeid(T).name(); +#endif + return ret_str; +} + +#ifdef __NVCC__ +template <> inline std::string demangle_name<__half>() { return "fp16"; } +template <> inline std::string demangle_name<__nv_bfloat16>() { return "bf16"; } +#else +template <> inline std::string demangle_name() { return "fp16"; } +template <> inline std::string demangle_name() { + return "bf16"; +} +#endif + +template class data_ser { + +public: + static void dump(json_stringstream &ss, T value, queue_t queue) { + auto obj = ss.object(); + obj.key("Data"); + obj.value("CODEPIN:ERROR:1: Unable to find the corresponding serialization " + "function."); + } + static void print_type_name(json_stringstream::json_obj &obj) { + obj.key("Type"); + obj.value(std::string(demangle_name())); + } +}; + +template +class data_ser::value>::type> { +public: + static void dump(json_stringstream &ss, const T &value, queue_t queue) { + auto arr = ss.array(); + arr.member(value); + } + static void print_type_name(json_stringstream::json_obj &obj) { + obj.key("Type"); + obj.value(std::string(demangle_name())); + } +}; + +#ifdef __NVCC__ +template <> class data_ser<__half> { +public: + static void dump(json_stringstream &ss, const __half &value, queue_t queue) { + float f = __half2float(value); + auto arr = ss.array(); + arr.member(value); + } + static void print_type_name(json_stringstream::json_obj &obj) { + obj.key("Type"); + obj.value(std::string(demangle_name<__half>())); + } +}; +template <> class data_ser<__nv_bfloat16> { +public: + static void dump(json_stringstream &ss, const __nv_bfloat16 &value, + queue_t queue) { + float f = __bfloat162float(value); + auto arr = ss.array(); + arr.member(value); + } + static void print_type_name(json_stringstream::json_obj &obj) { + obj.key("Type"); + obj.value(std::string(demangle_name<__nv_bfloat16>())); + } +}; +#else +template +class data_ser::value || + std::is_same::value>::type> { +public: + static void dump(json_stringstream &ss, const T &value, queue_t queue) { + auto arr = ss.array(); + arr.member(value); + } + static void print_type_name(json_stringstream::json_obj &obj) { + obj.key("Type"); + obj.value(std::string(demangle_name())); + } + +}; +#endif + +#ifdef __NVCC__ +template <> class data_ser { +public: + static void dump(json_stringstream &ss, const int3 &value, + queue_t queue) { + auto arr = ss.array(); + { + auto obj_x = arr.object(); + obj_x.key("x"); + auto value_x = + obj_x + .value(); + data_ser::print_type_name(value_x); + value_x.key("Data"); + data_ser::dump(ss, value.x, queue); + } + { + auto obj_y = arr.object(); + obj_y.key("y"); + auto value_y = + obj_y + .value(); + data_ser::print_type_name(value_y); + value_y.key("Data"); + data_ser::dump(ss, value.y, queue); + } + { + auto obj_z = arr.object(); + obj_z.key("z"); + auto value_z = + obj_z + .value(); + data_ser::print_type_name(value_z); + value_z.key("Data"); + data_ser::dump(ss, value.z, queue); + } + } + static void print_type_name(json_stringstream::json_obj &obj){ + obj.key("Type"); + obj.value("int3"); + } +}; + +template <> class data_ser { +public: + static void dump(json_stringstream &ss, const float3 &value, + queue_t queue) { + auto arr = ss.array(); + { + auto obj_x = arr.object(); + obj_x.key("x"); + auto value_x = + obj_x + .value(); + data_ser::print_type_name(value_x); + value_x.key("Data"); + data_ser::dump(ss, value.x, queue); + } + { + auto obj_y = arr.object(); + obj_y.key("y"); + auto value_y = + obj_y + .value(); + data_ser::print_type_name(value_y); + value_y.key("Data"); + data_ser::dump(ss, value.y, queue); + } + { + auto obj_z = arr.object(); + obj_z.key("z"); + auto value_z = + obj_z + .value(); + data_ser::print_type_name(value_z); + value_z.key("Data"); + data_ser::dump(ss, value.z, queue); + } + } + static void print_type_name(json_stringstream::json_obj &obj){ + obj.key("Type"); + obj.value("float3"); + } +}; + +#else +template <> class data_ser { +public: + static void dump(json_stringstream &ss, const sycl::int3 &value, + queue_t queue) { + auto arr = ss.array(); + { + auto obj_x = arr.object(); + obj_x.key("x"); + auto value_x = + obj_x + .value(); + data_ser::print_type_name(value_x); + value_x.key("Data"); + data_ser::dump(ss, value.x(), queue); + } + { + auto obj_y = arr.object(); + obj_y.key("y"); + auto value_y = + obj_y + .value(); + data_ser::print_type_name(value_y); + value_y.key("Data"); + data_ser::dump(ss, value.y(), queue); + } + { + auto obj_z = arr.object(); + obj_z.key("z"); + auto value_z = + obj_z + .value(); + data_ser::print_type_name(value_z); + value_z.key("Data"); + data_ser::dump(ss, value.z(), queue); + } + } + static void print_type_name(json_stringstream::json_obj &obj){ + obj.key("Type"); + obj.value("sycl::int3"); + } +}; + +template <> class data_ser { +public: + static void dump(json_stringstream &ss, const sycl::float3 &value, + queue_t queue) { + auto arr = ss.array(); + { + auto obj_x = arr.object(); + obj_x.key("x"); + auto value_x = + obj_x + .value(); + data_ser::print_type_name(value_x); + value_x.key("Data"); + data_ser::dump(ss, value.x(), queue); + } + { + auto obj_y = arr.object(); + obj_y.key("y"); + auto value_y = + obj_y + .value(); + data_ser::print_type_name(value_y); + value_y.key("Data"); + data_ser::dump(ss, value.y(), queue); + } + { + auto obj_z = arr.object(); + obj_z.key("z"); + auto value_z = + obj_z + .value(); + data_ser::print_type_name(value_z); + value_z.key("Data"); + data_ser::dump(ss, value.z(), queue); + } + } + static void print_type_name(json_stringstream::json_obj &obj){ + obj.key("Type"); + obj.value("sycl::float3"); + } +}; +#endif + +template <> class data_ser { +public: + static void dump(json_stringstream &ss, const char *value, + queue_t queue) { + auto obj = ss.object(); + obj.key("Data"); + const char *dump_addr = value; + bool is_dev = is_dev_ptr((void*)value); + if (is_dev) { + const char *h_data = new char[strlen(value)]; +#ifdef __NVCC__ + cudaMemcpyAsync((void *)h_data, (void *)value, + strlen(value) * sizeof(char), cudaMemcpyDeviceToHost, + queue); + cudaStreamSynchronize(queue); +#else + queue->memcpy((void *)h_data, (void *)value, strlen(value) * sizeof(char)) + .wait(); +#endif + dump_addr = h_data; + } + obj.value(std::string(dump_addr)); + } + static void print_type_name(json_stringstream::json_obj &obj){ + obj.key("Type"); + obj.value("char *"); + } +}; + +template <> class data_ser { +public: + static void dump(json_stringstream &ss, const std::string &value, + queue_t queue) { + auto obj = ss.object(); + obj.key("Data"); + obj.value(value); + } + static void print_type_name(json_stringstream::json_obj &obj){ + obj.key("Type"); + obj.value("std::string"); + } +}; + +} // namespace detail +} // namespace codepin +} // namespace experimental +} // namespace dpct + +#endif diff --git a/sycl/include/dpct/compat_service.hpp b/sycl/include/dpct/compat_service.hpp new file mode 100644 index 0000000000..c0d571ec8d --- /dev/null +++ b/sycl/include/dpct/compat_service.hpp @@ -0,0 +1,122 @@ +//==---- compat_service.hpp -----------------------*- C++ -*----------------==// +// +// Copyright (C) Intel Corporation +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// See https://llvm.org/LICENSE.txt for license information. +// +//===----------------------------------------------------------------------===// +// This file contains some compatibility service APIs which are used by all +// library utility helper functions. The compatibility service APIs are based on +// either dpct implementation or syclcompat implementation, which is controlled +// by the macro USE_DPCT_HELPER. +//===----------------------------------------------------------------------===// + +#ifndef __DPCT_COMPAT_SERVICE_HPP__ +#define __DPCT_COMPAT_SERVICE_HPP__ + +namespace dpct { +namespace cs { +#if USE_DPCT_HELPER +namespace ns = ::dpct; +using memcpy_direction = ::dpct::memcpy_direction; +template using kernel_name = dpct_kernel_name; +using byte_t = ::dpct::byte_t; +#else +namespace ns = ::syclcompat; +using memcpy_direction = ::syclcompat::detail::memcpy_direction; +template using kernel_name = syclcompat_kernel_name; +#ifndef __dpct_inline__ +#define __dpct_inline__ __syclcompat_inline__ +#endif +using byte_t = ::syclcompat::byte_t; +#endif + +using ns::get_current_device; +using ns::get_default_context; +using ns::queue_ptr; + +namespace detail { +using ns::detail::get_pointer_attribute; +using ns::detail::pointer_access_attribute; +} // namespace detail + +inline sycl::queue &get_default_queue() { +#if USE_DPCT_HELPER + return ::dpct::get_default_queue(); +#else + return *::syclcompat::detail::dev_mgr::instance() + .current_device() + .default_queue(); +#endif +} + +inline sycl::event +memcpy(sycl::queue &q, void *to_ptr, const void *from_ptr, size_t size, + memcpy_direction direction = memcpy_direction::automatic, + const std::vector &dep_events = {}) { +#if USE_DPCT_HELPER + return ::dpct::detail::dpct_memcpy(q, to_ptr, from_ptr, size, direction, + dep_events); +#else + return ::syclcompat::detail::memcpy(q, to_ptr, from_ptr, size, dep_events); +#endif +} + +inline std::vector +memcpy(sycl::queue &q, void *to_ptr, const void *from_ptr, size_t to_pitch, + size_t from_pitch, size_t x, size_t y, + memcpy_direction direction = memcpy_direction::automatic, + const std::vector &dep_events = {}) { +#if USE_DPCT_HELPER + return ::dpct::detail::dpct_memcpy( + q, to_ptr, from_ptr, sycl::range<3>(to_pitch, y, 1), + sycl::range<3>(from_pitch, y, 1), sycl::id<3>(0, 0, 0), + sycl::id<3>(0, 0, 0), sycl::range<3>(x, y, 1), direction, dep_events); +#else + return ::syclcompat::detail::memcpy( + q, to_ptr, from_ptr, sycl::range<3>(to_pitch, y, 1), + sycl::range<3>(from_pitch, y, 1), sycl::id<3>(0, 0, 0), + sycl::id<3>(0, 0, 0), sycl::range<3>(x, y, 1), dep_events); +#endif +} + +inline void *malloc(size_t size, sycl::queue q = get_default_queue()) { +#if USE_DPCT_HELPER + return ::dpct::dpct_malloc(size, q); +#else + return ::syclcompat::malloc(size, q); +#endif +} + +template +inline sycl::event fill(sycl::queue &q, void *dev_ptr, valueT value, + size_t size) { +#if USE_DPCT_HELPER + return ::dpct::detail::dpct_memset(q, dev_ptr, value, size); +#else + return ::syclcompat::detail::fill(q, dev_ptr, value, size); +#endif +} + +inline void free(void *to_ptr, sycl::queue q) { +#if USE_DPCT_HELPER + return ::dpct::detail::dpct_free(to_ptr, q); +#else + return ::syclcompat::free(to_ptr, q); +#endif +} + +inline sycl::event enqueue_free(const std::vector &pointers, + const std::vector &events, + sycl::queue q = get_default_queue()) { +#if USE_DPCT_HELPER + return ::dpct::detail::async_dpct_free(pointers, events, q); +#else + return ::syclcompat::enqueue_free(pointers, events, q); +#endif +} + +} // namespace cs +} // namespace dpct + +#endif // __DPCT_COMPAT_SERVICE_HPP__ diff --git a/sycl/include/dpct/detail/blas_gemm_utils_detail.hpp b/sycl/include/dpct/detail/blas_gemm_utils_detail.hpp new file mode 100644 index 0000000000..4506803143 --- /dev/null +++ b/sycl/include/dpct/detail/blas_gemm_utils_detail.hpp @@ -0,0 +1,426 @@ +//==---- blas_gemm_utils_detail.hpp---------------------*- C++ -*-----------==// +// +// Copyright (C) Intel Corporation +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// See https://llvm.org/LICENSE.txt for license information. +// +//===----------------------------------------------------------------------===// + +#ifndef __DPCT_BLAS_GEMM_UTILS_DETAIL_HPP__ +#define __DPCT_BLAS_GEMM_UTILS_DETAIL_HPP__ + +namespace dpct::blas_gemm::experimental::detail { +/// Sacling each row of matrix D with the corresponding element of vector alpha. +template +sycl::event scale_new_a_impl(::dpct::cs::queue_ptr q_ptr, int rows, int cols, + void *a_ptr, const void *alpha_ptr, + bool vector_alpha, bool device_alpha, + const void *a_scale_ptr, const void *b_scale_ptr, + const std::vector &dependencies) { + std::vector deps = dependencies; + T *a = (T *)a_ptr; + Tscale *alpha = (Tscale *)alpha_ptr; + Tscale *a_scale = (Tscale *)a_scale_ptr; + Tscale *b_scale = (Tscale *)b_scale_ptr; + + if (!device_alpha) { + Tscale *alpha_host = alpha; + alpha = (Tscale *)::dpct::cs::malloc(sizeof(Tscale), *q_ptr); + deps.push_back( + ::dpct::cs::memcpy(*q_ptr, alpha, alpha_host, sizeof(Tscale))); + } + if (!a_scale_ptr) { + a_scale = (Tscale *)::dpct::cs::malloc(sizeof(Tscale), *q_ptr); + deps.push_back(::dpct::cs::fill(*q_ptr, a_scale, 1.0, 1)); + } + if (!b_scale_ptr) { + b_scale = (Tscale *)::dpct::cs::malloc(sizeof(Tscale), *q_ptr); + deps.push_back(::dpct::cs::fill(*q_ptr, b_scale, 1.0, 1)); + } + + sycl::event e = q_ptr->submit([&](sycl::handler &cgh) { + cgh.depends_on(deps); +#ifdef DPCT_USM_LEVEL_NONE + access_wrapper a_acc(a, cgh); + access_wrapper alpha_acc(alpha, cgh); + access_wrapper a_scale_acc(a_scale, cgh); + access_wrapper b_scale_acc(b_scale, cgh); +#endif + cgh.parallel_for< + ::dpct::cs::kernel_name>( + sycl::range<2>(rows, cols), [=](sycl::id<2> index) { +#ifdef DPCT_USM_LEVEL_NONE + T *a_data = a_acc.get_raw_pointer(); + Tscale *alpha_data = alpha_acc.get_raw_pointer(); + Tscale *a_scale_data = a_scale_acc.get_raw_pointer(); + Tscale *b_scale_data = b_scale_acc.get_raw_pointer(); +#else + T *a_data = a; + const Tscale *alpha_data = alpha; + const Tscale *a_scale_data = a_scale; + const Tscale *b_scale_data = b_scale; +#endif + size_t row_idx = index.get(0); + size_t col_idx = index.get(1); + size_t idx = rows * col_idx + row_idx; + + Tscale ab_scale = a_scale_data[0] * b_scale_data[0]; + + if (vector_alpha) + a_data[idx] = a_data[idx] * alpha_data[row_idx] * ab_scale; + else + a_data[idx] = a_data[idx] * alpha_data[0] * ab_scale; + }); + }); + return q_ptr->submit([&](sycl::handler &cgh) { + cgh.depends_on(e); + cgh.host_task([=] { + if (!device_alpha) + ::dpct::cs::free(alpha, *q_ptr); + if (!a_scale_ptr) + ::dpct::cs::free(a_scale, *q_ptr); + if (!b_scale_ptr) + ::dpct::cs::free(b_scale, *q_ptr); + }); + }); +} + +// a is col major without padding +inline sycl::event scale_new_a(::dpct::cs::queue_ptr q_ptr, int rows, int cols, + void *a, library_data_t a_type, + const void *alpha, library_data_t scale_type, + bool vector_alpha, bool device_alpha, + const void *a_scale, const void *b_scale, + const std::vector &deps) { + std::uint64_t key = dpct::detail::get_type_combination_id(a_type, scale_type); + sycl::event e; + switch (key) { +#define __SCALE_NEW_A_IMPL_CASE(A_TYPE_ENUM, SCALE_TYPE_ENUM, A_TYPE, \ + SCALE_TYPE) \ + case dpct::detail::get_type_combination_id( \ + library_data_t::A_TYPE_ENUM, library_data_t::SCALE_TYPE_ENUM): { \ + e = scale_new_a_impl(q_ptr, rows, cols, a, alpha, \ + vector_alpha, device_alpha, \ + a_scale, b_scale, deps); \ + break; \ + } + __SCALE_NEW_A_IMPL_CASE(real_int8, real_float, std::int8_t, float) + __SCALE_NEW_A_IMPL_CASE(real_int32, real_float, int, float) + __SCALE_NEW_A_IMPL_CASE(real_int8, real_int32, std::int8_t, int) + __SCALE_NEW_A_IMPL_CASE(real_float, real_float, float, float) +#undef __SCALE_NEW_A_IMPL_CASE + default: + throw std::runtime_error("dpct::blas_gemm::experimental::detail::scale_new_" + "a_impl() does not support the data " + "type combination currently."); + } + return e; +} + +/// Get a linear idx map for a 2D point (row_idx, col_idx) between src_order and +/// dst_order. +inline std::tuple +get_linear_idx_map(size_t rows, size_t cols, size_t src_ld, order_t src_order, + size_t dst_ld, order_t dst_order, size_t row_idx, + size_t col_idx) { +#define COMBINE(from, to) \ + static_cast(from) << 8 | static_cast(to) + + size_t from_linear_idx, to_linear_idx; + switch (COMBINE(src_order, dst_order)) { + case COMBINE(order_t::col, order_t::row): { + from_linear_idx = src_ld * col_idx + row_idx; + to_linear_idx = dst_ld * row_idx + col_idx; + break; + } + case COMBINE(order_t::row, order_t::col): { + from_linear_idx = src_ld * row_idx + col_idx; + to_linear_idx = dst_ld * col_idx + row_idx; + break; + } + case COMBINE(order_t::col, order_t::col32): { + from_linear_idx = src_ld * col_idx + row_idx; + to_linear_idx = dst_ld * (col_idx / 32) + 32 * row_idx + col_idx % 32; + break; + } + case COMBINE(order_t::col32, order_t::col): { + from_linear_idx = src_ld * (col_idx / 32) + 32 * row_idx + col_idx % 32; + to_linear_idx = dst_ld * col_idx + row_idx; + break; + } + case COMBINE(order_t::col, order_t::col4_4r2_8c): { + from_linear_idx = src_ld * col_idx + row_idx; + + size_t from_row_in_row8_col32 = row_idx % 8; + size_t from_col_in_row8_col32 = col_idx % 32; + + size_t to_row_in_row8_col32 = + 4 * (from_row_in_row8_col32 % 2) + from_col_in_row8_col32 / 8; + size_t to_col_in_row8_col32 = 16 * ((from_col_in_row8_col32 / 4) % 2) + + 4 * (from_row_in_row8_col32 / 2) + + from_col_in_row8_col32 % 4; + size_t to_linear_idx_in_row8_col32 = + to_row_in_row8_col32 * 32 + to_col_in_row8_col32; + + to_linear_idx = dst_ld * (col_idx / 32) + (row_idx / 8) * (32 * 8) + + to_linear_idx_in_row8_col32; + break; + } + case COMBINE(order_t::col4_4r2_8c, order_t::col): { + to_linear_idx = dst_ld * col_idx + row_idx; + + size_t to_row_in_row8_col32 = row_idx % 8; + size_t to_col_in_row8_col32 = col_idx % 32; + + size_t from_row_in_row8_col32 = + 4 * (to_row_in_row8_col32 % 2) + to_col_in_row8_col32 / 8; + size_t from_col_in_row8_col32 = 16 * ((to_col_in_row8_col32 / 4) % 2) + + 4 * (to_row_in_row8_col32 / 2) + + to_col_in_row8_col32 % 4; + size_t from_linear_idx_in_row8_col32 = + from_row_in_row8_col32 * 32 + from_col_in_row8_col32; + + from_linear_idx = src_ld * (col_idx / 32) + (row_idx / 8) * (32 * 8) + + from_linear_idx_in_row8_col32; + break; + } + case COMBINE(order_t::col, order_t::col32_2r_4r4): { + from_linear_idx = src_ld * col_idx + row_idx; + + size_t from_row_in_row32_col32 = row_idx % 32; + size_t from_col_in_row32_col32 = col_idx % 32; + + size_t to_row_in_row32_col32 = 8 * ((from_row_in_row32_col32 % 8) / 2) + + (from_row_in_row32_col32 / 8) * 2 + + from_row_in_row32_col32 % 2; + size_t to_col_in_row32_col32 = from_col_in_row32_col32; + size_t to_linear_idx_in_row32_col32 = + to_row_in_row32_col32 * 32 + to_col_in_row32_col32; + + to_linear_idx = dst_ld * (col_idx / 32) + (row_idx / 32) * (32 * 32) + + to_linear_idx_in_row32_col32; + break; + } + case COMBINE(order_t::col32_2r_4r4, order_t::col): { + to_linear_idx = dst_ld * col_idx + row_idx; + + size_t to_row_in_row32_col32 = row_idx % 32; + size_t to_col_in_row32_col32 = col_idx % 32; + + size_t from_row_in_row32_col32 = 8 * ((to_row_in_row32_col32 % 8) / 2) + + (to_row_in_row32_col32 / 8) * 2 + + to_row_in_row32_col32 % 2; + size_t from_col_in_row32_col32 = to_col_in_row32_col32; + size_t from_linear_idx_in_row32_col32 = + from_row_in_row32_col32 * 32 + from_col_in_row32_col32; + + from_linear_idx = src_ld * (col_idx / 32) + (row_idx / 32) * (32 * 32) + + from_linear_idx_in_row32_col32; + break; + } + } +#undef COMBINE + return std::make_tuple(from_linear_idx, to_linear_idx); +} + +template