Skip to content

Commit 33a3092

Browse files
Dobiasdclaude
andcommitted
Speed up Conv1D/Conv2D with large dilation_rate (closes #132)
Previously, Conv1D/Conv2D with dilation_rate > 1 inflated the filter with zeros at construction time (dilate_filter -> dilate_tensor). For example, a 3-tap kernel with dilation_rate=64 became a 129-tap kernel with 126 zero entries, and the Eigen GEMM still multiplied through every entry. Keep filters un-dilated and pass dilation_rate down to convolve(). For the strides=(1,1) fast path, loop over the un-dilated (y_filt, x_filt) positions and do one GEMM per position, indexing the input at (y_filt*dil_y, x_filt*dil_x). The s1x1 output_temp wraparound trick still works as long as out_width_temp = out_width + (f_width-1)*dil_x. Benchmark (3-stack WaveNet, 16 channels, input 18000x3, dilations 1..64): master: 159 ms this PR: 49 ms (3.3x faster; matches the no-dilation baseline of 46 ms) convolve_transposed and depthwise/separable conv keep pre-dilating filters (separate code paths, not the bottleneck reported in #132). Conv3D is left for a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 399a371 commit 33a3092

2 files changed

Lines changed: 134 additions & 16 deletions

File tree

include/fdeep/convolution.hpp

Lines changed: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,74 @@ namespace internal {
159159
return output;
160160
}
161161

162+
// Dilated variant: filter values are at positions (y_filt * dil_y, x_filt * dil_x) in the
163+
// effective receptive field. We loop over the un-dilated filter positions and do one GEMM
164+
// per (y_filt, x_filt), which avoids multiplying by the zeros of an inflated kernel.
165+
inline tensor convolve_accumulative_s1x1_dilated(
166+
std::size_t out_height,
167+
std::size_t out_width,
168+
const shape2& dilation_rate,
169+
const convolution_filter_matrices& filter_mat,
170+
const tensor& in)
171+
{
172+
const tensor& filter_mats = filter_mat.filter_mats_;
173+
const auto f_height = filter_mat.filter_shape_.height_;
174+
const auto f_width = filter_mat.filter_shape_.width_;
175+
const auto f_depth = filter_mat.filter_shape_.depth_;
176+
const auto out_depth = filter_mat.filter_count_;
177+
const auto dil_y = dilation_rate.height_;
178+
const auto dil_x = dilation_rate.width_;
179+
const auto eff_f_width = (f_width - 1) * dil_x + 1;
180+
181+
tensor output = init_conv_output_tensor(out_height, out_width, out_depth, in.shape().rank(), filter_mat);
182+
183+
const std::size_t out_width_temp = out_width + eff_f_width - 1;
184+
tensor output_temp(tensor_shape_with_changed_rank(
185+
tensor_shape(out_height, out_width_temp, out_depth),
186+
in.shape().rank()),
187+
static_cast<float_type>(0));
188+
189+
const auto mapping_width = out_width_temp * (out_height - 1) + out_width;
190+
191+
for (std::size_t y_filt = 0; y_filt < f_height; ++y_filt) {
192+
for (std::size_t x_filt = 0; x_filt < f_width; ++x_filt) {
193+
const Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned>
194+
filter(const_cast<float_type*>(&filter_mats.get_ref_ignore_rank(tensor_pos(0, y_filt, x_filt, 0, 0))),
195+
static_cast<EigenIndex>(out_depth),
196+
static_cast<EigenIndex>(f_depth));
197+
198+
const Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned, Eigen::OuterStride<>>
199+
input(const_cast<float_type*>(&in.get_ref_ignore_rank(tensor_pos(0, 0, y_filt * dil_y, x_filt * dil_x, 0))),
200+
static_cast<EigenIndex>(f_depth),
201+
static_cast<EigenIndex>(mapping_width),
202+
Eigen::OuterStride<>(static_cast<EigenIndex>(f_depth)));
203+
204+
Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned>
205+
output_temp_map(&output_temp.get_ref_ignore_rank(tensor_pos(0, 0, 0, 0, 0)),
206+
static_cast<EigenIndex>(out_depth),
207+
static_cast<EigenIndex>(mapping_width));
208+
209+
output_temp_map.noalias() += filter * input;
210+
}
211+
}
212+
213+
for (std::size_t y_out = 0; y_out < out_height; ++y_out) {
214+
for (std::size_t x_out = 0; x_out < out_width; ++x_out) {
215+
for (std::size_t z_out = 0; z_out < out_depth; ++z_out) {
216+
output.get_ref_ignore_rank(tensor_pos(0, 0, y_out, x_out, z_out)) += output_temp.get_ref_ignore_rank(tensor_pos(0, 0, y_out, x_out, z_out));
217+
}
218+
}
219+
}
220+
221+
return output;
222+
}
223+
162224
inline tensor convolve_accumulative(
163225
std::size_t out_height,
164226
std::size_t out_width,
165227
std::size_t strides_y,
166228
std::size_t strides_x,
229+
const shape2& dilation_rate,
167230
const convolution_filter_matrices& filter_mat,
168231
const tensor& in)
169232
{
@@ -176,31 +239,65 @@ namespace internal {
176239
const auto f_width = filter_mat.filter_shape_.width_;
177240
const auto f_depth = filter_mat.filter_shape_.depth_;
178241
const auto out_depth = filter_mat.filter_count_;
242+
const auto dil_y = dilation_rate.height_;
243+
const auto dil_x = dilation_rate.width_;
244+
const auto eff_f_height = (f_height - 1) * dil_y + 1;
245+
const auto eff_f_width = (f_width - 1) * dil_x + 1;
179246

180247
assertion(f_depth == in.shape().depth_, "filter depth does not match input");
181248
assertion(filter_mats.shape().size_dim_4_ == f_height, "incorrect number of filter levels in y direction");
182-
assertion(out_width == (in.shape().width_ - f_width) / strides_x + 1, "output width does not match");
249+
assertion(out_width == (in.shape().width_ - eff_f_width) / strides_x + 1, "output width does not match");
183250
assertion(out_depth == filter_mat.biases_.size(), "invlid bias count");
184251

185252
if (strides_x == 1 && strides_y == 1) {
186-
return convolve_accumulative_s1x1(out_height, out_width, filter_mat, in);
253+
if (dil_x == 1 && dil_y == 1) {
254+
return convolve_accumulative_s1x1(out_height, out_width, filter_mat, in);
255+
}
256+
return convolve_accumulative_s1x1_dilated(out_height, out_width, dilation_rate, filter_mat, in);
187257
}
188258

189259
tensor output = init_conv_output_tensor(out_height, out_width, out_depth, in.shape().rank(), filter_mat);
190260

191-
for (std::size_t y_filt = 0; y_filt < f_height; ++y_filt) {
192-
const Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned>
193-
filter(const_cast<float_type*>(&filter_mats.get_ref_ignore_rank(tensor_pos(0, y_filt, 0, 0, 0))),
194-
static_cast<EigenIndex>(out_depth),
195-
static_cast<EigenIndex>(f_width * f_depth));
196-
for (std::size_t y = 0, y_out = 0; y < in.shape().height_ + 1 - f_height; y += strides_y, ++y_out) {
197-
const auto input = get_im2col_mapping(in, f_width, f_depth, strides_x, out_width, y, y_filt);
198-
Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned>
199-
output_map(&output.get_ref_ignore_rank(tensor_pos(0, 0, y_out, 0, 0)),
261+
if (dil_x == 1 && dil_y == 1) {
262+
for (std::size_t y_filt = 0; y_filt < f_height; ++y_filt) {
263+
const Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned>
264+
filter(const_cast<float_type*>(&filter_mats.get_ref_ignore_rank(tensor_pos(0, y_filt, 0, 0, 0))),
200265
static_cast<EigenIndex>(out_depth),
201-
static_cast<EigenIndex>(out_width));
266+
static_cast<EigenIndex>(f_width * f_depth));
267+
for (std::size_t y = 0, y_out = 0; y < in.shape().height_ + 1 - f_height; y += strides_y, ++y_out) {
268+
const auto input = get_im2col_mapping(in, f_width, f_depth, strides_x, out_width, y, y_filt);
269+
Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned>
270+
output_map(&output.get_ref_ignore_rank(tensor_pos(0, 0, y_out, 0, 0)),
271+
static_cast<EigenIndex>(out_depth),
272+
static_cast<EigenIndex>(out_width));
273+
274+
output_map.noalias() += filter * input;
275+
}
276+
}
277+
return output;
278+
}
202279

203-
output_map.noalias() += filter * input;
280+
// Strided + dilated: nested f_height x f_width x out_height GEMMs.
281+
for (std::size_t y_filt = 0; y_filt < f_height; ++y_filt) {
282+
for (std::size_t x_filt = 0; x_filt < f_width; ++x_filt) {
283+
const Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned>
284+
filter(const_cast<float_type*>(&filter_mats.get_ref_ignore_rank(tensor_pos(0, y_filt, x_filt, 0, 0))),
285+
static_cast<EigenIndex>(out_depth),
286+
static_cast<EigenIndex>(f_depth));
287+
for (std::size_t y = 0, y_out = 0; y < in.shape().height_ + 1 - eff_f_height; y += strides_y, ++y_out) {
288+
const Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned, Eigen::OuterStride<>>
289+
input(const_cast<float_type*>(&in.get_ref_ignore_rank(tensor_pos(0, 0, y + y_filt * dil_y, x_filt * dil_x, 0))),
290+
static_cast<EigenIndex>(f_depth),
291+
static_cast<EigenIndex>(out_width),
292+
Eigen::OuterStride<>(static_cast<EigenIndex>(f_depth * strides_x)));
293+
294+
Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned>
295+
output_map(&output.get_ref_ignore_rank(tensor_pos(0, 0, y_out, 0, 0)),
296+
static_cast<EigenIndex>(out_depth),
297+
static_cast<EigenIndex>(out_width));
298+
299+
output_map.noalias() += filter * input;
300+
}
204301
}
205302
}
206303

@@ -301,14 +398,19 @@ namespace internal {
301398
inline tensor convolve(
302399
const shape2& strides,
303400
const padding& pad_type,
401+
const shape2& dilation_rate,
304402
const convolution_filter_matrices& filter_mat,
305403
const tensor& input)
306404
{
307405
assertion(filter_mat.filter_shape_.depth_ == input.shape().depth_,
308406
"invalid filter depth");
309407

408+
const shape2 eff_filter_shape(
409+
(filter_mat.filter_shape_.height_ - 1) * dilation_rate.height_ + 1,
410+
(filter_mat.filter_shape_.width_ - 1) * dilation_rate.width_ + 1);
411+
310412
const auto conv_cfg = preprocess_convolution(
311-
filter_mat.filter_shape_.without_depth(),
413+
eff_filter_shape,
312414
strides, pad_type, input.shape().height_, input.shape().width_, false);
313415

314416
// The padding step usually (on a VGG19 net) only takes about 1% of the overall runtime.
@@ -321,10 +423,23 @@ namespace internal {
321423
return convolve_accumulative(
322424
conv_cfg.out_height_, conv_cfg.out_width_,
323425
strides.height_, strides.width_,
426+
dilation_rate,
324427
filter_mat,
325428
in_padded);
326429
}
327430

431+
// Backward-compatible overload for callers (e.g., transposed conv, depthwise conv,
432+
// separable pointwise conv) where the filter has already been pre-dilated or
433+
// dilation does not apply.
434+
inline tensor convolve(
435+
const shape2& strides,
436+
const padding& pad_type,
437+
const convolution_filter_matrices& filter_mat,
438+
const tensor& input)
439+
{
440+
return convolve(strides, pad_type, shape2(1, 1), filter_mat, input);
441+
}
442+
328443
inline tensor convolve_transposed(
329444
const shape2& strides,
330445
const padding& pad_type,
@@ -348,6 +463,7 @@ namespace internal {
348463
return convolve_accumulative(
349464
conv_cfg.out_height_, conv_cfg.out_width_,
350465
1, 1,
466+
shape2(1, 1),
351467
filter_mat,
352468
in_padded);
353469
}

include/fdeep/layers/conv_2d_layer.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ namespace internal {
3030
const float_vec& weights, const float_vec& bias)
3131
: layer(name)
3232
, filters_(generate_im2col_filter_matrix(
33-
generate_filters(dilation_rate, filter_shape, k, weights, bias, false)))
33+
generate_filters(shape2(1, 1), filter_shape, k, weights, bias, false)))
34+
, dilation_rate_(dilation_rate)
3435
, strides_(strides)
3536
, padding_(p)
3637
{
@@ -43,9 +44,10 @@ namespace internal {
4344
tensors apply_impl(const tensors& inputs) const override
4445
{
4546
const auto& input = single_tensor_from_tensors(inputs);
46-
return { convolve(strides_, padding_, filters_, input) };
47+
return { convolve(strides_, padding_, dilation_rate_, filters_, input) };
4748
}
4849
convolution_filter_matrices filters_;
50+
shape2 dilation_rate_;
4951
shape2 strides_;
5052
padding padding_;
5153
};

0 commit comments

Comments
 (0)