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
22- * max_texture2d_dim. IN_STORAGE=texture3d codegen.
23- * - buffer: vec4 at offset m*K4 + k4, same K packing.
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 the per-tile 2D
26+ * extent (M_TILE = OH_TILE * W_out) would exceed max_texture2d_dim — rare,
27+ * since OH_TILE is capped by the scratch byte budget. IN_STORAGE=texture3d
28+ * codegen.
29+ * - buffer: vec4 at offset r*K4 + k4, same K packing.
2430 * IN_STORAGE=buffer codegen.
2531 *
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.
32+ * The matmul interpretation (over this tile's rows) is:
33+ * out[r, n] = sum_k im2col[r, k] * weight[n, k] + bias[n]
34+ * with K = K_total, N = C_out, and r the tile-local row whose global output
35+ * spatial position is (OH_OFFSET + r / W_out, r % W_out).
2936 */
3037
3138#version 450 core
@@ -85,14 +92,23 @@ ${layout_declare_ubo(B, "ivec4", "out_sizes")}
8592// dims), so it is safe to bake at build time even under dynamic shapes.
8693// M = H_out * W_out IS shape-dependent, so it is derived at runtime from the
8794// refreshed out_sizes UBO in main() rather than read from here.
95+ //
96+ // This dispatch consumes one tile of the im2col matrix: OH_TILE output-height
97+ // rows starting at output-height row OH_OFFSET. The im2col scratch (t_in) holds
98+ // OH_TILE * W_out tile-local rows; the GEMM reads tile-local rows and writes the
99+ // output at the corresponding global spatial position (OH_OFFSET + oh_local,
100+ // ow). OH_OFFSET / OH_TILE are shape-independent (fixed at build time); the
101+ // global W_out / H_out come from the refreshed out_sizes UBO.
88102layout (push_constant) uniform restrict Block {
89- ivec4 gemm_dims; // (K4_total, _unused, _unused , _unused)
103+ ivec4 gemm_dims; // (K4_total, OH_OFFSET, OH_TILE , _unused)
90104 vec4 clamp_vals; // (out_min, out_max, _unused, _unused)
91105};
92106
93- #define K4_TOTAL gemm_dims.x
94- #define OUT_MIN clamp_vals.x
95- #define OUT_MAX clamp_vals.y
107+ #define K4_TOTAL gemm_dims.x
108+ #define OH_OFFSET gemm_dims.y
109+ #define OH_TILE gemm_dims.z
110+ #define OUT_MIN clamp_vals.x
111+ #define OUT_MAX clamp_vals.y
96112
97113layout (local_size_x_id = 0 , local_size_y_id = 1 , local_size_z_id = 2 ) in ;
98114
@@ -104,30 +120,32 @@ ${layout_declare_spec_const(C, "int", "activation_type", "0")}
104120
105121/*
106122 * 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.
123+ * The im2col scratch holds M_TILE tile-local rows in a contiguous
124+ * (M_TILE, K_total/4) matrix of vec4s; row here is the tile-local index, so the
125+ * load is a plain 2D fetch — no spatial decomposition. (The output store, not
126+ * this load, maps the tile-local row to its global spatial position.)
109127 */
110128void load_input_tile_with_checks(
111129 out FPInputTile tile,
112130 const int k4_start,
113131 const int m_start,
114132 const int K4,
115- const int M ,
133+ const int M_TILE ,
116134 const int W_out) {
117135 // W_out is only consumed by the texture3d variant below.
118136 [[unroll]] for (int m = 0 ; m < TILE_M; ++ m) {
119137 [[unroll]] for (int k4 = 0 ; k4 < TILE_K4; ++ k4) {
120- if (k4_start + k4 < K4 && m_start + m < M ) {
138+ if (k4_start + k4 < K4 && m_start + m < M_TILE ) {
121139 const int row = m_start + m;
122140 const int col = k4_start + k4;
123141#if defined(INPUT_BUFFER)
124142 // Cast SSBO texel into the input tile type (f16vec4 for half, vec4 for
125143 // float).
126144 tile.data[m][k4] = LINEAR_FP_INPUT_TILE_VEC4_T(t_in[row * K4 + col]);
127145#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.
146+ // texture3d scratch [1, K_total, OH_TILE, W_out]: the tile-local row
147+ // decomposes into (ow, oh_local) and K4 is along the Z axis. texelFetch
148+ // returns vec4 (fp32); cast to the input tile type.
131149 tile.data[m][k4] = LINEAR_FP_INPUT_TILE_VEC4_T(
132150 texelFetch(t_in, ivec3 (row % W_out, row / W_out, col), 0 ));
133151#else
@@ -141,17 +159,28 @@ void load_input_tile_with_checks(
141159 }
142160}
143161
162+ // m_start is a tile-local row offset; the scratch read uses it directly, but
163+ // the output store maps it to the GLOBAL spatial position via oh_global =
164+ // OH_OFFSET + (m_local / W_out). Rows whose global oh lands past H_out (the
165+ // partial trailing tile, or a dynamic shape that shrinks H_out) are skipped by
166+ // the `oh < H_out` guard. The companion `m_local < M_TILE` guard enforces the
167+ // tile's UPPER oh bound: since M_TILE = OH_TILE * W_out, m_local < M_TILE means
168+ // oh_local < OH_TILE, i.e. oh < OH_OFFSET + OH_TILE — so no row this dispatch
169+ // writes can leak into a neighboring tile's output-row range.
144170void store_output_tile_with_checks(
145171 const FPOutTile out_tile,
146172 const int n4_start,
147173 const int m_start,
148174 const int N4,
149- const int M,
175+ const int M_TILE,
176+ const int H_out,
150177 const int W_out) {
151178 [[unroll]] for (int m = 0 ; m < TILE_M; ++ m) {
152179 [[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;
180+ const int m_local = m_start + m;
181+ const int ow = m_local % W_out;
182+ const int oh = OH_OFFSET + m_local / W_out;
183+ if (m_local < M_TILE && oh < H_out && n4_start + n4 < N4) {
155184 // Cast the accumulator (f16vec4 for the buffer/half path) to the
156185 // texture3d output surface type for the activation clamp and store.
157186 OUT_VEC4_T texel = OUT_VEC4_T(out_tile.data[m][n4]);
@@ -160,8 +189,7 @@ void store_output_tile_with_checks(
160189 } else if (activation_type == 2 ) {
161190 texel = clamp (texel, OUT_VEC4_T(OUT_MIN), OUT_VEC4_T(OUT_MAX));
162191 }
163- imageStore(
164- t_out, ivec3 (spatial % W_out, spatial / W_out, n4_start + n4), texel);
192+ imageStore(t_out, ivec3 (ow, oh, n4_start + n4), texel);
165193 }
166194 }
167195 }
@@ -176,14 +204,16 @@ void main() {
176204
177205 const int W_out = out_sizes.x;
178206 const int H_out = out_sizes.y;
179- // M = H_out * W_out is derived from the refreshed out_sizes UBO so it tracks
207+ // W_out / H_out are derived from the refreshed out_sizes UBO so they track
180208 // dynamic output shapes (out_sizes is virtual_resize'd on trigger_resize).
181- const int M = W_out * H_out;
209+ // M_TILE = OH_TILE * W_out is the tile-local row count materialized in the
210+ // im2col scratch (t_in); the GEMM reads scratch rows in [0, M_TILE).
211+ const int M_TILE = OH_TILE * W_out;
182212 const int K4 = K4_TOTAL;
183213 const int N = out_sizes.z;
184214 const int N4 = div_up_4(N);
185215
186- if (n4_start >= N4 || m_start >= M ) {
216+ if (n4_start >= N4 || m_start >= M_TILE ) {
187217 return ;
188218 }
189219
@@ -194,7 +224,7 @@ void main() {
194224 FPWeightTile w_tile;
195225
196226 for (int k4 = 0 ; k4 < K4; k4 += TILE_K4) {
197- load_input_tile_with_checks(in_tile, k4, m_start, K4, M , W_out);
227+ load_input_tile_with_checks(in_tile, k4, m_start, K4, M_TILE , W_out);
198228 load_packed_weight_tile_with_checks(w_tile, n4_start, k4, 0 , N4, K4);
199229 fp_accumulate_with_fp_weight(out_tile, in_tile, w_tile);
200230 }
@@ -213,5 +243,6 @@ void main() {
213243 }
214244 }
215245
216- store_output_tile_with_checks(out_tile, n4_start, m_start, N4, M, W_out);
246+ store_output_tile_with_checks(
247+ out_tile, n4_start, m_start, N4, M_TILE, H_out, W_out);
217248}
0 commit comments