main
h 549 lines 21.3 KB
Raw
1 // Adapted from
2 // https://github.com/sgl-project/sglang/tree/main/sgl-kernel/csrc/cpu
3 //
4 // Synced from
5 // https://github.com/sgl-project/sglang/tree/7c248dde7fe1f3b5100966f8143f97a9932c22a4/sgl-kernel/csrc/cpu
6 // Sync date: 2026-07-29
7
8 // clang-format off
9
10 #pragma once
11
12 #include <ATen/ATen.h>
13 #include <ATen/Dispatch.h>
14 #include <ATen/Parallel.h>
15 #include <cstring>
16
17 #if defined(_OPENMP)
18 #include <omp.h>
19 #endif
20
21 // Adapted from sglang: dispatch utilities for FP8 W8A8 kernels
22 #define AT_DISPATCH_OUT_TYPES(TYPE, NAME, ...) \
23 AT_DISPATCH_SWITCH( \
24 TYPE, \
25 NAME, \
26 AT_PRIVATE_CASE_TYPE_USING_HINT(at::ScalarType::Float, out_t, __VA_ARGS__) \
27 AT_PRIVATE_CASE_TYPE_USING_HINT(at::ScalarType::BFloat16, out_t, __VA_ARGS__) \
28 AT_PRIVATE_CASE_TYPE_USING_HINT(at::ScalarType::Half, out_t, __VA_ARGS__))
29
30 #define AT_DISPATCH_CASE_ENUM_NO_RETURN(VALUE, TYPE, HINT, ...) \
31 case VALUE: { \
32 constexpr TYPE HINT = VALUE; \
33 __VA_ARGS__; \
34 break; \
35 }
36
37 #define AT_DISPATCH_BOOL_NO_RETURN(VALUE, NAME, HINT, ...) \
38 [&]() { \
39 switch (VALUE) { \
40 AT_DISPATCH_CASE_ENUM_NO_RETURN(true, bool, HINT, __VA_ARGS__) \
41 AT_DISPATCH_CASE_ENUM_NO_RETURN(false, bool, HINT, __VA_ARGS__) \
42 } \
43 }()
44
45 #define AT_DISPATCH_QUANT_MODE_NO_RETURN(MODE, NAME, HINT, ...) \
46 [&]() { \
47 switch (MODE) { \
48 AT_DISPATCH_CASE_ENUM_NO_RETURN(PER_TENSOR, int, HINT, __VA_ARGS__) \
49 AT_DISPATCH_CASE_ENUM_NO_RETURN(PER_ROW, int, HINT, __VA_ARGS__) \
50 AT_DISPATCH_CASE_ENUM_NO_RETURN(PER_GROUP, int, HINT, __VA_ARGS__) \
51 } \
52 }()
53
54 namespace {
55
56 // dispatch bool
57 #define AT_DISPATCH_BOOL(BOOL_V, BOOL_NAME, ...) \
58 [&] { \
59 if (BOOL_V) { \
60 constexpr bool BOOL_NAME = true; \
61 return __VA_ARGS__(); \
62 } else { \
63 constexpr bool BOOL_NAME = false; \
64 return __VA_ARGS__(); \
65 } \
66 }()
67
68 #define AT_DISPATCH_BOOL2(BOOL_V1, BOOL_NAME1, BOOL_V2, BOOL_NAME2, ...) \
69 [&] { \
70 if (BOOL_V1) { \
71 constexpr bool BOOL_NAME1 = true; \
72 if (BOOL_V2) { \
73 constexpr bool BOOL_NAME2 = true; \
74 return __VA_ARGS__(); \
75 } else { \
76 constexpr bool BOOL_NAME2 = false; \
77 return __VA_ARGS__(); \
78 } \
79 } else { \
80 constexpr bool BOOL_NAME1 = false; \
81 if (BOOL_V2) { \
82 constexpr bool BOOL_NAME2 = true; \
83 return __VA_ARGS__(); \
84 } else { \
85 constexpr bool BOOL_NAME2 = false; \
86 return __VA_ARGS__(); \
87 } \
88 } \
89 }()
90
91 // Half + BFloat16, plus one extra scalar type
92 #define AT_DISPATCH_CASE_REDUCED_FLOATING_TYPES_AND(SCALARTYPE, ...) \
93 AT_DISPATCH_CASE_REDUCED_FLOATING_TYPES(__VA_ARGS__) \
94 AT_DISPATCH_CASE(SCALARTYPE, __VA_ARGS__)
95
96 #define AT_DISPATCH_REDUCED_FLOATING_TYPES_AND(SCALARTYPE, TYPE, NAME, ...) \
97 AT_DISPATCH_SWITCH(TYPE, NAME, AT_DISPATCH_CASE_REDUCED_FLOATING_TYPES_AND(SCALARTYPE, __VA_ARGS__))
98
99 // dispatch: bfloat16, float16, int8_t, fp8_e4m3, uint8_t(mxfp4/int4)
100 #define CPU_DISPATCH_PACKED_TYPES(TYPE, ...) \
101 [&] { \
102 switch (TYPE) { \
103 case at::ScalarType::BFloat16: { \
104 using packed_t = at::BFloat16; \
105 return __VA_ARGS__(); \
106 } \
107 case at::ScalarType::Half: { \
108 using packed_t = at::Half; \
109 return __VA_ARGS__(); \
110 } \
111 case at::ScalarType::Char: { \
112 using packed_t = int8_t; \
113 return __VA_ARGS__(); \
114 } \
115 case at::ScalarType::Float8_e4m3fn: { \
116 using packed_t = at::Float8_e4m3fn; \
117 return __VA_ARGS__(); \
118 } \
119 case at::ScalarType::Byte: { \
120 using packed_t = uint8_t; \
121 return __VA_ARGS__(); \
122 } \
123 default: \
124 TORCH_CHECK(false, "Unsupported floating data type.\n"); \
125 } \
126 }()
127
128 // Helper MICRO for CPU_DISPATCH_FLOATING_TYPES_EXT:
129 // TYPE1: the primary dtype (input, output, weight);
130 // TYPE2: defined as PARAM_T input
131 #define CPU_DISPATCH_TYPE1_WITH_PARAM(TYPE1, PARAM_T, ...) \
132 switch (TYPE1) { \
133 case at::ScalarType::BFloat16: { \
134 using scalar_t = at::BFloat16; \
135 using param_t = PARAM_T; \
136 return __VA_ARGS__(); \
137 } \
138 case at::ScalarType::Half: { \
139 using scalar_t = at::Half; \
140 using param_t = PARAM_T; \
141 return __VA_ARGS__(); \
142 } \
143 case at::ScalarType::Float: { \
144 using scalar_t = float; \
145 using param_t = PARAM_T; \
146 return __VA_ARGS__(); \
147 } \
148 default: \
149 TORCH_CHECK(false, "Unsupported floating data type."); \
150 }
151
152 // Helper MICRO for CPU_DISPATCH_REDUCED_FLOATING_TYPES_EXT:
153 // TYPE1: the primary dtype (input, output, weight);
154 // TYPE2: defined as PARAM_T input
155 #define CPU_DISPATCH_TYPE1_WITH_PARAM_REDUCED(TYPE1, PARAM_T, ...) \
156 switch (TYPE1) { \
157 case at::ScalarType::BFloat16: { \
158 using scalar_t = at::BFloat16; \
159 using param_t = PARAM_T; \
160 return __VA_ARGS__(); \
161 } \
162 case at::ScalarType::Half: { \
163 using scalar_t = at::Half; \
164 using param_t = PARAM_T; \
165 return __VA_ARGS__(); \
166 } \
167 default: \
168 TORCH_CHECK(false, "Unsupported floating data type."); \
169 }
170
171 // Helper MICRO for CPU_DISPATCH_REDUCED_FLOATING_TYPES_EXT:
172 // TYPE1: the dtype both for scalar_t and param_t
173 #define CPU_DISPATCH_TYPE1_WITH_SAME_PARAM_REDUCED(TYPE1, ...) \
174 switch (TYPE1) { \
175 case at::ScalarType::BFloat16: { \
176 using scalar_t = at::BFloat16; \
177 using param_t = at::BFloat16; \
178 return __VA_ARGS__(); \
179 } \
180 case at::ScalarType::Half: { \
181 using scalar_t = at::Half; \
182 using param_t = at::Half; \
183 return __VA_ARGS__(); \
184 } \
185 default: \
186 TORCH_CHECK(false, "Unsupported reduced floating data type."); \
187 }
188
189 // dispatch with mixed dtypes (TYPE1, TYPE2):
190 // TYPE1: the primary dtype (input, output, weight);
191 // TYPE2: the secondary dtype (bias, etc.).
192 #define CPU_DISPATCH_FLOATING_TYPES_EXT(TYPE1, TYPE2, ...) \
193 [&] { \
194 if (TYPE2 == at::kFloat) { \
195 CPU_DISPATCH_TYPE1_WITH_PARAM(TYPE1, float, __VA_ARGS__) \
196 } else if (TYPE2 == at::ScalarType::BFloat16) { \
197 CPU_DISPATCH_TYPE1_WITH_PARAM(TYPE1, at::BFloat16, __VA_ARGS__) \
198 } else if (TYPE2 == at::ScalarType::Half) { \
199 CPU_DISPATCH_TYPE1_WITH_PARAM(TYPE1, at::Half, __VA_ARGS__) \
200 } else { \
201 TORCH_CHECK(false, "Unsupported floating data type."); \
202 } \
203 }()
204
205 // dispatch with mixed dtypes (reduced one, no float for TYPE1) (TYPE1, TYPE2):
206 // TYPE1: the primary dtype (input, output, weight);
207 // TYPE2: the secondary dtype (bias, etc.).
208 #define CPU_DISPATCH_REDUCED_FLOATING_TYPES_EXT(TYPE1, TYPE2, ...) \
209 [&] { \
210 if (TYPE2 == at::kFloat) { \
211 CPU_DISPATCH_TYPE1_WITH_PARAM_REDUCED(TYPE1, float, __VA_ARGS__) \
212 } else { \
213 TORCH_CHECK(TYPE1 == TYPE2); \
214 CPU_DISPATCH_TYPE1_WITH_SAME_PARAM_REDUCED(TYPE1, __VA_ARGS__) \
215 } \
216 }()
217
218 #define UNUSED(x) (void)(x)
219
220 #define CHECK_CPU(x) TORCH_CHECK(x.device().type() == at::kCPU, #x " must be a CPU tensor")
221
222 #define CHECK_CONTIGUOUS(x) TORCH_CHECK(x.is_contiguous(), #x " must be contiguous")
223 #define CHECK_LAST_DIM_CONTIGUOUS(x) \
224 TORCH_CHECK(x.strides()[x.strides().size() - 1] == 1, #x "must be contiguous at last dimension")
225
226 #define CHECK_INPUT(x) \
227 CHECK_CPU(x); \
228 CHECK_CONTIGUOUS(x)
229 #define CHECK_LAST_DIM_CONTIGUOUS_INPUT(x) \
230 CHECK_CPU(x); \
231 CHECK_LAST_DIM_CONTIGUOUS(x)
232
233 #define CHECK_DIM(d, x) TORCH_CHECK(x.dim() == d, #x " must be a " #d "D tensor")
234
235 #define CHECK_EQ(a, b) TORCH_CHECK((a) == (b), "CHECK_EQ(" #a ", " #b ") failed. ", a, " vs ", b)
236 #define CHECK_GT(a, b) TORCH_CHECK((a) > (b), "CHECK_GT(" #a ", " #b ") failed. ", a, " vs ", b)
237 #define CHECK_GE(a, b) TORCH_CHECK((a) >= (b), "CHECK_GE(" #a ", " #b ") failed. ", a, " vs ", b)
238
239 template <bool is_only_lastdim_contiguous>
240 static inline void CHECK_INPUT_SHAPE_DTYPE(const at::Tensor& tensor, const at::IntArrayRef sizes, at::ScalarType st) {
241 TORCH_CHECK(tensor.sizes() == sizes, "Input tensor shape mismatch: expected ", sizes, ", got ", tensor.sizes());
242 TORCH_CHECK(tensor.scalar_type() == st, "Input tensor dtype mismatch");
243 if constexpr (is_only_lastdim_contiguous) {
244 CHECK_LAST_DIM_CONTIGUOUS_INPUT(tensor);
245 } else {
246 CHECK_INPUT(tensor);
247 }
248 }
249
250 // [NB] Parallel Routines
251 //
252 // * at::parallel_for - applies for most of generic use cases, this will be compiled
253 // against openmp in default torch release.
254 //
255 // * parallel_for - same function as above, can choose payload partition scheme in
256 // balance211.
257 //
258 // * parallel_2d - parallel for 2 dimensions, used in GEMM, etc.
259 // this one will do payload balance across 2 dimensions.
260 //
261
262 // grain size for each thread
263 constexpr int GRAIN_SIZE = 1024;
264
265 template <typename T, typename std::enable_if<std::is_integral<T>::value, int>::type = 0>
266 inline T div_up(T x, T y) {
267 return (x + y - 1) / y;
268 }
269
270 // you can only use at::get_thread_num() with at::parallel_for()
271 // as it is lazy initialized, otherwise it will always return 0.
272 inline int get_thread_num() {
273 #if defined(_OPENMP)
274 return omp_get_thread_num();
275 #else
276 return 0;
277 #endif
278 }
279
280 // balance payload across each thread
281 template <typename T>
282 inline void balance211(T n, T nth, T ith, T& n_start, T& n_end) {
283 // pytorch aten partition pattern
284 T n_my = div_up(n, nth);
285 n_start = ith * n_my;
286 n_end = std::min(n_start + n_my, n);
287 }
288
289 template <typename func_t>
290 inline void parallel_for(int n, const func_t& f) {
291 #if defined(_OPENMP)
292 #pragma omp parallel
293 {
294 int nth = omp_get_num_threads();
295 int ith = omp_get_thread_num();
296 int tbegin, tend;
297 balance211(n, nth, ith, tbegin, tend);
298 f(tbegin, tend);
299 }
300 #else
301 f(0, n);
302 #endif
303 }
304
305 // for 1d parallel, use `actual_nth`
306 // for 2d parallel, use even nths, e.g. 43->42
307 int inline adjust_num_threads(int m) {
308 int actual_nth = at::get_num_threads();
309 if (m == 1) {
310 return actual_nth;
311 }
312 return std::max(1, (actual_nth >> 1) * 2);
313 }
314
315 template <typename func_t>
316 inline void parallel_2d(int m, int n, const func_t& f) {
317 // make sure we have even num_threads
318 int nth = adjust_num_threads(m);
319
320 // [NOTE] thread blocking:
321 //
322 // 1) prefer square block per thread
323 // 2) use even number of CPU cores
324 // 3) use all `num_threads` cores
325 //
326 // we have:
327 // TM * TN = T
328 // BM / TM = BN / TN
329 // then:
330 // TM = ((BM / BN) * T) ^ 0.5
331 //
332 float r = float(m) / n;
333 int nth_m = std::ceil(std::sqrt(r * nth));
334 int nth_n = 1;
335 for (; nth_m > 0; --nth_m) {
336 nth_n = nth / nth_m;
337 if (nth_m * nth_n == nth) {
338 break;
339 }
340 }
341
342 #if defined(_OPENMP)
343 #pragma omp parallel num_threads(nth)
344 {
345 int ith = omp_get_thread_num();
346 int ith_m = ith / nth_n;
347 int ith_n = ith % nth_n;
348
349 int thread_block_m = div_up(m, nth_m);
350 int thread_block_n = div_up(n, nth_n);
351
352 int begin_m = ith_m * thread_block_m;
353 int end_m = std::min(m, begin_m + thread_block_m);
354 int begin_n = ith_n * thread_block_n;
355 int end_n = std::min(n, begin_n + thread_block_n);
356
357 f(begin_m, end_m, begin_n, end_n);
358 }
359 #else
360 f(0, m, 0, n);
361 #endif
362 }
363
364 // Like parallel_2d but with explicit nth_m x nth_n decomposition.
365 // Caller is responsible for choosing nth_m and nth_n.
366 //
367 // Cherry-picked from a newer sglang commit than this file's last full sync
368 // (needed by mhc.cpp's phase-1 tiling); not part of a full common.h resync.
369 template <typename func_t>
370 inline void parallel_2d_tiled(int m, int n, int nth_m, int nth_n, const func_t& f) {
371 const int nth = nth_m * nth_n;
372 #if defined(_OPENMP)
373 #pragma omp parallel num_threads(nth)
374 {
375 int ith = omp_get_thread_num();
376 int ith_m = ith / nth_n;
377 int ith_n = ith % nth_n;
378
379 int thread_block_m = div_up(m, nth_m);
380 int thread_block_n = div_up(n, nth_n);
381
382 int begin_m = std::min(ith_m * thread_block_m, m);
383 int end_m = std::min(begin_m + thread_block_m, m);
384 int begin_n = std::min(ith_n * thread_block_n, n);
385 int end_n = std::min(begin_n + thread_block_n, n);
386
387 f(begin_m, end_m, begin_n, end_n);
388 }
389 #else
390 f(0, m, 0, n);
391 #endif
392 }
393
394 // limit max cache blocks
395 // when we need to do pre-unpack for weights, e.g. fp8
396 #define MAX_CACHE_BLOCK_SIZE 4
397
398 template <typename T>
399 inline int get_cache_blocks(int chunk_size) {
400 // L2 2MB and ratio of 50%
401 const int L2_size = 2048 * 1024 >> 1;
402 return std::max(1, int(L2_size / (chunk_size * sizeof(T))));
403 }
404
405 template <>
406 inline int get_cache_blocks<at::Float8_e4m3fn>(int chunk_size) {
407 // fp8 uses bf16 as accumulate type
408 int cache_block_size = get_cache_blocks<at::BFloat16>(chunk_size);
409 return std::min(MAX_CACHE_BLOCK_SIZE, cache_block_size);
410 }
411
412 template <>
413 inline int get_cache_blocks<uint8_t>(int chunk_size) {
414 // mxfp4 uses bf16 as accumulate type
415 int cache_block_size = get_cache_blocks<at::BFloat16>(chunk_size);
416 return std::min(MAX_CACHE_BLOCK_SIZE, cache_block_size);
417 }
418
419 // 2d sequential loop in range : [mb0, mb1), [nb0, nb1)
420 template <typename T, typename func_t>
421 inline void loop_2d(int64_t mb0, int64_t mb1, int64_t nb0, int64_t nb1, int64_t chunk_size, const func_t& f) {
422 // get number of blocks for L2 in most inner loop
423 int64_t cache_blocks_nb = get_cache_blocks<T>(chunk_size);
424
425 // loop order: [NB / cache_blocks_nb, MB, cache_blocks_nb]
426 // TODO: implement reverse order of [MB / cache_blocks_mb, NB, cache_blocks_mb]
427 for (int64_t nbb = nb0; nbb < nb1; nbb += cache_blocks_nb) {
428 for (int64_t mb = mb0; mb < mb1; ++mb) {
429 for (int64_t nb = nbb; nb < std::min(nbb + cache_blocks_nb, nb1); ++nb) {
430 f(mb, nb, nb - nbb);
431 }
432 }
433 }
434 }
435
436 // data indexing for dimension collapse
437 template <typename T>
438 inline T data_index_init(T offset) {
439 return offset;
440 }
441
442 template <typename T, typename... Args>
443 inline T data_index_init(T offset, T& x, const T& X, Args&&... args) {
444 offset = data_index_init(offset, std::forward<Args>(args)...);
445 x = offset % X;
446 return offset / X;
447 }
448
449 inline bool data_index_step() {
450 return true;
451 }
452
453 template <typename T, typename... Args>
454 inline bool data_index_step(T& x, const T& X, Args&&... args) {
455 if (data_index_step(std::forward<Args>(args)...)) {
456 x = ((x + 1) == X) ? 0 : (x + 1);
457 return x == 0;
458 }
459 return false;
460 }
461
462 // forced unroll for perf critical path
463
464 #if __has_attribute(always_inline)
465 #define ALWAYS_INLINE __attribute__((__always_inline__)) inline
466 #else
467 #define ALWAYS_INLINE inline
468 #endif
469
470 template <int n>
471 struct Unroll {
472 template <typename Func, typename... Args>
473 ALWAYS_INLINE void operator()(const Func& f, Args... args) const {
474 Unroll<n - 1>{}(f, args...);
475 f(std::integral_constant<int, n - 1>{}, args...);
476 }
477 };
478
479 template <>
480 struct Unroll<1> {
481 template <typename Func, typename... Args>
482 ALWAYS_INLINE void operator()(const Func& f, Args... args) const {
483 f(std::integral_constant<int, 0>{}, args...);
484 }
485 };
486
487 // conditional data ptr for optional tensor
488 template <typename T>
489 inline T* conditional_data_ptr(const std::optional<at::Tensor>& opt) {
490 return opt.has_value() ? opt.value().data_ptr<T>() : nullptr;
491 }
492
493 // Adapted from sglang: M blocking utilities for FP8 W8A8 kernels
494 inline int64_t get_m_block(int64_t M) {
495 if (M <= 48) {
496 return M;
497 } else if (M < 64) {
498 return 32;
499 } else if (M < 96) {
500 return 64;
501 } else {
502 return 128;
503 }
504 }
505
506 inline std::tuple<bool, int64_t, int64_t, int64_t> get_m_blocking(int64_t M) {
507 bool parallel_on_M = M > 128;
508 int64_t block_m = get_m_block(M);
509 int64_t Mc = (M + block_m - 1) / block_m;
510 int64_t Mc_parallel = parallel_on_M ? Mc : 1;
511 return std::make_tuple(parallel_on_M, block_m, Mc, Mc_parallel);
512 }
513
514 #if defined(CPU_CAPABILITY_AVX512)
515 template <typename T>
516 void zero_buffer(T* data, int64_t size) {
517 const int32_t vec_size = at::vec::Vectorized<T>::size();
518 auto zero_vec = at::vec::Vectorized<T>(0);
519 int64_t d = 0;
520 for (; d < size - (size % vec_size); d += vec_size) {
521 zero_vec.store(data + d);
522 }
523 if (d < size) {
524 zero_vec.store(data + d, size - d);
525 }
526 }
527 #else
528 template <typename T>
529 void zero_buffer(T* data, int64_t size) {
530 memset(data, 0, sizeof(T) * size);
531 }
532 #endif
533
534 #if defined(__x86_64__)
535 // CPU_CAPABILITY_AVX10_2 is defined by cmake only when the compiler accepts
536 // -mavx10.2; without it the builtin below is not merely absent but a hard
537 // error, so the AVX512 paths have to stand in.
538 bool avx10_2_available() {
539 #ifdef CPU_CAPABILITY_AVX10_2
540 // __builtin_cpu_supports returns the masked feature bit rather than 0/1.
541 static const bool available = __builtin_cpu_supports("avx10.2");
542 return available;
543 #else
544 return false;
545 #endif
546 }
547 #endif
548
549 } // anonymous namespace