main
cu 457 lines 19.2 KB
Raw
1 /*
2 * Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include <torch/csrc/stable/tensor.h>
18 #include "libtorch_stable/torch_utils.h"
19 #include "libtorch_stable/dispatch_utils.h"
20 #include "../../cuda_vec_utils.cuh"
21
22 #include <cuda_runtime_api.h>
23 #include <cuda_runtime.h>
24
25 #include <cuda_fp8.h>
26
27 #include "cuda_utils.h"
28 #include "nvfp4_utils.cuh"
29 #include "libtorch_stable/launch_bounds_utils.h"
30
31 namespace vllm {
32
33 // NVFP4 quantization kernel for experts (low-latency path).
34 // When FUSE_SILU_MUL=true, expects input with gate||up layout and fuses
35 // SiLU(gate)*up before quantization.
36 // Use UE4M3 by default.
37 template <class Type, bool FUSE_SILU_MUL = false, bool UE8M0_SF = false,
38 bool SMALL_NUM_EXPERTS = false>
39 __global__ void __launch_bounds__(512, VLLM_BLOCKS_PER_SM(512))
40 cvt_fp16_to_fp4(int32_t numRows, int32_t numCols, Type const* in,
41 float const* SFScale, uint32_t* out, uint32_t* SFout,
42 uint32_t* input_offset_by_experts,
43 uint32_t* output_scale_offset_by_experts, int n_experts,
44 bool low_latency) {
45 using PackedVec = PackedVec<Type, CVT_FP4_PACK16>;
46 static constexpr int CVT_FP4_NUM_THREADS_PER_SF =
47 (CVT_FP4_SF_VEC_SIZE / CVT_FP4_ELTS_PER_THREAD);
48 static_assert(sizeof(PackedVec) == sizeof(Type) * CVT_FP4_ELTS_PER_THREAD,
49 "Vec size is not matched.");
50
51 // Precompute SF layout parameter (constant for entire kernel).
52 int32_t const numKTiles = (numCols + 63) / 64;
53
54 int tid = blockIdx.x * blockDim.x + threadIdx.x;
55 int colsPerRow = numCols / CVT_FP4_ELTS_PER_THREAD;
56 int32_t const numValidRows =
57 min(numRows,
58 static_cast<int32_t>(__ldca(&input_offset_by_experts[n_experts])));
59 // When fusing SiLU+Mul, input has gate || up layout (doubled width)
60 int inColsPerRow = FUSE_SILU_MUL ? colsPerRow * 2 : colsPerRow;
61
62 // Each global thread processes one element
63 for (int globalIdx = tid; globalIdx < numValidRows * colsPerRow;
64 globalIdx += gridDim.x * blockDim.x) {
65 // Calculate which row and column this global thread should process
66 int rowIdx = globalIdx / colsPerRow;
67 int colIdx = globalIdx % colsPerRow;
68
69 // Find index within the experts using different strategies based on expert
70 // count
71 int rowIdx_in_expert = 0;
72 int expert_idx = 0;
73
74 if constexpr (SMALL_NUM_EXPERTS) {
75 for (int i = 0; i < n_experts; i++) {
76 uint32_t current_offset = __ldca(&input_offset_by_experts[i]);
77 uint32_t next_offset = __ldca(&input_offset_by_experts[i + 1]);
78 if (rowIdx >= current_offset && rowIdx < next_offset) {
79 rowIdx_in_expert = rowIdx - current_offset;
80 expert_idx = i;
81 break;
82 }
83 }
84 } else {
85 // Load input offsets into registers first, then do the computation.
86 // Local array size set to 17 because of register limit.
87 uint32_t local_offsets[17];
88 for (int chunk_start = 0; chunk_start < n_experts; chunk_start += 16) {
89 *reinterpret_cast<int4*>(local_offsets) =
90 __ldca(reinterpret_cast<const int4*>(
91 &input_offset_by_experts[chunk_start]));
92 *reinterpret_cast<int4*>(local_offsets + 4) =
93 __ldca(reinterpret_cast<const int4*>(
94 &input_offset_by_experts[chunk_start + 4]));
95 *reinterpret_cast<int4*>(local_offsets + 8) =
96 __ldca(reinterpret_cast<const int4*>(
97 &input_offset_by_experts[chunk_start + 8]));
98 *reinterpret_cast<int4*>(local_offsets + 12) =
99 __ldca(reinterpret_cast<const int4*>(
100 &input_offset_by_experts[chunk_start + 12]));
101 local_offsets[16] = __ldca(&input_offset_by_experts[chunk_start + 16]);
102
103 // Check against the 16 loaded offsets
104 #pragma unroll
105 for (int i = 0; i < 16; i++) {
106 if (rowIdx >= local_offsets[i] && rowIdx < local_offsets[i + 1]) {
107 rowIdx_in_expert = rowIdx - local_offsets[i];
108 expert_idx = chunk_start + i;
109 break;
110 }
111 }
112 }
113 }
114
115 // Load input and optionally apply fused SiLU+Mul
116 int64_t inOffset = rowIdx * inColsPerRow + colIdx;
117 PackedVec in_vec = reinterpret_cast<PackedVec const*>(in)[inOffset];
118 PackedVec quant_input;
119 if constexpr (FUSE_SILU_MUL) {
120 PackedVec in_vec_up =
121 reinterpret_cast<PackedVec const*>(in)[inOffset + colsPerRow];
122 quant_input = compute_silu_mul(in_vec, in_vec_up);
123 } else {
124 quant_input = in_vec;
125 }
126
127 // Get the output tensor offset.
128 // Same as inOffset because 8 elements are packed into one uint32_t.
129 int64_t outOffset = rowIdx * colsPerRow + colIdx;
130 auto& out_pos = out[outOffset];
131
132 // Get the global scaling factor, which will be applied to the SF.
133 // Note SFScale is the same as next GEMM's alpha, which is
134 // (448.f / (Alpha_A / 6.f)).
135 float const SFScaleVal = SFScale == nullptr ? 1.0f : SFScale[expert_idx];
136
137 uint32_t* SFout_in_expert =
138 SFout + output_scale_offset_by_experts[expert_idx] * numKTiles;
139
140 auto sf_out =
141 cvt_quant_to_fp4_get_sf_out_offset<uint32_t,
142 CVT_FP4_NUM_THREADS_PER_SF>(
143 rowIdx_in_expert, colIdx, numKTiles, SFout_in_expert);
144
145 out_pos = cvt_warp_fp16_to_fp4<Type, CVT_FP4_NUM_THREADS_PER_SF, UE8M0_SF>(
146 quant_input, SFScaleVal, sf_out);
147 }
148 }
149
150 // NVFP4 quantization kernel for LARGE_M_TOPK = true (large m_topk optimized
151 // version). When FUSE_SILU_MUL=true, expects input with gate||up layout and
152 // fuses SiLU(gate)*up before quantization.
153 template <class Type, bool FUSE_SILU_MUL = false, bool UE8M0_SF = false,
154 bool SMALL_NUM_EXPERTS = false>
155 __global__ void __launch_bounds__(1024, VLLM_BLOCKS_PER_SM(1024))
156 cvt_fp16_to_fp4(int32_t numRows, int32_t numCols, Type const* in,
157 float const* SFScale, uint32_t* out, uint32_t* SFout,
158 uint32_t* input_offset_by_experts,
159 uint32_t* output_scale_offset_by_experts, int n_experts) {
160 using PackedVec = PackedVec<Type, CVT_FP4_PACK16>;
161 static constexpr int CVT_FP4_NUM_THREADS_PER_SF =
162 (CVT_FP4_SF_VEC_SIZE / CVT_FP4_ELTS_PER_THREAD);
163 static_assert(sizeof(PackedVec) == sizeof(Type) * CVT_FP4_ELTS_PER_THREAD,
164 "Vec size is not matched.");
165
166 // Precompute SF layout parameter (constant for entire kernel).
167 int32_t const numKTiles = (numCols + 63) / 64;
168
169 extern __shared__ uint32_t shared_input_offsets[];
170
171 // Load input offsets into shared memory.
172 // If n_experts is larger than 4, use vectorized int4 to save instructions.
173 // If n_experts is smaller than 4, read directly.
174 if constexpr (SMALL_NUM_EXPERTS) {
175 for (int i = threadIdx.x; i < n_experts + 1; i += blockDim.x) {
176 shared_input_offsets[i] = input_offset_by_experts[i];
177 }
178 } else {
179 for (int i = threadIdx.x * 4; i < n_experts; i += blockDim.x * 4) {
180 *reinterpret_cast<int4*>(&shared_input_offsets[i]) =
181 *reinterpret_cast<const int4*>(&input_offset_by_experts[i]);
182 }
183 if (threadIdx.x == 0) {
184 shared_input_offsets[n_experts] = input_offset_by_experts[n_experts];
185 }
186 }
187
188 __syncthreads();
189
190 int tid = blockIdx.x * blockDim.x + threadIdx.x;
191 int colsPerRow = numCols / CVT_FP4_ELTS_PER_THREAD;
192 int32_t const numValidRows =
193 min(numRows, static_cast<int32_t>(shared_input_offsets[n_experts]));
194 // When fusing SiLU+Mul, input has gate || up layout (doubled width)
195 int inColsPerRow = FUSE_SILU_MUL ? colsPerRow * 2 : colsPerRow;
196
197 // Each global thread processes one element
198 for (int globalIdx = tid; globalIdx < numValidRows * colsPerRow;
199 globalIdx += gridDim.x * blockDim.x) {
200 // Calculate which row and column this global thread should process
201 int rowIdx = globalIdx / colsPerRow;
202 int colIdx = globalIdx % colsPerRow;
203
204 // Find expert using binary search for better performance with large m_topk
205 int rowIdx_in_expert = 0;
206 int expert_idx = 0;
207
208 // Binary search through experts using shared memory
209 int left = 0, right = n_experts - 1;
210 while (left <= right) {
211 int mid = (left + right) / 2;
212 // Get offsets: shared_input_offsets[i] corresponds to
213 // input_offset_by_experts[i]
214 uint32_t mid_offset = shared_input_offsets[mid];
215 uint32_t next_offset = shared_input_offsets[mid + 1];
216
217 if (rowIdx >= mid_offset && rowIdx < next_offset) {
218 rowIdx_in_expert = rowIdx - mid_offset;
219 expert_idx = mid;
220 break;
221 } else if (rowIdx < mid_offset) {
222 right = mid - 1;
223 } else {
224 left = mid + 1;
225 }
226 }
227
228 // Load input and optionally apply fused SiLU+Mul
229 int64_t inOffset = rowIdx * inColsPerRow + colIdx;
230 PackedVec in_vec = reinterpret_cast<PackedVec const*>(in)[inOffset];
231 PackedVec quant_input;
232 if constexpr (FUSE_SILU_MUL) {
233 PackedVec in_vec_up =
234 reinterpret_cast<PackedVec const*>(in)[inOffset + colsPerRow];
235 quant_input = compute_silu_mul(in_vec, in_vec_up);
236 } else {
237 quant_input = in_vec;
238 }
239
240 int64_t outOffset = rowIdx * colsPerRow + colIdx;
241 auto& out_pos = out[outOffset];
242
243 float const SFScaleVal = SFScale == nullptr ? 1.0f : SFScale[expert_idx];
244
245 uint32_t* SFout_in_expert =
246 SFout + output_scale_offset_by_experts[expert_idx] * numKTiles;
247
248 auto sf_out =
249 cvt_quant_to_fp4_get_sf_out_offset<uint32_t,
250 CVT_FP4_NUM_THREADS_PER_SF>(
251 rowIdx_in_expert, colIdx, numKTiles, SFout_in_expert);
252
253 out_pos = cvt_warp_fp16_to_fp4<Type, CVT_FP4_NUM_THREADS_PER_SF, UE8M0_SF>(
254 quant_input, SFScaleVal, sf_out);
255 }
256 }
257
258 template <typename T, bool FUSE_SILU_MUL = false>
259 void quant_impl(void* output, void* output_scale, void* input,
260 void* input_global_scale, void* input_offset_by_experts,
261 void* output_scale_offset_by_experts, int m_topk, int k,
262 int n_experts, cudaStream_t stream) {
263 int multiProcessorCount =
264 get_device_attribute(cudaDevAttrMultiProcessorCount, -1);
265
266 // Grid, Block size.
267 // Each thread converts 8 values.
268 int const workSizePerRow = k / ELTS_PER_THREAD;
269 int const totalWorkSize = m_topk * workSizePerRow;
270 dim3 block(std::min(workSizePerRow, 512));
271 // Get number of blocks per SM
272 int const numBlocksPerSM =
273 vllm_runtime_blocks_per_sm(static_cast<int>(block.x));
274 dim3 grid(std::min(static_cast<int>((totalWorkSize + block.x - 1) / block.x),
275 multiProcessorCount * numBlocksPerSM));
276 while (grid.x <= multiProcessorCount && block.x > 64) {
277 grid.x *= 2;
278 block.x = (block.x + 1) / 2;
279 }
280
281 int const blockRepeat =
282 (totalWorkSize + block.x * grid.x - 1) / (block.x * grid.x);
283 if (blockRepeat > 1) {
284 size_t shared_mem_size = (n_experts + 1) * sizeof(uint32_t);
285 // The shared-memory vectorized offset load only handles full 4-expert
286 // chunks. Use the scalar specialization for the remainder cases.
287 if (n_experts >= 4 && n_experts % 4 == 0) {
288 cvt_fp16_to_fp4<T, FUSE_SILU_MUL, false, false>
289 <<<grid, block, shared_mem_size, stream>>>(
290 m_topk, k, reinterpret_cast<T*>(input),
291 reinterpret_cast<float*>(input_global_scale),
292 reinterpret_cast<uint32_t*>(output),
293 reinterpret_cast<uint32_t*>(output_scale),
294 reinterpret_cast<uint32_t*>(input_offset_by_experts),
295 reinterpret_cast<uint32_t*>(output_scale_offset_by_experts),
296 n_experts);
297 } else {
298 cvt_fp16_to_fp4<T, FUSE_SILU_MUL, false, true>
299 <<<grid, block, shared_mem_size, stream>>>(
300 m_topk, k, reinterpret_cast<T*>(input),
301 reinterpret_cast<float*>(input_global_scale),
302 reinterpret_cast<uint32_t*>(output),
303 reinterpret_cast<uint32_t*>(output_scale),
304 reinterpret_cast<uint32_t*>(input_offset_by_experts),
305 reinterpret_cast<uint32_t*>(output_scale_offset_by_experts),
306 n_experts);
307 }
308 } else {
309 // The low-latency vectorized expert lookup only handles full 16-expert
310 // chunks. Fall back to the scalar lookup path for the remainder cases.
311 if (n_experts >= 16 && n_experts % 16 == 0) {
312 cvt_fp16_to_fp4<T, FUSE_SILU_MUL, false, false>
313 <<<grid, block, 0, stream>>>(
314 m_topk, k, reinterpret_cast<T*>(input),
315 reinterpret_cast<float*>(input_global_scale),
316 reinterpret_cast<uint32_t*>(output),
317 reinterpret_cast<uint32_t*>(output_scale),
318 reinterpret_cast<uint32_t*>(input_offset_by_experts),
319 reinterpret_cast<uint32_t*>(output_scale_offset_by_experts),
320 n_experts, /* bool low_latency */ true);
321 } else {
322 cvt_fp16_to_fp4<T, FUSE_SILU_MUL, false, true>
323 <<<grid, block, 0, stream>>>(
324 m_topk, k, reinterpret_cast<T*>(input),
325 reinterpret_cast<float*>(input_global_scale),
326 reinterpret_cast<uint32_t*>(output),
327 reinterpret_cast<uint32_t*>(output_scale),
328 reinterpret_cast<uint32_t*>(input_offset_by_experts),
329 reinterpret_cast<uint32_t*>(output_scale_offset_by_experts),
330 n_experts, /* bool low_latency */ true);
331 }
332 }
333 }
334
335 } // namespace vllm
336
337 /*Quantization entry for fp4 experts quantization*/
338 #define CHECK_TH_CUDA(x, m) \
339 STD_TORCH_CHECK(x.is_cuda(), m, "must be a CUDA tensor")
340 #define CHECK_CONTIGUOUS(x, m) \
341 STD_TORCH_CHECK(x.is_contiguous(), m, "must be contiguous")
342 #define CHECK_INPUT(x, m) \
343 CHECK_TH_CUDA(x, m); \
344 CHECK_CONTIGUOUS(x, m);
345
346 constexpr auto HALF = torch::headeronly::ScalarType::Half;
347 constexpr auto BF16 = torch::headeronly::ScalarType::BFloat16;
348 constexpr auto FLOAT = torch::headeronly::ScalarType::Float;
349 constexpr auto INT = torch::headeronly::ScalarType::Int;
350 constexpr auto UINT8 = torch::headeronly::ScalarType::Byte;
351
352 // Common validation for fp4 experts quantization entry points.
353 static void validate_fp4_experts_quant_inputs(
354 torch::stable::Tensor const& output,
355 torch::stable::Tensor const& output_scale,
356 torch::stable::Tensor const& input,
357 torch::stable::Tensor const& input_global_scale,
358 torch::stable::Tensor const& input_offset_by_experts,
359 torch::stable::Tensor const& output_scale_offset_by_experts, int64_t m_topk,
360 int64_t k) {
361 CHECK_INPUT(output, "output");
362 CHECK_INPUT(output_scale, "output_scale");
363 CHECK_INPUT(input, "input");
364 CHECK_INPUT(input_global_scale, "input_global_scale");
365 CHECK_INPUT(input_offset_by_experts, "input_offset_by_experts");
366 CHECK_INPUT(output_scale_offset_by_experts, "output_scale_offset_by_experts");
367
368 STD_TORCH_CHECK(output.dim() == 2);
369 STD_TORCH_CHECK(output_scale.dim() == 2);
370 STD_TORCH_CHECK(input.dim() == 2);
371 STD_TORCH_CHECK(input_global_scale.dim() == 1);
372 STD_TORCH_CHECK(input_offset_by_experts.dim() == 1);
373 STD_TORCH_CHECK(output_scale_offset_by_experts.dim() == 1);
374
375 STD_TORCH_CHECK(input.scalar_type() == HALF || input.scalar_type() == BF16);
376 STD_TORCH_CHECK(input_global_scale.scalar_type() == FLOAT);
377 STD_TORCH_CHECK(input_offset_by_experts.scalar_type() == INT);
378 STD_TORCH_CHECK(output_scale_offset_by_experts.scalar_type() == INT);
379 // output is uint8 (two nvfp4 values are packed into one uint8)
380 // output_scale is int32 (four fp8 values are packed into one int32)
381 STD_TORCH_CHECK(output.scalar_type() == UINT8);
382 STD_TORCH_CHECK(output_scale.scalar_type() == INT);
383
384 const int BLOCK_SIZE = 16;
385 STD_TORCH_CHECK(k % BLOCK_SIZE == 0, "k must be a multiple of 16");
386 auto n_experts = input_global_scale.size(0);
387 STD_TORCH_CHECK(input_offset_by_experts.size(0) == n_experts + 1);
388 STD_TORCH_CHECK(output_scale_offset_by_experts.size(0) == n_experts + 1);
389 STD_TORCH_CHECK(output.size(0) == m_topk);
390 STD_TORCH_CHECK(output.size(1) == k / 2);
391 int scales_k = k / BLOCK_SIZE;
392 // 4 means the swizzle requirement by nvidia nvfp4.
393 int padded_k = (scales_k + (4 - 1)) / 4 * 4;
394 // 4 means 4 fp8 values are packed into one int32
395 STD_TORCH_CHECK(output_scale.size(1) * 4 == padded_k);
396 }
397
398 void scaled_fp4_experts_quant_sm1xxa(
399 torch::stable::Tensor& output, torch::stable::Tensor& output_scale,
400 torch::stable::Tensor const& input,
401 torch::stable::Tensor const& input_global_scale,
402 torch::stable::Tensor const& input_offset_by_experts,
403 torch::stable::Tensor const& output_scale_offset_by_experts) {
404 auto m_topk = input.size(0);
405 auto k = input.size(1);
406
407 validate_fp4_experts_quant_inputs(output, output_scale, input,
408 input_global_scale, input_offset_by_experts,
409 output_scale_offset_by_experts, m_topk, k);
410
411 auto n_experts = input_global_scale.size(0);
412 const torch::stable::accelerator::DeviceGuard device_guard(
413 input.get_device_index());
414 const cudaStream_t stream = get_current_cuda_stream(input.get_device_index());
415
416 VLLM_STABLE_DISPATCH_HALF_TYPES(
417 input.scalar_type(), "nvfp4_experts_quant_kernel", [&] {
418 using cuda_type = vllm::CUDATypeConverter<scalar_t>::Type;
419 vllm::quant_impl<cuda_type, /*FUSE_SILU_MUL=*/false>(
420 output.data_ptr(), output_scale.data_ptr(), input.data_ptr(),
421 input_global_scale.data_ptr(), input_offset_by_experts.data_ptr(),
422 output_scale_offset_by_experts.data_ptr(), m_topk, k, n_experts,
423 stream);
424 });
425 }
426
427 void silu_and_mul_scaled_fp4_experts_quant_sm1xxa(
428 torch::stable::Tensor& output, torch::stable::Tensor& output_scale,
429 torch::stable::Tensor const& input,
430 torch::stable::Tensor const& input_global_scale,
431 torch::stable::Tensor const& input_offset_by_experts,
432 torch::stable::Tensor const& output_scale_offset_by_experts) {
433 auto m_topk = input.size(0);
434 // Input has gate || up layout, so k = input.size(1) / 2
435 auto k_times_2 = input.size(1);
436 STD_TORCH_CHECK(k_times_2 % 2 == 0, "input width must be even (gate || up)");
437 auto k = k_times_2 / 2;
438
439 validate_fp4_experts_quant_inputs(output, output_scale, input,
440 input_global_scale, input_offset_by_experts,
441 output_scale_offset_by_experts, m_topk, k);
442
443 auto n_experts = input_global_scale.size(0);
444 const torch::stable::accelerator::DeviceGuard device_guard(
445 input.get_device_index());
446 const cudaStream_t stream = get_current_cuda_stream(input.get_device_index());
447
448 VLLM_STABLE_DISPATCH_HALF_TYPES(
449 input.scalar_type(), "silu_mul_nvfp4_experts_quant_kernel", [&] {
450 using cuda_type = vllm::CUDATypeConverter<scalar_t>::Type;
451 vllm::quant_impl<cuda_type, /*FUSE_SILU_MUL=*/true>(
452 output.data_ptr(), output_scale.data_ptr(), input.data_ptr(),
453 input_global_scale.data_ptr(), input_offset_by_experts.data_ptr(),
454 output_scale_offset_by_experts.data_ptr(), m_topk, k, n_experts,
455 stream);
456 });
457 }