Skip to content

Commit 7f5c264

Browse files
authored
Gate output-centric image_to_nchw staging to discrete GPUs (#21022)
Differential Revision: D112599710 Pull Request resolved: #21022
1 parent 294c328 commit 7f5c264

4 files changed

Lines changed: 103 additions & 22 deletions

File tree

backends/vulkan/runtime/graph/ops/glsl/image_to_nchw.glsl

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ ${define_explicit_type_extensions(DTYPE)}
1919

2020
${define_active_storage_type(STORAGE)}
2121

22+
$if TO_STAGING:
23+
#define TO_STAGING
24+
$if COALESCED_WRITES:
25+
#define COALESCED_WRITES
26+
27+
// The coalesced path writes contiguous NCHW staging offsets directly and does
28+
// not honor buf_meta / buf_layout, so it is only valid for the staging
29+
// destination. Fail the build if a coalesced non-staging (clone) variant is
30+
// ever configured.
31+
#if defined(COALESCED_WRITES) && !defined(TO_STAGING)
32+
#error "COALESCED_WRITES requires TO_STAGING"
33+
#endif
34+
2235
layout(std430) buffer;
2336

2437
#include "indexing.glslh"
@@ -39,17 +52,53 @@ $if not TO_STAGING:
3952
${layout_declare_spec_const(C, "int", "buf_layout", "CONTIG_LAYOUT_INT")}
4053

4154
void main() {
55+
#ifdef COALESCED_WRITES
56+
// Output-centric dispatch: one thread per staging (NCHW) element. Consecutive
57+
// threads write consecutive staging offsets, so writes to the host-visible
58+
// staging buffer are fully coalesced. On a discrete GPU the staging buffer is
59+
// PCIe-backed, where coalescing the writes matters far more than coalescing
60+
// the (VRAM-cached) texture reads -- even though each texel is now fetched up
61+
// to 4 times, once per component. On unified-memory (mobile) GPUs the extra
62+
// fetches are a net loss, so this variant is gated to discrete GPUs.
63+
const int oi = int(gl_GlobalInvocationID.x);
64+
const int W = inp.sizes.x;
65+
const int H = inp.sizes.y;
66+
const int C = inp.sizes.z;
67+
const int N = inp.sizes.w;
68+
if (oi >= W * H * C * N) {
69+
return;
70+
}
71+
72+
TensorIndex4D tidx;
73+
tidx.data.x = oi % W;
74+
tidx.data.y = (oi / W) % H;
75+
tidx.data.z = (oi / (W * H)) % C;
76+
tidx.data.w = oi / (W * H * C);
77+
78+
const TextureElementIndex tex =
79+
tensor4d_idx_to_texture_element_idx_simple(inp, tidx, in_layout);
80+
#ifdef USING_TEXTURE2D
81+
const VEC4_T intex = texelFetch(t_in, tex.pos.xy, 0);
82+
#else
83+
const VEC4_T intex = texelFetch(t_in, tex.pos, 0);
84+
#endif
85+
buf_out[oi] = BUF_T(intex[tex.comp]);
86+
#else
87+
// Texel-centric dispatch: one thread per texture texel, writing up to 4 packed
88+
// components. Reads are coalesced (one fetch per texel); writes are strided.
89+
// Preferred on unified-memory GPUs (mobile) where the staging buffer is not
90+
// PCIe-backed, so write coalescing buys nothing and the single fetch wins.
4291
const ivec3 pos = ivec3(gl_GlobalInvocationID);
4392
if (out_of_bounds(pos, inp)) {
4493
return;
4594
}
4695

4796
TensorIndex4D tidx = texture_pos_to_tensor4d_idx_simple(inp, pos, in_layout);
48-
#ifdef USING_TEXTURE2D
97+
#ifdef USING_TEXTURE2D
4998
const VEC4_T intex = texelFetch(t_in, pos.xy, 0);
50-
#else
99+
#else
51100
const VEC4_T intex = texelFetch(t_in, pos, 0);
52-
#endif
101+
#endif
53102

54103
const int packed_dim = get_packed_dim(in_layout);
55104
int packed_dim_val;
@@ -77,16 +126,17 @@ void main() {
77126
int limit = min(4, packed_dim_size - packed_dim_val);
78127

79128
for (int comp = 0; comp < limit; comp++) {
80-
$if TO_STAGING:
81-
// Compute contiguous NCHW index
82-
int nchwi = tidx.data.x
83-
+ tidx.data.y * inp.sizes.x
84-
+ tidx.data.z * inp.sizes.x * inp.sizes.y
85-
+ tidx.data.w * inp.sizes.x * inp.sizes.y * inp.sizes.z;
86-
buf_out[nchwi] = BUF_T(intex[comp]);
87-
$else:
88-
int bufi = tensor4d_idx_to_buf_idx(buf_meta, tidx, buf_layout);
89-
buf_out[bufi] = BUF_T(intex[comp]);
129+
#ifdef TO_STAGING
130+
// Staging buffer is contiguous NCHW; compute the flat offset directly.
131+
int nchwi = tidx.data.x
132+
+ tidx.data.y * inp.sizes.x
133+
+ tidx.data.z * inp.sizes.x * inp.sizes.y
134+
+ tidx.data.w * inp.sizes.x * inp.sizes.y * inp.sizes.z;
135+
buf_out[nchwi] = BUF_T(intex[comp]);
136+
#else
137+
int bufi = tensor4d_idx_to_buf_idx(buf_meta, tidx, buf_layout);
138+
buf_out[bufi] = BUF_T(intex[comp]);
139+
#endif
90140

91141
if (packed_dim == 0) {
92142
tidx.data.x++;
@@ -98,4 +148,5 @@ void main() {
98148
tidx.data.w++;
99149
}
100150
}
151+
#endif
101152
}

backends/vulkan/runtime/graph/ops/glsl/image_to_nchw.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ image_to_nchw:
1010
BUF_DTYPE: float
1111
STORAGE: texture3d
1212
TO_STAGING: True
13+
COALESCED_WRITES: False
1314
generate_variant_forall:
1415
combination:
1516
parameter_names: [DTYPE, BUF_DTYPE]
@@ -25,5 +26,10 @@ image_to_nchw:
2526
- NAME: image_to_nchw_texture3d
2627
- NAME: image_to_nchw_texture2d
2728
STORAGE: texture2d
29+
- NAME: image_to_nchw_coalesced_texture3d
30+
COALESCED_WRITES: True
31+
- NAME: image_to_nchw_coalesced_texture2d
32+
STORAGE: texture2d
33+
COALESCED_WRITES: True
2834
- NAME: clone_image_to_buffer
2935
TO_STAGING: False

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

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ bool is_nchw_to_bitw8_shader(const vkapi::ShaderInfo& shader) {
3636
return shader_prefix_str == kNchwToBitw8PrefixStr;
3737
}
3838

39+
bool is_coalesced_image_to_nchw_shader(const vkapi::ShaderInfo& shader) {
40+
return shader.kernel_name.find("image_to_nchw_coalesced") !=
41+
std::string::npos;
42+
}
43+
3944
void add_staging_to_tensor_node(
4045
ComputeGraph& graph,
4146
const ValueRef in_staging,
@@ -94,19 +99,26 @@ utils::uvec3 tensor_to_staging_global_wg_size(
9499

95100
utils::uvec3 global_wg_size = graph->create_global_wg_size(in_tensor);
96101

97-
// Normally, the image_to_nchw shader is structured so that each thread reads
98-
// one texel from the input texture and writes each component of the texel
99-
// into the corresponding location in the output buffer. However, this shader
100-
// is structured slightly differently in that each thread writes out a
101-
// complete 32 bit integer (containing 4 packed 8-bit integers) into the
102-
// output buffer. Therefore, the global work group size for this shader will
103-
// be the number of elements in the output buffer divided by 4, as opposed to
104-
// the extents of the input texture.
102+
// The bitw8 shader writes out a complete 32 bit integer (containing 4 packed
103+
// 8-bit integers) per thread, so its global work group size is the number of
104+
// elements in the output buffer divided by 4.
105105
if (is_bitw8_shader(shader)) {
106106
const uint32_t buffer_len = utils::safe_downcast<uint32_t>(
107107
graph->get_staging(out_staging)->numel() / 4);
108108
global_wg_size = {buffer_len, 1, 1};
109+
} else if (is_coalesced_image_to_nchw_shader(shader)) {
110+
// The coalesced (output-centric) image_to_nchw variant dispatches one
111+
// thread per output (staging) element so that consecutive threads write
112+
// consecutive NCHW offsets, keeping writes to the PCIe-backed staging
113+
// buffer fully coalesced. This mirrors the buffer_to_nchw path, whose
114+
// global size is already numel-based via create_global_wg_size.
115+
const uint32_t buffer_len = utils::safe_downcast<uint32_t>(
116+
graph->get_staging(out_staging)->numel());
117+
global_wg_size = {buffer_len, 1, 1};
109118
}
119+
// Otherwise (texel-centric image_to_nchw, used on unified-memory GPUs) keep
120+
// the default texel-grid global size from create_global_wg_size: one thread
121+
// per texture texel.
110122

111123
return global_wg_size;
112124
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,19 @@ vkapi::ShaderInfo get_tensor_to_nchw_shader(
8080
return VK_KERNEL_FROM_STR(kernel_name);
8181
}
8282

83-
kernel_name = "image_to_nchw";
83+
// On a discrete GPU the staging buffer is read back over PCIe, so the
84+
// output-centric (coalesced-write) variant is a large win. On integrated GPUs
85+
// the staging buffer is not PCIe-backed, and the coalesced variant's
86+
// redundant texture fetches make it a net loss -- so default to the
87+
// texel-centric variant there.
88+
//
89+
// Gate on device type, not has_unified_memory(): a discrete GPU with
90+
// Resizable BAR exposes a DEVICE_LOCAL | HOST_VISIBLE memory type, so
91+
// has_unified_memory() returns true for it and would wrongly route it to the
92+
// texel-centric path (measured ~10x end-to-end regression on an RTX 4080).
93+
const bool coalesced_writes =
94+
!graph.context()->adapter_ptr()->is_integrated_gpu();
95+
kernel_name = coalesced_writes ? "image_to_nchw_coalesced" : "image_to_nchw";
8496
add_storage_type_suffix(kernel_name, src_storage_type);
8597
add_dtype_suffix(kernel_name, src_dtype);
8698
add_dtype_suffix(kernel_name, staging_dtype);

0 commit comments

Comments
 (0)