99/*
1010 * conv2d_gemm: GEMM step of im2col-backed conv2d.
1111 *
12- * Reads the im2col'd input produced by conv2d_im2col.glsl as a 2D matrix
13- * of shape [M, K_total] (M = H_out * W_out, K_total = Kh*Kw*Cin_padded)
14- * and writes the conv2d output as texture3D channels-packed
15- * logical shape [1, C_out, H_out, W_out].
12+ * Reads one tile of the im2col'd input produced by conv2d_im2col.glsl — a 2D
13+ * matrix of shape [M_TILE, K_total] holding OH_TILE output-height rows
14+ * (M_TILE = OH_TILE * W_out, K_total = Kh*Kw*Cin_padded) starting at output
15+ * row OH_OFFSET — and writes the corresponding output rows as texture3D
16+ * channels-packed, logical shape [1, C_out, H_out, W_out]. The full im2col
17+ * matrix is processed OH_TILE rows per dispatch so the scratch tensor is bounded
18+ * to a fixed byte budget regardless of resolution; with tiling disabled the
19+ * caller passes OH_OFFSET = 0 and OH_TILE = H_out (one dispatch covers all M).
1620 *
17- * The im2col input can be any of:
18- * - texture2d, width-packed: texel at (k4, m ) holds 4 K values for row m.
19- * IN_STORAGE=texture2d codegen.
20- * - texture3d, channels-packed: texel at (ow, oh , k4) holds 4 K values
21- * for output spatial position (oh, ow) . Used when M would exceed
21+ * The im2col input tile can be any of:
22+ * - texture2d, width-packed: texel at (k4, r ) holds 4 K values for tile-local
23+ * row r. IN_STORAGE=texture2d codegen.
24+ * - texture3d, channels-packed: texel at (ow, oh_local , k4) holds 4 K values
25+ * for tile-local row r = oh_local * W_out + ow . Used when M would exceed
2226 * max_texture2d_dim. IN_STORAGE=texture3d codegen.
23- * - buffer: vec4 at offset m *K4 + k4, same K packing.
27+ * - buffer: vec4 at offset r *K4 + k4, same K packing.
2428 * IN_STORAGE=buffer codegen.
2529 *
26- * The matmul interpretation is:
27- * out[m, n] = sum_k im2col[m, k] * weight[n, k] + bias[n]
28- * with M = H_out * W_out, K = K_total, N = C_out.
30+ * The matmul interpretation (over this tile's rows) is:
31+ * out[r, n] = sum_k im2col[r, k] * weight[n, k] + bias[n]
32+ * with K = K_total, N = C_out, and r the tile-local row whose global output
33+ * spatial position is (OH_OFFSET + r / W_out, r % W_out).
2934 */
3035
3136#version 450 core
@@ -85,14 +90,23 @@ ${layout_declare_ubo(B, "ivec4", "out_sizes")}
8590// dims), so it is safe to bake at build time even under dynamic shapes.
8691// M = H_out * W_out IS shape-dependent, so it is derived at runtime from the
8792// refreshed out_sizes UBO in main() rather than read from here.
93+ //
94+ // This dispatch consumes one tile of the im2col matrix: OH_TILE output-height
95+ // rows starting at output-height row OH_OFFSET. The im2col scratch (t_in) holds
96+ // OH_TILE * W_out tile-local rows; the GEMM reads tile-local rows and writes the
97+ // output at the corresponding global spatial position (OH_OFFSET + oh_local,
98+ // ow). OH_OFFSET / OH_TILE are shape-independent (fixed at build time); the
99+ // global W_out / H_out come from the refreshed out_sizes UBO.
88100layout (push_constant) uniform restrict Block {
89- ivec4 gemm_dims; // (K4_total, _unused, _unused , _unused)
101+ ivec4 gemm_dims; // (K4_total, OH_OFFSET, OH_TILE , _unused)
90102 vec4 clamp_vals; // (out_min, out_max, _unused, _unused)
91103};
92104
93- #define K4_TOTAL gemm_dims.x
94- #define OUT_MIN clamp_vals.x
95- #define OUT_MAX clamp_vals.y
105+ #define K4_TOTAL gemm_dims.x
106+ #define OH_OFFSET gemm_dims.y
107+ #define OH_TILE gemm_dims.z
108+ #define OUT_MIN clamp_vals.x
109+ #define OUT_MAX clamp_vals.y
96110
97111layout (local_size_x_id = 0 , local_size_y_id = 1 , local_size_z_id = 2 ) in ;
98112
@@ -104,30 +118,32 @@ ${layout_declare_spec_const(C, "int", "activation_type", "0")}
104118
105119/*
106120 * Load TILE_M rows × TILE_K4 K-tiles of the im2col'd input.
107- * The im2col output is a contiguous (M, K_total/4) matrix of vec4s, so the
108- * load is a plain 2D fetch — no spatial decomposition.
121+ * The im2col scratch holds M_TILE tile-local rows in a contiguous
122+ * (M_TILE, K_total/4) matrix of vec4s; row here is the tile-local index, so the
123+ * load is a plain 2D fetch — no spatial decomposition. (The output store, not
124+ * this load, maps the tile-local row to its global spatial position.)
109125 */
110126void load_input_tile_with_checks(
111127 out FPInputTile tile,
112128 const int k4_start,
113129 const int m_start,
114130 const int K4,
115- const int M ,
131+ const int M_TILE ,
116132 const int W_out) {
117133 // W_out is only consumed by the texture3d variant below.
118134 [[unroll]] for (int m = 0 ; m < TILE_M; ++ m) {
119135 [[unroll]] for (int k4 = 0 ; k4 < TILE_K4; ++ k4) {
120- if (k4_start + k4 < K4 && m_start + m < M ) {
136+ if (k4_start + k4 < K4 && m_start + m < M_TILE ) {
121137 const int row = m_start + m;
122138 const int col = k4_start + k4;
123139#if defined(INPUT_BUFFER)
124140 // Cast SSBO texel into the input tile type (f16vec4 for half, vec4 for
125141 // float).
126142 tile.data[m][k4] = LINEAR_FP_INPUT_TILE_VEC4_T(t_in[row * K4 + col]);
127143#elif defined(INPUT_TEXTURE3D)
128- // texture3d layout: row (the flat M index) decomposes into (ow, oh)
129- // and K4 is along the Z axis. texelFetch returns vec4 (fp32); cast to
130- // the input tile type.
144+ // texture3d scratch [1, K_total, OH_TILE, W_out]: the tile-local row
145+ // decomposes into (ow, oh_local) and K4 is along the Z axis. texelFetch
146+ // returns vec4 (fp32); cast to the input tile type.
131147 tile.data[m][k4] = LINEAR_FP_INPUT_TILE_VEC4_T(
132148 texelFetch(t_in, ivec3 (row % W_out, row / W_out, col), 0 ));
133149#else
@@ -141,17 +157,24 @@ void load_input_tile_with_checks(
141157 }
142158}
143159
160+ // m_start is a tile-local row offset; the scratch read uses it directly, but
161+ // the output store maps it to the GLOBAL spatial position via oh_global =
162+ // OH_OFFSET + (m_local / W_out). Rows whose global oh lands past H_out (the
163+ // partial trailing tile, or a dynamic shape that shrinks H_out) are skipped.
144164void store_output_tile_with_checks(
145165 const FPOutTile out_tile,
146166 const int n4_start,
147167 const int m_start,
148168 const int N4,
149- const int M,
169+ const int M_TILE,
170+ const int H_out,
150171 const int W_out) {
151172 [[unroll]] for (int m = 0 ; m < TILE_M; ++ m) {
152173 [[unroll]] for (int n4 = 0 ; n4 < TILE_N4; ++ n4) {
153- if (m_start + m < M && n4_start + n4 < N4) {
154- const int spatial = m_start + m;
174+ const int m_local = m_start + m;
175+ const int ow = m_local % W_out;
176+ const int oh = OH_OFFSET + m_local / W_out;
177+ if (m_local < M_TILE && oh < H_out && n4_start + n4 < N4) {
155178 // Cast the accumulator (f16vec4 for the buffer/half path) to the
156179 // texture3d output surface type for the activation clamp and store.
157180 OUT_VEC4_T texel = OUT_VEC4_T(out_tile.data[m][n4]);
@@ -160,8 +183,7 @@ void store_output_tile_with_checks(
160183 } else if (activation_type == 2 ) {
161184 texel = clamp (texel, OUT_VEC4_T(OUT_MIN), OUT_VEC4_T(OUT_MAX));
162185 }
163- imageStore(
164- t_out, ivec3 (spatial % W_out, spatial / W_out, n4_start + n4), texel);
186+ imageStore(t_out, ivec3 (ow, oh, n4_start + n4), texel);
165187 }
166188 }
167189 }
@@ -176,14 +198,16 @@ void main() {
176198
177199 const int W_out = out_sizes.x;
178200 const int H_out = out_sizes.y;
179- // M = H_out * W_out is derived from the refreshed out_sizes UBO so it tracks
201+ // W_out / H_out are derived from the refreshed out_sizes UBO so they track
180202 // dynamic output shapes (out_sizes is virtual_resize'd on trigger_resize).
181- const int M = W_out * H_out;
203+ // M_TILE = OH_TILE * W_out is the tile-local row count materialized in the
204+ // im2col scratch (t_in); the GEMM reads scratch rows in [0, M_TILE).
205+ const int M_TILE = OH_TILE * W_out;
182206 const int K4 = K4_TOTAL;
183207 const int N = out_sizes.z;
184208 const int N4 = div_up_4(N);
185209
186- if (n4_start >= N4 || m_start >= M ) {
210+ if (n4_start >= N4 || m_start >= M_TILE ) {
187211 return ;
188212 }
189213
@@ -194,7 +218,7 @@ void main() {
194218 FPWeightTile w_tile;
195219
196220 for (int k4 = 0 ; k4 < K4; k4 += TILE_K4) {
197- load_input_tile_with_checks(in_tile, k4, m_start, K4, M , W_out);
221+ load_input_tile_with_checks(in_tile, k4, m_start, K4, M_TILE , W_out);
198222 load_packed_weight_tile_with_checks(w_tile, n4_start, k4, 0 , N4, K4);
199223 fp_accumulate_with_fp_weight(out_tile, in_tile, w_tile);
200224 }
@@ -213,5 +237,6 @@ void main() {
213237 }
214238 }
215239
216- store_output_tile_with_checks(out_tile, n4_start, m_start, N4, M, W_out);
240+ store_output_tile_with_checks(
241+ out_tile, n4_start, m_start, N4, M_TILE, H_out, W_out);
217242}
0 commit comments