-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCMakeLists.txt
More file actions
318 lines (286 loc) · 16.7 KB
/
Copy pathCMakeLists.txt
File metadata and controls
318 lines (286 loc) · 16.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
cmake_minimum_required(VERSION 3.20)
project(tensorlib LANGUAGES CXX)
# This project's own build (tests/benches) compiles at C++23. The public
# headers, however, only require C++17 (inline variables, std::optional,
# structured bindings — nothing from C++20/23); consumers may include them at
# C++17+. The INTERFACE target does NOT force a standard on consumers. The one
# C++23-ish dependency, metal.h's #embed, is macOS-only and unreached elsewhere.
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(TENSORLIB_CUDA "Build the CUDA backend (needs CUDA Toolkit at build time only)" OFF)
add_library(tensorlib INTERFACE)
target_include_directories(tensorlib INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
if(APPLE)
target_link_libraries(tensorlib INTERFACE
"-framework Accelerate" "-framework Metal" "-framework Foundation")
endif()
if(TENSORLIB_CUDA)
# The CUDA backend does NOT link CUDA — the driver is loaded at runtime
# (dlopen on Unix, LoadLibrary(nvcuda.dll) on Windows; see cuda.h) and kernels
# run as PTX JIT'd by the driver. So nvcc is used only to compile
# kernels/tensorlib_cuda.cu to PTX, which bin2c turns into a byte array the
# header #includes (off-Apple compilers predate C23 #embed). We deliberately
# DON'T enable_language(CUDA): that probes the full host-compiler CUDA
# toolchain and fails on Windows without the Visual Studio CUDA integration.
# Locating nvcc directly is all the PTX step needs. (On Windows, nvcc still
# invokes cl.exe for preprocessing, so cl.exe must be on PATH — the CI uses
# ilammy/msvc-dev-cmd; locally, run from a Developer Command Prompt.)
find_program(TL_NVCC NAMES nvcc
HINTS ENV CUDA_PATH ENV CUDA_HOME /usr/local/cuda
PATH_SUFFIXES bin)
if(NOT TL_NVCC)
message(FATAL_ERROR
"TENSORLIB_CUDA=ON but nvcc was not found. Add it to PATH or set CUDA_PATH.")
endif()
# PTX virtual arch: compute_80 PTX JITs onto any sm_80+ device (the RTX 3090
# is sm_86), so one PTX blob is portable across recent GPUs.
if(NOT TENSORLIB_CUDA_PTX_ARCH)
set(TENSORLIB_CUDA_PTX_ARCH sm_80)
endif()
set(TL_CUDA_PTX ${CMAKE_BINARY_DIR}/tensorlib_cuda.ptx)
set(TL_CUDA_INC ${CMAKE_BINARY_DIR}/tensorlib_cuda_ptx.inc)
set(TL_NVCC_ARGS -arch=${TENSORLIB_CUDA_PTX_ARCH} -ptx)
if(WIN32)
# nvcc's host-compiler version gate hard-errors (C1189) on a Visual Studio
# newer than its supported window (VS 2017-2022) — e.g. VS 18 on an updated
# runner or dev box. PTX is device code (the host compiler only
# preprocesses), so skipping the gate is safe. No-op where the VS is in
# range; the CI pins windows-2022 (in range), and this additionally lets a
# newer local toolchain build -DTENSORLIB_CUDA=ON.
list(APPEND TL_NVCC_ARGS -allow-unsupported-compiler)
endif()
add_custom_command(
OUTPUT ${TL_CUDA_PTX}
COMMAND ${TL_NVCC} ${TL_NVCC_ARGS}
${CMAKE_CURRENT_SOURCE_DIR}/kernels/tensorlib_cuda.cu -o ${TL_CUDA_PTX}
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/kernels/tensorlib_cuda.cu
COMMENT "nvcc: tensorlib_cuda.cu -> PTX (${TENSORLIB_CUDA_PTX_ARCH})"
VERBATIM)
add_custom_command(
OUTPUT ${TL_CUDA_INC}
COMMAND ${CMAKE_COMMAND} -DINPUT=${TL_CUDA_PTX} -DOUTPUT=${TL_CUDA_INC}
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/bin2c.cmake
DEPENDS ${TL_CUDA_PTX}
COMMENT "bin2c: PTX -> tensorlib_cuda_ptx.inc"
VERBATIM)
add_custom_target(tensorlib_cuda_ptx DEPENDS ${TL_CUDA_INC})
# Everything a CUDA-enabled consumer target needs: the generated .inc on the
# include path, the TENSORLIB_CUDA switch, and libdl for dlopen.
function(tensorlib_enable_cuda tgt)
add_dependencies(${tgt} tensorlib_cuda_ptx)
target_include_directories(${tgt} PRIVATE ${CMAKE_BINARY_DIR})
target_compile_definitions(${tgt} PRIVATE TENSORLIB_CUDA)
target_link_libraries(${tgt} PRIVATE ${CMAKE_DL_LIBS})
endfunction()
endif()
enable_testing()
add_executable(tensorlib_test test/test_basic.cpp test/test_array.cpp)
target_link_libraries(tensorlib_test PRIVATE tensorlib)
if(TENSORLIB_CUDA)
# The test suite's gpu/auto modes now exercise the real CUDA backend (the
# gpu:: facade routes to cuda off-Apple), falling back to CPU with no device.
tensorlib_enable_cuda(tensorlib_test)
# Kernel-level CUDA oracle (like check_cpu_ukernel for the CPU backend):
# every op vs a CPU reference, direct on the cuda:: API. Only built/run when
# CUDA is enabled.
add_executable(tensorlib_check_cuda bench/cuda/check/check_cuda.cpp)
target_link_libraries(tensorlib_check_cuda PRIVATE tensorlib)
tensorlib_enable_cuda(tensorlib_check_cuda)
add_test(NAME cuda_ukernel COMMAND tensorlib_check_cuda)
# Real-MNIST convergence gate — asserts the GPU op stack (hand-rolled backprop)
# drives a 784-256-10 ReLU MLP to a real test-accuracy target. Skips-as-pass
# with no GPU (CI) or no data (offline); fetches MNIST once via curl into the
# build dir. A few seconds on-device.
add_executable(tensorlib_check_mnist bench/shared/check/check_mnist.cpp)
target_link_libraries(tensorlib_check_mnist PRIVATE tensorlib)
tensorlib_enable_cuda(tensorlib_check_mnist)
add_test(NAME mnist_converge COMMAND tensorlib_check_mnist
${CMAKE_BINARY_DIR}/mnist-data)
# M7 bf16-weight decode GEMV bench (Path B). Direct cuda:: API, no cudart —
# times bf16 vs f32 weight storage on the memory-bound decode GEMV. Needs the
# project cuda.h before the CUDA TK's driver header (same BEFORE-include idiom
# as bench_cuda_gemm).
add_executable(tensorlib_bench_bf16 bench/cuda/speed/bench_bf16_gemv.cpp)
target_include_directories(tensorlib_bench_bf16 BEFORE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(tensorlib_bench_bf16 PRIVATE tensorlib)
tensorlib_enable_cuda(tensorlib_bench_bf16)
add_executable(tensorlib_bench_qwen_gemv bench/cuda/speed/bench_qwen_gemv.cpp)
target_include_directories(tensorlib_bench_qwen_gemv BEFORE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(tensorlib_bench_qwen_gemv PRIVATE tensorlib)
tensorlib_enable_cuda(tensorlib_bench_qwen_gemv)
# M9 fused decode-attention bench (Path-B style). Direct cuda:: API.
add_executable(tensorlib_bench_attn bench/cuda/speed/bench_attn_decode.cpp)
target_include_directories(tensorlib_bench_attn BEFORE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(tensorlib_bench_attn PRIVATE tensorlib)
tensorlib_enable_cuda(tensorlib_bench_attn)
# M9 end-to-end: multi-layer llama decode loop wired to the persistent
# kv_cache (GQA) via the array<->native bridge, verified vs a CPU reference at
# every step. Uses the array API + tl::cuda::kv_cache, so CUDA-build only.
add_executable(tensorlib_check_llm_decode bench/cuda/check/check_llm_decode.cpp)
target_include_directories(tensorlib_check_llm_decode BEFORE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(tensorlib_check_llm_decode PRIVATE tensorlib)
tensorlib_enable_cuda(tensorlib_check_llm_decode)
add_test(NAME llm_decode COMMAND tensorlib_check_llm_decode)
# M9 head_dim generalization: the attention/KV kernels are templated on
# head_dim {64,128}; this validates the D=64 instantiations (Qwen2 head_dim)
# against the same CPU references. Direct cuda:: API, CUDA-build only.
add_executable(tensorlib_check_attn64 bench/cuda/check/check_attn64.cpp)
target_include_directories(tensorlib_check_attn64 BEFORE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(tensorlib_check_attn64 PRIVATE tensorlib)
tensorlib_enable_cuda(tensorlib_check_attn64)
add_test(NAME attn64 COMMAND tensorlib_check_attn64)
# M9 "actually chat": load a real Qwen2.5-0.5B GGUF (F16) and run the decoder
# end to end vs a tight numpy reference on the same weights. Not a ctest (needs
# the ~1.2 GB model file, which lives outside the repo). Run manually.
add_executable(tensorlib_check_qwen bench/cuda/check/check_qwen.cpp)
target_include_directories(tensorlib_check_qwen BEFORE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/bench/cuda)
target_link_libraries(tensorlib_check_qwen PRIVATE tensorlib)
tensorlib_enable_cuda(tensorlib_check_qwen)
# End-to-end chat: own GGUF loader + own BPE tokenizer + own CUDA kernels.
# prompt string -> generated text. Not a ctest (needs the external model).
add_executable(tensorlib_chat_qwen bench/cuda/chat_qwen.cpp)
target_include_directories(tensorlib_chat_qwen BEFORE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/bench/cuda)
target_link_libraries(tensorlib_chat_qwen PRIVATE tensorlib)
tensorlib_enable_cuda(tensorlib_chat_qwen)
# M9 decode-overhead census: per-region wall-clock breakdown of one decode
# step (StepProf). Locates the 6.7x gap to llama.cpp. Not a ctest (external
# model file); short/isolated per the WSL2 sysmem-cliff discipline.
add_executable(tensorlib_bench_qwen_decode bench/cuda/speed/bench_qwen_decode.cpp)
target_include_directories(tensorlib_bench_qwen_decode BEFORE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/bench/cuda)
target_link_libraries(tensorlib_bench_qwen_decode PRIVATE tensorlib)
tensorlib_enable_cuda(tensorlib_bench_qwen_decode)
# M9 decode ctx-scaling census: per-token cost as the KV cache grows, for the
# imperative and CUDA-graph paths, plus the isolated per-layer attention cost
# (split-KV vs the graph's pinned S=1 grid). Not a ctest (external model).
add_executable(tensorlib_bench_qwen_ctx bench/cuda/speed/bench_qwen_ctx.cpp)
target_include_directories(tensorlib_bench_qwen_ctx BEFORE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/bench/cuda)
target_link_libraries(tensorlib_bench_qwen_ctx PRIVATE tensorlib)
tensorlib_enable_cuda(tensorlib_bench_qwen_ctx)
# M9 prefill census: which existing token-by-token path prefills fastest, and
# how much batching the prompt (M>1 projections + tiled attention) would buy.
# Not a ctest (external model).
add_executable(tensorlib_bench_qwen_prefill bench/cuda/speed/bench_qwen_prefill.cpp)
target_include_directories(tensorlib_bench_qwen_prefill BEFORE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/bench/cuda)
target_link_libraries(tensorlib_bench_qwen_prefill PRIVATE tensorlib)
tensorlib_enable_cuda(tensorlib_bench_qwen_prefill)
# M8 int4-weight decode GEMV bench (Path-B style). Direct cuda:: API.
add_executable(tensorlib_bench_q4 bench/cuda/speed/bench_q4_gemv.cpp)
target_include_directories(tensorlib_bench_q4 BEFORE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(tensorlib_bench_q4 PRIVATE tensorlib)
tensorlib_enable_cuda(tensorlib_bench_q4)
# CUDA SGEMM census (M6 stage-2 tuning). Links cuBLAS + the CUDA runtime as a
# measurement REFERENCE only (the gate is >=90% cuBLAS) — never a library dep;
# cuda.h still dlopen's the driver and ships zero third-party runtime deps.
# Not a ctest (it's a perf bench, and cuBLAS may be absent on CI). Only built
# when the CUDA Toolkit's cuBLAS is found.
find_library(CUBLAS_LIB cublas
PATHS ${CMAKE_CUDA_COMPILER_LIBRARY_ROOT}/lib64
${CMAKE_CUDA_COMPILER_LIBRARY_ROOT}/targets/x86_64-linux/lib
/usr/local/cuda/lib64 /usr/lib/x86_64-linux-gnu)
find_library(CUDART_LIB cudart
PATHS ${CMAKE_CUDA_COMPILER_LIBRARY_ROOT}/lib64
${CMAKE_CUDA_COMPILER_LIBRARY_ROOT}/targets/x86_64-linux/lib
/usr/local/cuda/lib64 /usr/lib/x86_64-linux-gnu)
find_path(CUDA_TK_INC cublas_v2.h
PATHS ${CMAKE_CUDA_COMPILER_LIBRARY_ROOT}/include
${CMAKE_CUDA_COMPILER_LIBRARY_ROOT}/targets/x86_64-linux/include
/usr/local/cuda/include)
if(CUBLAS_LIB AND CUDART_LIB AND CUDA_TK_INC)
add_executable(tensorlib_bench_cuda bench/cuda/speed/bench_cuda_gemm.cpp)
target_link_libraries(tensorlib_bench_cuda PRIVATE tensorlib
${CUBLAS_LIB} ${CUDART_LIB})
# The project include MUST come before the CUDA Toolkit include: the bench's
# #include "cuda.h" means the project header, but the CUDA TK ships its own
# cuda.h (the driver API) — BEFORE prepends so ours wins the quote-include.
target_include_directories(tensorlib_bench_cuda BEFORE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include)
target_include_directories(tensorlib_bench_cuda PRIVATE ${CUDA_TK_INC})
tensorlib_enable_cuda(tensorlib_bench_cuda)
message(STATUS "cuda bench: cuBLAS gate enabled (${CUBLAS_LIB})")
else()
message(STATUS "cuda bench: cuBLAS not found — SGEMM gate bench skipped")
endif()
endif()
add_executable(tensorlib_bench bench/shared/speed/bench_main.cpp)
target_link_libraries(tensorlib_bench PRIVATE tensorlib)
# auto-mode CPU/GPU crossover census (misc/census.cpp). Device-agnostic array
# API, so the gpu:: facade routes it to Metal on Apple and CUDA off-Apple —
# tensorlib_enable_cuda below embeds the PTX so gpu_available() is true.
add_executable(tensorlib_census misc/census.cpp)
target_link_libraries(tensorlib_census PRIVATE tensorlib)
# Local-LLM shape bench — the dev harness for M7-M9 (decode/prefill GEMM +
# attention at real decoder-layer shapes). Same backend-agnostic array API.
add_executable(tensorlib_bench_llm bench/shared/speed/bench_llm.cpp)
target_link_libraries(tensorlib_bench_llm PRIVATE tensorlib)
# MNIST-shaped MLP training bench (synthetic data, hand-rolled backprop) — a
# real end-to-end workload (loss down / acc up) exercising the full training op
# mix on the GPU. Fixed-step throughput, sub-second; no data download.
add_executable(tensorlib_bench_mlp bench/shared/speed/bench_mlp_train.cpp)
target_link_libraries(tensorlib_bench_mlp PRIVATE tensorlib)
# M9 model-surface check: RoPE op + RMSNorm/SiLU/SwiGLU compositions + a full
# llama decoder block, all via the array API, verified vs a from-scratch CPU
# reference. Backend-agnostic; gates correctness of the transformer wiring.
add_executable(tensorlib_check_llm_block bench/shared/check/check_llm_block.cpp)
target_link_libraries(tensorlib_check_llm_block PRIVATE tensorlib)
add_test(NAME llm_block COMMAND tensorlib_check_llm_block)
# These benches drive the GPU through the backend-agnostic eval seam; on a CUDA
# build they must carry the embedded PTX + TENSORLIB_CUDA define, same as the
# tests, or gpu_available() stays false and every GPU section silently skips.
if(TENSORLIB_CUDA)
tensorlib_enable_cuda(tensorlib_bench)
tensorlib_enable_cuda(tensorlib_census)
tensorlib_enable_cuda(tensorlib_bench_llm)
tensorlib_enable_cuda(tensorlib_bench_mlp)
tensorlib_enable_cuda(tensorlib_check_llm_block)
endif()
# Own CPU GEMM bench (M5 tuning). Links OpenBLAS for the off-Apple gate when
# present (ARM OpenBLAS = same NEON ISA); own-vs-ref only otherwise.
add_executable(tensorlib_bench_cpu bench/cpu/speed/bench_cpu_gemm.cpp)
target_link_libraries(tensorlib_bench_cpu PRIVATE tensorlib)
find_library(OPENBLAS_LIB openblas
PATHS /opt/homebrew/opt/openblas/lib /usr/local/opt/openblas/lib)
if(OPENBLAS_LIB)
target_compile_definitions(tensorlib_bench_cpu PRIVATE BENCH_HAS_OPENBLAS)
target_link_libraries(tensorlib_bench_cpu PRIVATE ${OPENBLAS_LIB})
find_path(OPENBLAS_INC cblas.h
PATHS /opt/homebrew/opt/openblas/include /usr/local/opt/openblas/include)
if(OPENBLAS_INC)
target_include_directories(tensorlib_bench_cpu PRIVATE ${OPENBLAS_INC})
endif()
message(STATUS "cpu bench: OpenBLAS gate enabled (${OPENBLAS_LIB})")
endif()
# The same binary runs in three device modes (silarray convention).
add_test(NAME cpu COMMAND tensorlib_test --cpu)
add_test(NAME gpu COMMAND tensorlib_test --gpu)
add_test(NAME auto COMMAND tensorlib_test --auto)
# CPU microkernel correctness oracle (bench/cpu/check/check_cpu_ukernel.cpp):
# validates each available ukernel (scalar; NEON on ARM; AVX2 on x86) against a
# naive triple loop across full and edge tiles. Pure host code, no external
# deps and no GPU — the one check with nothing to skip on, so it runs as a
# ctest everywhere (unlike check_gguf/tokenizer/qwen, which need the model
# file).
add_executable(tensorlib_check_cpu_ukernel bench/cpu/check/check_cpu_ukernel.cpp)
target_link_libraries(tensorlib_check_cpu_ukernel PRIVATE tensorlib)
add_test(NAME cpu_ukernel COMMAND tensorlib_check_cpu_ukernel)
# GGUF v3 reader check (include/gguf.h): parses a real Qwen2.5-0.5B fp16 model
# and asserts the directory against the reference gguf-py manifest. Pure host
# binary parsing — no GPU; not a ctest (the model file lives outside the repo).
add_executable(check_gguf bench/host/check_gguf.cpp)
target_link_libraries(check_gguf PRIVATE tensorlib)
# Qwen2 BPE tokenizer check (include/tokenizer.h): encodes the HF-oracle test
# strings from bench/host/tokenizer_oracle_data.h and asserts token-exact ids +
# decode round-trip. Pure host code; not a ctest (needs the 1.2GB model file
# outside the repo).
add_executable(check_tokenizer bench/host/check_tokenizer.cpp)
target_link_libraries(check_tokenizer PRIVATE tensorlib)