Skip to content

Commit f6136e9

Browse files
ssjiaSS-JIA
authored andcommitted
[ET-VK] Add nchw_to_int8x4_buffer shader for prepacking int8 staging data
Pull Request resolved: #17392 This adds a GLSL compute shader and supporting C++ dispatch logic to transfer int8 tensor data from a staging buffer (in NCHW contiguous order) to a GPU buffer in any PackedInt8 layout (4W, 4C, 4W4C, 4H4W, 4C1W). Previously there was no prepack path for kInt8x4 tensors, so constant int8 tensors (TensorRef inputs) could not be transferred to GPU buffers. This is needed to support constant quantized weights in q8ta operators. The shader uses texel-level dispatch where each thread writes one texel (one int32 = 4 packed int8 values). It decomposes the texel index into block-space coordinates using BufferMetadata strides and hashed_layout dim_order, then reads the corresponding int8 bytes from the staging buffer (interpreted as int32 for device compatibility, avoiding the need for 8-bit buffer support). New files: - nchw_to_int8x4_buffer.glsl: Compute shader handling all PackedInt8 layouts - nchw_to_int8x4_buffer.yaml: Shader variant config - Q8taStaging.h/cpp: C++ dispatch function creating the PrepackNode Modified files: - Staging.cpp: Routes kInt8x4 tensors in prepack_op() to the new function - TestQ8taBinary.cpp: Prepacks TensorRef inputs before quantization - test_q8ta_binary.cpp: Adds const_b test cases for constant tensor B inputs This diff was authored with Claude. ghstack-source-id: 341022576 @exported-using-ghexport Differential Revision: [D93000169](https://our.internmc.facebook.com/intern/diff/D93000169/)
1 parent 3ccdb8e commit f6136e9

11 files changed

Lines changed: 268 additions & 43 deletions

File tree

backends/vulkan/op_registry.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,6 @@ def register_q8ta_add():
510510
return OpFeatures(
511511
inputs_storage=utils.PACKED_INT8_BUFFER,
512512
supports_resize=False,
513-
supports_prepacking=True,
514513
)
515514

516515

backends/vulkan/runtime/api/containers/StagingBuffer.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,12 @@ StagingBuffer::StagingBuffer(
136136
const vkapi::CopyDirection direction)
137137
: context_p_(context_p),
138138
dtype_(get_staging_dtype(context_p, dtype)),
139+
// For 8-bit types, align numel to the next multiple of 4. Devices that
140+
// lack 8-bit storage buffer support will interpret the data as int32, so
141+
// the buffer size must be a multiple of 4 bytes.
139142
vulkan_buffer_(context_p_->adapter_ptr()->vma().create_staging_buffer(
140-
element_size(dtype_) * numel,
143+
element_size(dtype_) *
144+
(element_size(dtype_) == 1 ? utils::align_up_4(numel) : numel),
141145
direction)),
142146
mapped_data_(nullptr) {}
143147

backends/vulkan/runtime/graph/ops/glsl/indexing.glslh

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -334,45 +334,66 @@ TensorIndex linear_idx_to_tensor_idx(
334334
/*
335335
* Convert a linear texel index to a TensorIndex4D.
336336
*
337-
* This function is used for texel-based dispatch where each thread handles
338-
* one packed texel (4 elements along the packed dimension). The texel index
339-
* is decomposed using the dim_order and strides from the tensor's layout.
337+
* This is the inverse of tensor4d_idx_to_texel_idx. It handles both
338+
* single-packed layouts (outer_block_size == 1) and block-packed layouts
339+
* (e.g., 4W4C where outer_block_size > 1).
340340
*
341-
* The strides in BufferMetadata should already be in texel space (with packed
342-
* dimension size divided by 4).
341+
* The approach mirrors tensor4d_idx_to_texel_idx by decomposing the problem
342+
* into two levels:
343+
* 1. Decompose texel_idx into block_idx and intra-block texel offset
344+
* 2. Decompose block_idx into block-space tensor coordinates using strides
345+
* 3. Convert block-space coordinates to element-space by multiplying by
346+
* block sizes
347+
* 4. Add the intra-block outer-dimension offset
348+
*
349+
* For single-packed layouts (outer_block_size == 1, inner_dim == outer_dim),
350+
* texels_per_block == 1, so block_idx == texel_idx and intra_block_texel == 0.
351+
* The only effective multiplication is tidx[inner_dim] *= inner_block_size
352+
* (i.e., *= 4), matching the previous single-packed behavior.
343353
*
344354
* Parameters:
345-
* meta: BufferMetadata with tensor sizes and texel-space strides
355+
* meta: BufferMetadata with block-space strides
346356
* texel_idx: Linear index into packed texels (0 to num_texels-1)
347357
* hashed_layout: Packed layout info containing dim_order and packed_dim
348358
*
349-
* Returns: TensorIndex4D with logical tensor coordinates (packed dim is base of 4-element block)
359+
* Returns: TensorIndex4D with logical tensor coordinates (packed dims are
360+
* base of their respective blocks)
350361
*/
351362
TensorIndex4D texel_idx_to_tensor4d_idx(
352363
const BufferMetadata meta,
353364
uint texel_idx,
354365
const int hashed_layout) {
355366
TensorIndex4D tidx;
356367

357-
const int packed_dim = get_packed_dim(hashed_layout);
368+
const int inner_dim = get_packed_dim(hashed_layout);
369+
const int outer_dim = get_outer_packed_dim(hashed_layout);
370+
const int inner_block_size = get_packed_dim_block_size(hashed_layout);
371+
const int outer_block_size = get_outer_packed_dim_block_size(hashed_layout);
358372

359-
// Decompose texel_idx using dim_order from hashed_layout and strides from meta
360-
// Iterate from slowest-varying dimension (d=3) to fastest (d=0)
361-
// This follows the pattern of linear_idx_to_tensor_idx in indexing.glslh
373+
// Number of texels per block: each block has inner_block_size *
374+
// outer_block_size elements, and each texel holds 4 elements
375+
const int texels_per_block = (inner_block_size * outer_block_size) / 4;
376+
377+
// Decompose texel_idx into block_idx and intra-block texel offset
378+
const uint block_idx = texel_idx / texels_per_block;
379+
const int intra_block_texel = int(texel_idx % texels_per_block);
380+
381+
// Decompose block_idx into block-space tensor coordinates using dim_order
382+
// and strides. Iterate from slowest-varying (d=3) to fastest (d=0).
383+
uint remaining = block_idx;
362384
[[unroll]] for (int d = 3; d >= 0; d--) {
363-
// Get dim index from hashed_layout's dim_order (bits 0-15)
364385
int dim_idx = extract_4b(hashed_layout, d);
365-
366-
// Get stride for this dimension from BufferMetadata
367386
uint dim_stride = meta.strides[0][dim_idx];
368-
369-
// Compute coordinate for this dimension
370-
tidx.data[dim_idx] = int(texel_idx / dim_stride);
371-
texel_idx = texel_idx % dim_stride;
387+
tidx.data[dim_idx] = int(remaining / dim_stride);
388+
remaining = remaining % dim_stride;
372389
}
373390

374-
// Convert packed dimension from texel index to element index
375-
tidx.data[packed_dim] *= 4;
391+
// Convert block-space coordinates to element-space
392+
tidx.data[inner_dim] *= inner_block_size;
393+
tidx.data[outer_dim] *= outer_block_size;
394+
395+
// Add intra-block outer-dimension offset
396+
tidx.data[outer_dim] += intra_block_texel;
376397

377398
return tidx;
378399
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#version 450 core
10+
11+
#define PRECISION ${PRECISION}
12+
13+
${define_active_storage_type("buffer")}
14+
15+
layout(std430) buffer;
16+
17+
#include "indexing.glslh"
18+
19+
// Output buffer: packed int8x4 values (each int32 contains 4 packed int8)
20+
${layout_declare_tensor(B, "w", "t_outp", "int", "buffer")}
21+
// Input staging buffer: raw int8 data interpreted as int32 for device compat
22+
${layout_declare_tensor(B, "r", "nchw_in", "int", "buffer")}
23+
24+
// Metadata for output tensor
25+
${layout_declare_ubo(B, "BufferMetadata", "outp")}
26+
27+
layout(local_size_x_id = 0, local_size_y_id = 1, local_size_z_id = 2) in;
28+
29+
${layout_declare_spec_const(C, "int", "outp_layout", "CONTIG_LAYOUT_INT")}
30+
31+
void main() {
32+
const uint texel_idx = gl_GlobalInvocationID.x;
33+
const uint num_texels = numel(outp) / 4;
34+
if (texel_idx >= num_texels) {
35+
return;
36+
}
37+
38+
const int inner_dim = get_packed_dim(outp_layout);
39+
const int outer_dim = get_outer_packed_dim(outp_layout);
40+
41+
const TensorIndex4D tidx =
42+
texel_idx_to_tensor4d_idx(outp, texel_idx, outp_layout);
43+
44+
// Bounds check on outer dimension
45+
if (tidx.data[outer_dim] >= int(outp.sizes[0][outer_dim])) {
46+
return;
47+
}
48+
49+
// Tensor sizes in WHCN order for NCHW contiguous index computation
50+
const uint W = outp.sizes[0][0];
51+
const uint H = outp.sizes[0][1];
52+
const uint C = outp.sizes[0][2];
53+
54+
// Pack 4 int8 values along inner dimension into one int32
55+
int packed = 0;
56+
[[unroll]] for (int i = 0; i < 4; ++i) {
57+
const int elem_inner = tidx.data[inner_dim] + i;
58+
if (elem_inner < int(outp.sizes[0][inner_dim])) {
59+
// Build element coordinates
60+
ivec4 elem = tidx.data;
61+
elem[inner_dim] = elem_inner;
62+
63+
// Compute NCHW contiguous index: w + h*W + c*H*W + n*C*H*W
64+
const uint nchw_idx = uint(elem[0]) + uint(elem[1]) * W +
65+
uint(elem[2]) * H * W + uint(elem[3]) * C * H * W;
66+
67+
// Read int8 from staging buffer (each int32 contains 4 bytes)
68+
const uint int_idx = nchw_idx >> 2;
69+
const uint byte_pos = nchw_idx & 3;
70+
const int staging_val = nchw_in[int_idx];
71+
const int byte_val = (staging_val >> (byte_pos * 8)) & 0xFF;
72+
73+
packed |= (byte_val << (i * 8));
74+
}
75+
}
76+
77+
t_outp[texel_idx] = packed;
78+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
nchw_to_int8x4_buffer:
8+
parameter_names_with_default_values:
9+
DTYPE: int
10+
shader_variants:
11+
- NAME: nchw_to_int8x4_buffer
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Q8taStaging.h>
10+
11+
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Common.h>
12+
#include <executorch/backends/vulkan/runtime/graph/ops/utils/ShaderNameUtils.h>
13+
14+
namespace vkcompute {
15+
16+
void add_staging_to_int8x4_buffer_node(
17+
ComputeGraph& graph,
18+
const ValueRef tensor_data,
19+
const ValueRef tensor) {
20+
VK_CHECK_COND(graph.dtype_of(tensor) == vkapi::kInt8x4);
21+
22+
std::string kernel_name = "nchw_to_int8x4_buffer";
23+
24+
vkapi::ParamsBindList param_buffers;
25+
param_buffers.append(graph.buffer_meta_ubo(tensor));
26+
27+
// One thread per texel (each texel = one int32 = 4 packed int8).
28+
// Use padded_numel to account for dimension padding in packed int8 layouts
29+
// (e.g., kPackedInt8_4C with C=3 pads to C=4).
30+
uint32_t num_texels =
31+
utils::safe_downcast<uint32_t>(graph.padded_numel_of(tensor) / 4);
32+
utils::uvec3 global_wg_size = {num_texels, 1, 1};
33+
utils::uvec3 local_wg_size = graph.create_local_wg_size(global_wg_size);
34+
35+
graph.prepack_nodes().emplace_back(new PrepackNode(
36+
graph,
37+
VK_KERNEL_FROM_STR(kernel_name),
38+
global_wg_size,
39+
local_wg_size,
40+
// Input and Output
41+
tensor_data,
42+
tensor,
43+
// Parameter Buffers
44+
param_buffers,
45+
// Specialization Constants
46+
{graph.hashed_layout_of(tensor)}));
47+
}
48+
49+
} // namespace vkcompute
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#pragma once
10+
11+
#include <executorch/backends/vulkan/runtime/graph/ComputeGraph.h>
12+
13+
namespace vkcompute {
14+
15+
void add_staging_to_int8x4_buffer_node(
16+
ComputeGraph& graph,
17+
const ValueRef tensor_data,
18+
const ValueRef tensor);
19+
20+
} // namespace vkcompute

backends/vulkan/runtime/graph/ops/impl/Staging.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
#include <executorch/backends/vulkan/runtime/graph/ops/DynamicDispatchNode.h>
1414
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Common.h>
15+
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Q8taStaging.h>
1516
#include <executorch/backends/vulkan/runtime/graph/ops/utils/ShaderNameUtils.h>
1617
#include <executorch/backends/vulkan/runtime/graph/ops/utils/StagingUtils.h>
1718

@@ -327,6 +328,9 @@ ValueRef prepack_int4_linear_weight_transposed_interleaved(
327328
}
328329

329330
void prepack_op(ComputeGraph& graph, const std::vector<ValueRef>& args) {
331+
if (graph.dtype_of(args[1]) == vkapi::kInt8x4) {
332+
return add_staging_to_int8x4_buffer_node(graph, args[0], args[1]);
333+
}
330334
return add_prepack_standard_node(graph, args[0], args[1]);
331335
}
332336

backends/vulkan/runtime/graph/ops/utils/ShaderNameUtils.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ void add_dtype_suffix(std::string& kernel_name, const vkapi::ScalarType dtype) {
6464
case vkapi::kUInt64:
6565
kernel_name += "_uint64";
6666
break;
67+
case vkapi::kInt8x4:
68+
kernel_name += "_int32";
69+
break;
6770
default:
6871
break;
6972
}

backends/vulkan/test/custom_ops/impl/TestQ8taBinary.cpp

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010

1111
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Q8taBinary.h>
1212
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Q8taQuantizeDequantize.h>
13+
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Q8taStaging.h>
1314

1415
namespace vkcompute {
1516

1617
void q8ta_add_test(ComputeGraph& graph, const std::vector<ValueRef>& args) {
1718
int32_t idx = 0;
18-
const ValueRef fp_input_a = args.at(idx++);
19-
const ValueRef fp_input_b = args.at(idx++);
19+
ValueRef fp_input_a = args.at(idx++);
20+
ValueRef input_b = args.at(idx++);
2021
const ValueRef input_a_scale = args.at(idx++);
2122
const ValueRef input_a_zp = args.at(idx++);
2223
const ValueRef input_b_scale = args.at(idx++);
@@ -32,6 +33,10 @@ void q8ta_add_test(ComputeGraph& graph, const std::vector<ValueRef>& args) {
3233
utils::GPUMemoryLayout quant_layout =
3334
static_cast<utils::GPUMemoryLayout>(layout_value);
3435

36+
// Check if input_b is a pre-quantized int8 TensorRef
37+
bool input_b_is_int8 =
38+
graph.val_is_tref(input_b) && graph.dtype_of(input_b) == vkapi::kChar;
39+
3540
// Create temporary tensors for quantized data with the specified layout
3641
TmpTensor packed_int8_input_a(
3742
&graph,
@@ -40,12 +45,8 @@ void q8ta_add_test(ComputeGraph& graph, const std::vector<ValueRef>& args) {
4045
utils::kBuffer,
4146
quant_layout);
4247

43-
TmpTensor packed_int8_input_b(
44-
&graph,
45-
graph.sizes_of(fp_input_b),
46-
vkapi::kInt8x4,
47-
utils::kBuffer,
48-
quant_layout);
48+
ValueRef packed_int8_input_b = graph.add_tensor(
49+
graph.sizes_of(input_b), vkapi::kInt8x4, utils::kBuffer, quant_layout);
4950

5051
TmpTensor packed_int8_output(
5152
&graph,
@@ -54,12 +55,19 @@ void q8ta_add_test(ComputeGraph& graph, const std::vector<ValueRef>& args) {
5455
utils::kBuffer,
5556
quant_layout);
5657

57-
// Quantize: FP -> int8x4 with specified layout
58+
// Quantize input A: FP -> int8x4
5859
add_q8ta_quantize_node(
5960
graph, fp_input_a, input_a_scale, input_a_zp, packed_int8_input_a);
6061

61-
add_q8ta_quantize_node(
62-
graph, fp_input_b, input_b_scale, input_b_zp, packed_int8_input_b);
62+
if (input_b_is_int8) {
63+
// Input B is a pre-quantized int8 TensorRef; prepack directly into packed
64+
// int8x4 format
65+
add_staging_to_int8x4_buffer_node(graph, input_b, packed_int8_input_b);
66+
} else {
67+
// Input B is a float tensor; quantize at runtime
68+
add_q8ta_quantize_node(
69+
graph, input_b, input_b_scale, input_b_zp, packed_int8_input_b);
70+
}
6371

6472
// Binary add: int8x4 -> int8x4 (same layout for all tensors)
6573
add_q8ta_binary_node(

0 commit comments

Comments
 (0)