Skip to content

Commit 2d34693

Browse files
Dobiasdclaude
andauthored
Add Conv3DTranspose, UpSampling3D, and stride-1×1×1 fast path for Conv3D (#456)
Builds on the Conv3D foundation: - Conv3DTranspose: dilates input then runs forward conv with strides (1,1,1) and reversed filters along all three spatial axes. - UpSampling3D: nearest-neighbor resize for volumetric tensors. - convolve_accumulative_s1x1x1_3d: collapses the spatial output loops into one big im2col mapping, mirroring the 2D fast path. Triggers automatically when strides are (1,1,1), so it also speeds up Conv3DTranspose. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ecfa571 commit 2d34693

9 files changed

Lines changed: 374 additions & 16 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Would you like to build/train a model using Keras/Python? And would you like to
4343
* `AveragePooling1D/2D/3D`, `GlobalAveragePooling1D/2D/3D`
4444
* `TimeDistributed`
4545
* `Conv1D/2D/3D`, `SeparableConv2D`, `DepthwiseConv2D`
46-
* `Conv1DTranspose`, `Conv2DTranspose`
46+
* `Conv1DTranspose`, `Conv2DTranspose`, `Conv3DTranspose`
4747
* `Cropping1D/2D/3D`, `ZeroPadding1D/2D/3D`, `CenterCrop`
4848
* `BatchNormalization`, `Dense`, `Flatten`, `Normalization`
4949
* `Dropout`, `AlphaDropout`, `GaussianDropout`, `GaussianNoise`
@@ -52,7 +52,7 @@ Would you like to build/train a model using Keras/Python? And would you like to
5252
* `RandomContrast`, `RandomFlip`, `RandomHeight`
5353
* `RandomRotation`, `RandomTranslation`, `RandomWidth`, `RandomZoom`
5454
* `MaxPooling1D/2D/3D`, `GlobalMaxPooling1D/2D/3D`
55-
* `UpSampling1D/2D`, `Resizing`, `Rescaling`
55+
* `UpSampling1D/2D/3D`, `Resizing`, `Rescaling`
5656
* `Reshape`, `Permute`, `RepeatVector`
5757
* `Embedding`, `CategoryEncoding`
5858
* `Attention`, `AdditiveAttention`, `MultiHeadAttention`
@@ -79,7 +79,7 @@ Would you like to build/train a model using Keras/Python? And would you like to
7979
`RepeatVector`, `RNN`, `SimpleRNN`,
8080
`SimpleRNNCell`, `StackedRNNCells`, `StringLookup`, `TextVectorization`,
8181
`Bidirectional`, `GRU`, `LSTM`, `CuDNNGRU`, `CuDNNLSTM`,
82-
`ThresholdedReLU`, `Upsampling3D`, `temporal` models
82+
`ThresholdedReLU`, `temporal` models
8383

8484
Usage
8585
-----

include/fdeep/convolution3d.hpp

Lines changed: 159 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ namespace internal {
3838
padding pad_type,
3939
std::size_t input_shape_size_d4,
4040
std::size_t input_shape_height,
41-
std::size_t input_shape_width)
41+
std::size_t input_shape_width,
42+
bool transposed = false)
4243
{
4344
const int filter_size_d4 = static_cast<int>(filter_shape.size_dim_4_);
4445
const int filter_height = static_cast<int>(filter_shape.height_);
@@ -59,9 +60,15 @@ namespace internal {
5960
out_height = fplus::ceil(static_cast<float>(in_height) / static_cast<float>(strides_y) - 0.001);
6061
out_width = fplus::ceil(static_cast<float>(in_width) / static_cast<float>(strides_x) - 0.001);
6162
} else {
62-
out_size_d4 = fplus::ceil(static_cast<float>(in_size_d4 - filter_size_d4 + 1) / static_cast<float>(strides_d4) - 0.001);
63-
out_height = fplus::ceil(static_cast<float>(in_height - filter_height + 1) / static_cast<float>(strides_y) - 0.001);
64-
out_width = fplus::ceil(static_cast<float>(in_width - filter_width + 1) / static_cast<float>(strides_x) - 0.001);
63+
if (transposed) {
64+
out_size_d4 = fplus::ceil(static_cast<float>(in_size_d4 + filter_size_d4 - 1) / static_cast<float>(strides_d4) - 0.001);
65+
out_height = fplus::ceil(static_cast<float>(in_height + filter_height - 1) / static_cast<float>(strides_y) - 0.001);
66+
out_width = fplus::ceil(static_cast<float>(in_width + filter_width - 1) / static_cast<float>(strides_x) - 0.001);
67+
} else {
68+
out_size_d4 = fplus::ceil(static_cast<float>(in_size_d4 - filter_size_d4 + 1) / static_cast<float>(strides_d4) - 0.001);
69+
out_height = fplus::ceil(static_cast<float>(in_height - filter_height + 1) / static_cast<float>(strides_y) - 0.001);
70+
out_width = fplus::ceil(static_cast<float>(in_width - filter_width + 1) / static_cast<float>(strides_x) - 0.001);
71+
}
6572
}
6673

6774
int pad_front = 0;
@@ -71,6 +78,15 @@ namespace internal {
7178
int pad_left = 0;
7279
int pad_right = 0;
7380

81+
if (transposed) {
82+
pad_front = filter_size_d4 - 1;
83+
pad_back = filter_size_d4 - 1;
84+
pad_top = filter_height - 1;
85+
pad_bottom = filter_height - 1;
86+
pad_left = filter_width - 1;
87+
pad_right = filter_width - 1;
88+
}
89+
7490
if (pad_type == padding::same) {
7591
int pad_along_d4 = 0;
7692
int pad_along_height = 0;
@@ -125,28 +141,35 @@ namespace internal {
125141
tensor filter_mats_;
126142
};
127143

128-
inline tensor dilate_tensor_3d(const shape3& dilation_rate, const tensor& in)
144+
inline tensor dilate_tensor_3d(const shape3& dilation_rate, const tensor& in,
145+
bool trailing_zeros = false)
129146
{
130147
if (dilation_rate == shape3(1, 1, 1)) {
131148
return in;
132149
}
133150
assertion(in.shape().rank() == 4, "Invalid rank for 3d dilation");
134151

135152
const auto in_shape = in.shape();
153+
const std::size_t expansion_d4 = trailing_zeros ? (dilation_rate.size_dim_4_ - 1) : 0;
154+
const std::size_t expansion_y = trailing_zeros ? (dilation_rate.height_ - 1) : 0;
155+
const std::size_t expansion_x = trailing_zeros ? (dilation_rate.width_ - 1) : 0;
136156
const tensor_shape dilated_shape(
137-
(in_shape.size_dim_4_ - 1) * dilation_rate.size_dim_4_ + 1,
138-
(in_shape.height_ - 1) * dilation_rate.height_ + 1,
139-
(in_shape.width_ - 1) * dilation_rate.width_ + 1,
157+
(in_shape.size_dim_4_ - 1) * dilation_rate.size_dim_4_ + 1 + expansion_d4,
158+
(in_shape.height_ - 1) * dilation_rate.height_ + 1 + expansion_y,
159+
(in_shape.width_ - 1) * dilation_rate.width_ + 1 + expansion_x,
140160
in_shape.depth_);
161+
const std::size_t offset_d4 = expansion_d4 - expansion_d4 / 2;
162+
const std::size_t offset_y = expansion_y - expansion_y / 2;
163+
const std::size_t offset_x = expansion_x - expansion_x / 2;
141164
tensor result(dilated_shape, static_cast<float_type>(0));
142165
for (std::size_t d4 = 0; d4 < in_shape.size_dim_4_; ++d4) {
143166
for (std::size_t y = 0; y < in_shape.height_; ++y) {
144167
for (std::size_t x = 0; x < in_shape.width_; ++x) {
145168
for (std::size_t z = 0; z < in_shape.depth_; ++z) {
146169
result.set_ignore_rank(tensor_pos(
147-
d4 * dilation_rate.size_dim_4_,
148-
y * dilation_rate.height_,
149-
x * dilation_rate.width_,
170+
d4 * dilation_rate.size_dim_4_ + offset_d4,
171+
y * dilation_rate.height_ + offset_y,
172+
x * dilation_rate.width_ + offset_x,
150173
z),
151174
in.get_ignore_rank(tensor_pos(d4, y, x, z)));
152175
}
@@ -156,6 +179,16 @@ namespace internal {
156179
return result;
157180
}
158181

182+
inline tensor reverse_size_dim_4_dimension(const tensor& in)
183+
{
184+
tensor out = tensor(in.shape(), static_cast<float_type>(0));
185+
loop_over_all_dims(in.shape(), [&in, &out](std::size_t dim5, std::size_t dim4, std::size_t y, std::size_t x, std::size_t z) {
186+
out.set_ignore_rank(tensor_pos(dim5, in.shape().size_dim_4_ - dim4 - 1, y, x, z),
187+
in.get_ignore_rank(tensor_pos(dim5, dim4, y, x, z)));
188+
});
189+
return out;
190+
}
191+
159192
inline filter dilate_filter_3d(const shape3& dilation_rate, const filter& undilated)
160193
{
161194
return filter(dilate_tensor_3d(dilation_rate, undilated.get_tensor()),
@@ -165,7 +198,8 @@ namespace internal {
165198
inline filter_vec generate_filters_3d(
166199
const shape3& dilation_rate,
167200
const tensor_shape& filter_shape, std::size_t k,
168-
const float_vec& weights, const float_vec& bias)
201+
const float_vec& weights, const float_vec& bias,
202+
bool transpose = false)
169203
{
170204
filter_vec filters(k, filter(tensor(filter_shape, 0), 0));
171205

@@ -186,6 +220,11 @@ namespace internal {
186220
for (auto& filt : filters) {
187221
filt.set_params(*it_filter_val, *it_filter_bias);
188222
filt = dilate_filter_3d(dilation_rate, filt);
223+
if (transpose) {
224+
filt = filter(reverse_size_dim_4_dimension(filt.get_tensor()), filt.get_bias());
225+
filt = filter(reverse_height_dimension(filt.get_tensor()), filt.get_bias());
226+
filt = filter(reverse_width_dimension(filt.get_tensor()), filt.get_bias());
227+
}
189228
++it_filter_val;
190229
++it_filter_bias;
191230
}
@@ -276,6 +315,73 @@ namespace internal {
276315
Eigen::OuterStride<>(static_cast<EigenIndex>(f_depth * strides_x)));
277316
}
278317

318+
// Special version for convolution with strides 1×1×1.
319+
// Uses fewer but larger GEMMs by collapsing the spatial output loops into one big input mapping.
320+
inline tensor convolve_accumulative_s1x1x1_3d(
321+
std::size_t out_size_d4,
322+
std::size_t out_height,
323+
std::size_t out_width,
324+
const convolution3d_filter_matrices& filter_mat,
325+
const tensor& in)
326+
{
327+
const tensor& filter_mats = filter_mat.filter_mats_;
328+
const auto f_size_d4 = filter_mat.filter_shape_.size_dim_4_;
329+
const auto f_height = filter_mat.filter_shape_.height_;
330+
const auto f_width = filter_mat.filter_shape_.width_;
331+
const auto f_depth = filter_mat.filter_shape_.depth_;
332+
const auto out_depth = filter_mat.filter_count_;
333+
334+
assertion(f_depth == in.shape().depth_, "filter depth does not match input");
335+
assertion(out_height == (in.shape().height_ - f_height) + 1, "output height does not match");
336+
assertion(out_width == (in.shape().width_ - f_width) + 1, "output width does not match");
337+
assertion(out_size_d4 == (in.shape().size_dim_4_ - f_size_d4) + 1, "output d4 size does not match");
338+
assertion(out_depth == filter_mat.biases_.size(), "invalid bias count");
339+
340+
tensor output = init_conv_output_tensor_3d(out_size_d4, out_height, out_width, out_depth, in.shape().rank(), filter_mat);
341+
342+
const std::size_t out_height_temp = out_height + f_height - 1;
343+
const std::size_t out_width_temp = out_width + f_width - 1;
344+
tensor output_temp(tensor_shape_with_changed_rank(
345+
tensor_shape(out_size_d4, out_height_temp, out_width_temp, out_depth),
346+
in.shape().rank()),
347+
static_cast<float_type>(0));
348+
349+
const auto mapping_width = (out_size_d4 - 1) * out_height_temp * out_width_temp
350+
+ (out_height - 1) * out_width_temp
351+
+ out_width;
352+
353+
for (std::size_t d4_filt = 0; d4_filt < f_size_d4; ++d4_filt) {
354+
for (std::size_t y_filt = 0; y_filt < f_height; ++y_filt) {
355+
const Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned>
356+
filter(const_cast<float_type*>(&filter_mats.get_ref_ignore_rank(tensor_pos(d4_filt, y_filt, 0, 0, 0))),
357+
static_cast<EigenIndex>(out_depth),
358+
static_cast<EigenIndex>(f_width * f_depth));
359+
360+
const auto input = get_im2col_mapping_3d(in, f_width, f_depth, 1, mapping_width, 0, 0, d4_filt, y_filt);
361+
362+
Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned>
363+
output_temp_map(&output_temp.get_ref_ignore_rank(tensor_pos(0, 0, 0, 0, 0)),
364+
static_cast<EigenIndex>(out_depth),
365+
static_cast<EigenIndex>(mapping_width));
366+
367+
output_temp_map.noalias() += filter * input;
368+
}
369+
}
370+
371+
for (std::size_t d4_out = 0; d4_out < out_size_d4; ++d4_out) {
372+
for (std::size_t y_out = 0; y_out < out_height; ++y_out) {
373+
for (std::size_t x_out = 0; x_out < out_width; ++x_out) {
374+
for (std::size_t z_out = 0; z_out < out_depth; ++z_out) {
375+
output.get_ref_ignore_rank(tensor_pos(0, d4_out, y_out, x_out, z_out))
376+
+= output_temp.get_ref_ignore_rank(tensor_pos(0, d4_out, y_out, x_out, z_out));
377+
}
378+
}
379+
}
380+
}
381+
382+
return output;
383+
}
384+
279385
inline tensor convolve_accumulative_3d(
280386
std::size_t out_size_d4,
281387
std::size_t out_height,
@@ -299,6 +405,10 @@ namespace internal {
299405
assertion(out_width == (in.shape().width_ - f_width) / strides_x + 1, "output width does not match");
300406
assertion(out_depth == filter_mat.biases_.size(), "invalid bias count");
301407

408+
if (strides_d4 == 1 && strides_y == 1 && strides_x == 1) {
409+
return convolve_accumulative_s1x1x1_3d(out_size_d4, out_height, out_width, filter_mat, in);
410+
}
411+
302412
tensor output = init_conv_output_tensor_3d(out_size_d4, out_height, out_width, out_depth, in.shape().rank(), filter_mat);
303413

304414
for (std::size_t d4_filt = 0; d4_filt < f_size_d4; ++d4_filt) {
@@ -358,5 +468,42 @@ namespace internal {
358468
in_padded);
359469
}
360470

471+
inline tensor convolve_transposed_3d(
472+
const shape3& strides,
473+
const padding& pad_type,
474+
const convolution3d_filter_matrices& filter_mat,
475+
const tensor& input)
476+
{
477+
assertion(filter_mat.filter_shape_.depth_ == input.shape().depth_,
478+
"invalid filter depth");
479+
480+
const auto input_dilated = dilate_tensor_3d(strides, input, pad_type == padding::same);
481+
482+
const shape3 filter_spatial_shape(
483+
filter_mat.filter_shape_.size_dim_4_,
484+
filter_mat.filter_shape_.height_,
485+
filter_mat.filter_shape_.width_);
486+
487+
const auto conv_cfg = preprocess_convolution_3d(
488+
filter_spatial_shape,
489+
shape3(1, 1, 1), pad_type,
490+
input_dilated.shape().size_dim_4_,
491+
input_dilated.shape().height_,
492+
input_dilated.shape().width_,
493+
true);
494+
495+
const auto in_padded = pad_tensor(0,
496+
conv_cfg.pad_front_, conv_cfg.pad_back_,
497+
conv_cfg.pad_top_, conv_cfg.pad_bottom_,
498+
conv_cfg.pad_left_, conv_cfg.pad_right_,
499+
input_dilated);
500+
501+
return convolve_accumulative_3d(
502+
conv_cfg.out_size_d4_, conv_cfg.out_height_, conv_cfg.out_width_,
503+
1, 1, 1,
504+
filter_mat,
505+
in_padded);
506+
}
507+
361508
}
362509
}

include/fdeep/import_model.hpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
#include "fdeep/layers/conv_2d_layer.hpp"
4141
#include "fdeep/layers/conv_2d_transpose_layer.hpp"
4242
#include "fdeep/layers/conv_3d_layer.hpp"
43+
#include "fdeep/layers/conv_3d_transpose_layer.hpp"
4344
#include "fdeep/layers/cropping_3d_layer.hpp"
4445
#include "fdeep/layers/dense_layer.hpp"
4546
#include "fdeep/layers/depthwise_conv_2d_layer.hpp"
@@ -94,6 +95,7 @@
9495
#include "fdeep/layers/unit_normalization_layer.hpp"
9596
#include "fdeep/layers/upsampling_1d_layer.hpp"
9697
#include "fdeep/layers/upsampling_2d_layer.hpp"
98+
#include "fdeep/layers/upsampling_3d_layer.hpp"
9799
#include "fdeep/layers/zero_padding_3d_layer.hpp"
98100
#include "fdeep/tensor.hpp"
99101
#include "fdeep/tensor_shape.hpp"
@@ -447,6 +449,36 @@ namespace internal {
447449
dilation_rate, weights, bias);
448450
}
449451

452+
inline layer_ptr create_conv_3d_transpose_layer(const get_param_f& get_param,
453+
const nlohmann::json& data,
454+
const std::string& name)
455+
{
456+
const std::string padding_str = data["config"]["padding"];
457+
const auto pad_type = create_padding(padding_str);
458+
459+
const shape3 strides = create_shape3(data["config"]["strides"]);
460+
const shape3 dilation_rate = create_shape3(data["config"]["dilation_rate"]);
461+
462+
const auto filter_count = create_size_t(data["config"]["filters"]);
463+
float_vec bias(filter_count, 0);
464+
const bool use_bias = data["config"]["use_bias"];
465+
if (use_bias)
466+
bias = decode_floats(get_param(name, "bias"));
467+
assertion(bias.size() == filter_count, "size of bias does not match");
468+
469+
const float_vec weights = decode_floats(get_param(name, "weights"));
470+
const shape3 kernel_size = create_shape3(data["config"]["kernel_size"]);
471+
assertion(weights.size() % kernel_size.volume() == 0,
472+
"invalid number of weights");
473+
const std::size_t filter_depths = weights.size() / (kernel_size.volume() * filter_count);
474+
const tensor_shape filter_shape(
475+
kernel_size.size_dim_4_, kernel_size.height_, kernel_size.width_, filter_depths);
476+
477+
return std::make_shared<conv_3d_transpose_layer>(name,
478+
filter_shape, filter_count, strides, pad_type,
479+
dilation_rate, weights, bias);
480+
}
481+
450482
inline layer_ptr create_conv_2d_transpose_layer(const get_param_f& get_param,
451483
const nlohmann::json& data,
452484
const std::string& name)
@@ -660,6 +692,15 @@ namespace internal {
660692
name, scale_factor, interpolation);
661693
}
662694

695+
inline layer_ptr create_upsampling_3d_layer(
696+
const get_param_f&, const nlohmann::json& data,
697+
const std::string& name)
698+
{
699+
const auto scale_factor = create_shape3(data["config"]["size"]);
700+
return std::make_shared<upsampling_3d_layer>(
701+
name, scale_factor);
702+
}
703+
663704
inline layer_ptr create_dense_layer(const get_param_f& get_param,
664705
const nlohmann::json& data, const std::string& name)
665706
{
@@ -1301,6 +1342,7 @@ namespace internal {
13011342
{ "Conv3D", create_conv_3d_layer },
13021343
{ "Conv1DTranspose", create_conv_2d_transpose_layer },
13031344
{ "Conv2DTranspose", create_conv_2d_transpose_layer },
1345+
{ "Conv3DTranspose", create_conv_3d_transpose_layer },
13041346
{ "SeparableConv1D", create_separable_conv_2D_layer },
13051347
{ "SeparableConv2D", create_separable_conv_2D_layer },
13061348
{ "DepthwiseConv2D", create_depthwise_conv_2D_layer },
@@ -1365,6 +1407,7 @@ namespace internal {
13651407
{ "GlobalAveragePooling3D", create_global_average_pooling_3d_layer },
13661408
{ "UpSampling1D", create_upsampling_1d_layer },
13671409
{ "UpSampling2D", create_upsampling_2d_layer },
1410+
{ "UpSampling3D", create_upsampling_3d_layer },
13681411
{ "Dense", create_dense_layer },
13691412
{ "Add", create_add_layer },
13701413
{ "Maximum", create_maximum_layer },

0 commit comments

Comments
 (0)