main
cu 334 lines 12.1 KB
Raw
1 #include <torch/csrc/stable/tensor.h>
2
3 #include <cmath>
4
5 #include "../../../dispatch_utils.h"
6 #include "../../../torch_utils.h"
7 #include "../../vectorization_utils.cuh"
8 #include "../../../cub_helpers.h"
9
10 static inline __device__ int8_t float_to_int8_rn(float x) {
11 #ifdef USE_ROCM
12 static constexpr auto i8_min =
13 static_cast<float>(std::numeric_limits<int8_t>::min());
14 static constexpr auto i8_max =
15 static_cast<float>(std::numeric_limits<int8_t>::max());
16
17 // To match the rounding mode of CUDA, we use nearbyint.
18 // It uses the current rounding mode, which is always FE_TONEAREST on HIP.
19 // If that changes in the future, we may need to set the rounding mode
20 // explicitly, either at runtime or compile time.
21 float dst = std::nearbyint(x);
22
23 // saturate
24 // See https://github.com/pytorch/pytorch/issues/127666
25 // See https://github.com/llvm/llvm-project/issues/95183
26 // hip-clang std::clamp __glibcxx_assert_fail host function when building on
27 // Arch/gcc14. The following replaces std::clamp usage with similar logic
28 // dst = std::clamp(dst, i8_min, i8_max);
29 dst = (dst < i8_min) ? i8_min : (dst > i8_max) ? i8_max : dst;
30 return static_cast<int8_t>(dst);
31 #else
32 // CUDA path
33 uint32_t dst;
34 asm volatile("cvt.rni.sat.s8.f32 %0, %1;" : "=r"(dst) : "f"(x));
35 return reinterpret_cast<const int8_t&>(dst);
36 #endif
37 }
38
39 static inline __device__ int32_t float_to_int32_rn(float x) {
40 #ifdef USE_ROCM
41 // int32_max is not exactly representable as float.
42 // Therefore, we need to be careful and manually return int32_max on overflow.
43 // For symmetry, we also do the same for int32_min, even though it is exactly
44 // representable as float and the conversion should be exact.
45 static constexpr auto i32_min = std::numeric_limits<int32_t>::min();
46 static constexpr auto i32_min_f = static_cast<float>(i32_min);
47 static constexpr auto i32_max = std::numeric_limits<int32_t>::max();
48 static constexpr auto i32_max_f = static_cast<float>(i32_max);
49
50 // To match the rounding mode of CUDA, we use nearbyint.
51 // It uses the current rounding mode, which is always FE_TONEAREST on HIP.
52 // If that changes in the future, we may need to set the rounding mode
53 // explicitly, either at runtime or compile time.
54 float dst = std::nearbyint(x);
55
56 // saturate on the higher end.
57 if (dst >= i32_max_f) {
58 return i32_max;
59 }
60 // saturate on the lower end.
61 if (dst <= i32_min_f) {
62 return i32_min;
63 }
64
65 return static_cast<int32_t>(dst);
66 #else
67 // CUDA path
68 uint32_t dst;
69 asm volatile("cvt.rni.sat.s32.f32 %0, %1;" : "=r"(dst) : "f"(x));
70 return reinterpret_cast<const int32_t&>(dst);
71 #endif
72 }
73
74 static inline __device__ int8_t int32_to_int8(int32_t x) {
75 #ifdef USE_ROCM
76 static constexpr auto i8_min =
77 static_cast<int32_t>(std::numeric_limits<int8_t>::min());
78 static constexpr auto i8_max =
79 static_cast<int32_t>(std::numeric_limits<int8_t>::max());
80
81 // saturate
82 // See https://github.com/pytorch/pytorch/issues/127666
83 // See https://github.com/llvm/llvm-project/issues/95183
84 // hip-clang std::clamp __glibcxx_assert_fail host function when building on
85 // Arch/gcc14. The following replaces std::clamp usage with similar logic
86 // int32_t dst = std::clamp(x, i8_min, i8_max);
87 int32_t dst = (x < i8_min) ? i8_min : (x > i8_max) ? i8_max : x;
88 return static_cast<int8_t>(dst);
89 #else
90 // CUDA path
91 uint32_t dst;
92 asm volatile("cvt.sat.s8.s32 %0, %1;" : "=r"(dst) : "r"(x));
93 return reinterpret_cast<const int8_t&>(dst);
94 #endif
95 }
96
97 namespace vllm {
98
99 template <typename scalar_t, typename scale_t>
100 __global__ void static_scaled_int8_quant_kernel(
101 const scalar_t* __restrict__ input, int8_t* __restrict__ output,
102 const scale_t* scale_ptr, const int hidden_size) {
103 const int tid = threadIdx.x;
104 const int stride = blockDim.x;
105 const int64_t token_idx = blockIdx.x;
106 const float scale = *scale_ptr;
107
108 // Must be performed using 64-bit math to avoid integer overflow.
109 const scalar_t* row_in = input + token_idx * hidden_size;
110 int8_t* row_out = output + token_idx * hidden_size;
111
112 vectorize_with_alignment<16>(
113 row_in, row_out, hidden_size, tid, stride,
114 [=] __device__(int8_t& dst, const scalar_t& src) {
115 dst = float_to_int8_rn(static_cast<float>(src) / scale);
116 });
117 }
118
119 template <typename scalar_t, typename scale_t, typename azp_t>
120 __global__ void static_scaled_int8_azp_quant_kernel(
121 const scalar_t* __restrict__ input, int8_t* __restrict__ output,
122 const scale_t* scale_ptr, const azp_t* azp_ptr, const int hidden_size) {
123 const int tid = threadIdx.x;
124 const int stride = blockDim.x;
125 const int64_t token_idx = blockIdx.x;
126 const float scale = *scale_ptr;
127 const azp_t azp = *azp_ptr;
128 const float inv_s = 1.0f / scale;
129
130 // Must be performed using 64-bit math to avoid integer overflow.
131 const scalar_t* row_in = input + token_idx * hidden_size;
132 int8_t* row_out = output + token_idx * hidden_size;
133
134 vectorize_with_alignment<16>(
135 row_in, row_out, hidden_size, tid, stride,
136 [=] __device__(int8_t& dst, const scalar_t& src) {
137 const auto v = static_cast<float>(src) * inv_s;
138 dst = int32_to_int8(float_to_int32_rn(v) + azp);
139 });
140 }
141
142 template <typename scalar_t, typename scale_t>
143 __global__ void dynamic_scaled_int8_quant_kernel(
144 const scalar_t* __restrict__ input, int8_t* __restrict__ output,
145 scale_t* scale_out, const int hidden_size) {
146 const int tid = threadIdx.x;
147 const int stride = blockDim.x;
148 const int64_t token_idx = blockIdx.x;
149
150 // Must be performed using 64-bit math to avoid integer overflow.
151 const scalar_t* row_in = input + token_idx * hidden_size;
152 int8_t* row_out = output + token_idx * hidden_size;
153
154 // calculate for absmax
155 float thread_max = 0.f;
156 vectorize_read_with_alignment<16>(
157 row_in, hidden_size, tid, stride, [&] __device__(const scalar_t& src) {
158 const float v = fabsf(static_cast<float>(src));
159 thread_max = fmaxf(thread_max, v);
160 });
161 using BlockReduce = cub::BlockReduce<float, 256>;
162 __shared__ typename BlockReduce::TempStorage tmp;
163 float block_max = BlockReduce(tmp).Reduce(thread_max, CubMaxOp{}, blockDim.x);
164 __shared__ float absmax;
165 if (tid == 0) {
166 absmax = block_max;
167 scale_out[blockIdx.x] = absmax / 127.f;
168 }
169 __syncthreads();
170
171 float inv_s = (absmax == 0.f) ? 0.f : 127.f / absmax;
172
173 vectorize_with_alignment<16>(
174 row_in, row_out, hidden_size, tid, stride,
175 [=] __device__(int8_t& dst, const scalar_t& src) {
176 dst = float_to_int8_rn(static_cast<float>(src) * inv_s);
177 });
178 }
179
180 // MinMax structure to hold min and max values in one go
181 struct MinMax {
182 float min, max;
183
184 __host__ __device__ MinMax()
185 : min(std::numeric_limits<float>::max()),
186 max(std::numeric_limits<float>::lowest()) {}
187
188 __host__ __device__ explicit MinMax(float v) : min(v), max(v) {}
189
190 __host__ __device__ MinMax& operator+=(float v) {
191 min = fminf(min, v);
192 max = fmaxf(max, v);
193 return *this;
194 }
195
196 // merge two MinMax objects
197 __host__ __device__ MinMax& operator&=(const MinMax& other) {
198 min = fminf(min, other.min);
199 max = fmaxf(max, other.max);
200 return *this;
201 }
202 };
203
204 __host__ __device__ inline MinMax operator+(MinMax a, float v) {
205 return a += v;
206 }
207 __host__ __device__ inline MinMax operator&(MinMax a, const MinMax& b) {
208 return a &= b;
209 }
210
211 template <typename scalar_t, typename scale_t, typename azp_t>
212 __global__ void dynamic_scaled_int8_azp_quant_kernel(
213 const scalar_t* __restrict__ input, int8_t* __restrict__ output,
214 scale_t* scale_out, azp_t* azp_out, const int hidden_size) {
215 const int tid = threadIdx.x;
216 const int stride = blockDim.x;
217 const int64_t token_idx = blockIdx.x;
218
219 // Must be performed using 64-bit math to avoid integer overflow.
220 const scalar_t* row_in = input + token_idx * hidden_size;
221 int8_t* row_out = output + token_idx * hidden_size;
222
223 MinMax thread_mm;
224 vectorize_read_with_alignment<16>(row_in, hidden_size, tid, stride,
225 [&] __device__(const scalar_t& src) {
226 thread_mm += static_cast<float>(src);
227 });
228
229 using BlockReduce = cub::BlockReduce<MinMax, 256>;
230 __shared__ typename BlockReduce::TempStorage tmp;
231
232 MinMax mm = BlockReduce(tmp).Reduce(
233 thread_mm,
234 [] __device__(MinMax a, const MinMax& b) {
235 a &= b;
236 return a;
237 },
238 blockDim.x);
239
240 __shared__ float scale_sh;
241 __shared__ azp_t azp_sh;
242 if (tid == 0) {
243 float s = (mm.max - mm.min) / 255.f;
244 float zp = nearbyintf(-128.f - mm.min / s); // round-to-even
245 scale_sh = s;
246 azp_sh = azp_t(zp);
247 scale_out[blockIdx.x] = s;
248 azp_out[blockIdx.x] = azp_sh;
249 }
250 __syncthreads();
251
252 const float inv_s = 1.f / scale_sh;
253 const azp_t azp = azp_sh;
254
255 vectorize_with_alignment<16>(
256 row_in, row_out, hidden_size, tid, stride,
257 [=] __device__(int8_t& dst, const scalar_t& src) {
258 const auto v = static_cast<float>(src) * inv_s;
259 dst = int32_to_int8(float_to_int32_rn(v) + azp);
260 });
261 }
262
263 } // namespace vllm
264
265 void static_scaled_int8_quant(
266 torch::stable::Tensor& out, // [..., hidden_size]
267 torch::stable::Tensor const& input, // [..., hidden_size]
268 torch::stable::Tensor const& scale,
269 std::optional<torch::stable::Tensor> const& azp) {
270 STD_TORCH_CHECK(input.is_contiguous());
271 STD_TORCH_CHECK(out.is_contiguous());
272 STD_TORCH_CHECK(scale.numel() == 1);
273 STD_TORCH_CHECK(!azp || azp->numel() == 1);
274
275 int const hidden_size = input.size(-1);
276 int const num_tokens = input.numel() / hidden_size;
277 dim3 const grid(num_tokens);
278 dim3 const block(std::min(hidden_size, 256));
279 const torch::stable::accelerator::DeviceGuard device_guard(
280 input.get_device_index());
281 const cudaStream_t stream = get_current_cuda_stream();
282 VLLM_STABLE_DISPATCH_FLOATING_TYPES(
283 input.scalar_type(), "static_scaled_int8_quant_kernel", [&] {
284 if (!azp) {
285 vllm::static_scaled_int8_quant_kernel<scalar_t, float>
286 <<<grid, block, 0, stream>>>(input.const_data_ptr<scalar_t>(),
287 out.mutable_data_ptr<int8_t>(),
288 scale.const_data_ptr<float>(),
289 hidden_size);
290 } else {
291 vllm::static_scaled_int8_azp_quant_kernel<scalar_t, float, int32_t>
292 <<<grid, block, 0, stream>>>(
293 input.const_data_ptr<scalar_t>(),
294 out.mutable_data_ptr<int8_t>(), scale.const_data_ptr<float>(),
295 azp->const_data_ptr<int32_t>(), hidden_size);
296 }
297 });
298 }
299
300 void dynamic_scaled_int8_quant(
301 torch::stable::Tensor& out, // [..., hidden_size]
302 torch::stable::Tensor const& input, // [..., hidden_size]
303 torch::stable::Tensor& scales,
304 std::optional<torch::stable::Tensor> const& azp) {
305 STD_TORCH_CHECK(input.is_contiguous());
306 STD_TORCH_CHECK(out.is_contiguous());
307 STD_TORCH_CHECK(scales.is_contiguous());
308 STD_TORCH_CHECK(!azp || azp->is_contiguous());
309
310 int const hidden_size = input.size(-1);
311 int const num_tokens = input.numel() / hidden_size;
312 dim3 const grid(num_tokens);
313 dim3 const block(std::min(hidden_size, 256));
314 const torch::stable::accelerator::DeviceGuard device_guard(
315 input.get_device_index());
316 const cudaStream_t stream = get_current_cuda_stream();
317 VLLM_STABLE_DISPATCH_FLOATING_TYPES(
318 input.scalar_type(), "dynamic_scaled_int8_quant_kernel", [&] {
319 if (!azp) {
320 vllm::dynamic_scaled_int8_quant_kernel<scalar_t, float>
321 <<<grid, block, 0, stream>>>(input.const_data_ptr<scalar_t>(),
322 out.mutable_data_ptr<int8_t>(),
323 scales.mutable_data_ptr<float>(),
324 hidden_size);
325 } else {
326 vllm::dynamic_scaled_int8_azp_quant_kernel<scalar_t, float, int32_t>
327 <<<grid, block, 0, stream>>>(input.const_data_ptr<scalar_t>(),
328 out.mutable_data_ptr<int8_t>(),
329 scales.mutable_data_ptr<float>(),
330 azp->mutable_data_ptr<int32_t>(),
331 hidden_size);
332 }
333 });
334 }