11#include " engine/framework/core/backend.h"
22#include " engine/framework/modules/conv_modules.h"
3+ #include " engine/framework/modules/structural_modules.h"
34
45#include < ggml-backend.h>
56#include < ggml.h>
67
8+ #include < algorithm>
9+ #include < chrono>
710#include < cmath>
811#include < iostream>
912#include < optional>
@@ -31,6 +34,12 @@ struct ConvTransposeCase {
3134struct RunResult {
3235 engine::core::TensorShape shape;
3336 std::vector<float > values;
37+ double compute_ms = 0.0 ;
38+ };
39+
40+ struct DiffStats {
41+ float max_diff = 0 .0f ;
42+ double mean_diff = 0.0 ;
3443};
3544
3645std::vector<float > make_patterned_f32 (size_t count, float phase, float scale) {
@@ -95,13 +104,16 @@ struct BackendModuleRunner {
95104 allocate_tensors ();
96105 ggml_cgraph * graph = ggml_new_graph_custom (ggml, kTestGraphNodes , false );
97106 ggml_build_forward_expand (graph, output.tensor );
107+ const auto start = std::chrono::steady_clock::now ();
98108 const ggml_status status = ggml_backend_graph_compute (backend, graph);
109+ const auto end = std::chrono::steady_clock::now ();
99110 if (status != GGML_STATUS_SUCCESS ) {
100111 std::ostringstream oss;
101112 oss << " backend graph compute failed with status " << static_cast <int >(status);
102113 throw std::runtime_error (oss.str ());
103114 }
104115 RunResult result{output.shape , {}};
116+ result.compute_ms = std::chrono::duration<double , std::milli>(end - start).count ();
105117 engine::core::read_tensor_f32_into (output.tensor , result.values );
106118 return result;
107119 }
@@ -138,6 +150,111 @@ engine::modules::ConvTranspose1dConfig make_config(const ConvTransposeCase & tes
138150 };
139151}
140152
153+ int64_t conv_transpose1d_output_frames (
154+ const engine::modules::ConvTranspose1dConfig & config,
155+ int64_t input_frames) {
156+ return (input_frames - 1 ) * config.stride - 2 * config.padding +
157+ config.dilation * (config.kernel_size - 1 ) + 1 ;
158+ }
159+
160+ engine::core::TensorValue view_batch_matrix (
161+ engine::core::ModuleBuildContext & ctx,
162+ const engine::core::TensorValue & input,
163+ int64_t batch_index,
164+ int64_t channels,
165+ int64_t frames) {
166+ auto * view = ggml_view_2d (
167+ ctx.ggml ,
168+ input.tensor ,
169+ frames,
170+ channels,
171+ input.tensor ->nb [1 ],
172+ static_cast <size_t >(batch_index) * input.tensor ->nb [2 ]);
173+ return engine::core::wrap_tensor (view, engine::core::TensorShape::from_dims ({channels, frames}), input.type );
174+ }
175+
176+ engine::core::TensorValue add_bias_if_needed (
177+ engine::core::ModuleBuildContext & ctx,
178+ const engine::core::TensorValue & output,
179+ int64_t out_channels,
180+ const std::optional<engine::core::TensorValue> & bias) {
181+ if (!bias.has_value ()) {
182+ return output;
183+ }
184+ auto * bias_view = ggml_reshape_3d (ctx.ggml , bias->tensor , 1 , out_channels, 1 );
185+ auto * bias_expanded = ggml_repeat (ctx.ggml , bias_view, output.tensor );
186+ return engine::core::wrap_tensor (ggml_add (ctx.ggml , output.tensor , bias_expanded), output.shape , GGML_TYPE_F32 );
187+ }
188+
189+ engine::core::TensorValue build_conv_transpose1d_slow_test_path (
190+ engine::core::ModuleBuildContext & ctx,
191+ const engine::modules::ConvTranspose1dConfig & config,
192+ const engine::core::TensorValue & input,
193+ const engine::modules::ConvTranspose1dWeights & weights,
194+ const engine::core::TensorShape & output_shape) {
195+ engine::core::TensorValue output;
196+ for (int64_t batch_index = 0 ; batch_index < input.shape .dims [0 ]; ++batch_index) {
197+ const auto matrix_input = view_batch_matrix (ctx, input, batch_index, config.in_channels , input.shape .dims [2 ]);
198+ auto batch_output = engine::core::wrap_tensor (
199+ ggml_conv_transpose_1d (
200+ ctx.ggml ,
201+ weights.weight .tensor ,
202+ matrix_input.tensor ,
203+ config.stride ,
204+ config.padding ,
205+ config.dilation ),
206+ engine::core::TensorShape::from_dims ({1 , config.out_channels , output_shape.dims [2 ]}),
207+ GGML_TYPE_F32 );
208+ output = output.valid () ? engine::modules::ConcatModule ({0 }).build (ctx, output, batch_output) : batch_output;
209+ }
210+ return add_bias_if_needed (ctx, output, config.out_channels , weights.bias );
211+ }
212+
213+ engine::core::TensorValue build_conv_transpose1d_col2im_test_path (
214+ engine::core::ModuleBuildContext & ctx,
215+ const engine::modules::ConvTranspose1dConfig & config,
216+ const engine::core::TensorValue & input,
217+ const engine::modules::ConvTranspose1dWeights & weights,
218+ const engine::core::TensorShape & output_shape) {
219+ auto * weight_perm = ggml_reshape_2d (
220+ ctx.ggml ,
221+ ggml_cont (ctx.ggml , ggml_permute (ctx.ggml , weights.weight .tensor , 1 , 2 , 0 , 3 )),
222+ config.in_channels ,
223+ config.kernel_size * config.out_channels );
224+ ggml_tensor * bias_matrix = nullptr ;
225+ if (config.use_bias ) {
226+ if (!weights.bias .has_value ()) {
227+ throw std::runtime_error (" test col2im path requires bias when use_bias is true" );
228+ }
229+ bias_matrix = ggml_reshape_2d (ctx.ggml , weights.bias ->tensor , 1 , config.out_channels );
230+ }
231+ engine::core::TensorValue output;
232+ for (int64_t batch_index = 0 ; batch_index < input.shape .dims [0 ]; ++batch_index) {
233+ auto * batch_input = ggml_view_2d (
234+ ctx.ggml ,
235+ input.tensor ,
236+ input.tensor ->ne [0 ],
237+ input.tensor ->ne [1 ],
238+ input.tensor ->nb [1 ],
239+ static_cast <size_t >(batch_index) * input.tensor ->nb [2 ]);
240+ auto * transposed_input = ggml_cont (ctx.ggml , ggml_transpose (ctx.ggml , batch_input));
241+ auto * columns = ggml_mul_mat (ctx.ggml , weight_perm, transposed_input);
242+ auto * batch_output = ggml_col2im_1d (ctx.ggml , columns, config.stride , static_cast <int >(config.out_channels ), config.padding );
243+ if (bias_matrix != nullptr ) {
244+ batch_output = ggml_add (ctx.ggml , batch_output, bias_matrix);
245+ }
246+ auto batch_value = engine::core::wrap_tensor (
247+ ggml_reshape_3d (ctx.ggml , batch_output, batch_output->ne [0 ], batch_output->ne [1 ], 1 ),
248+ engine::core::TensorShape::from_dims ({1 , config.out_channels , batch_output->ne [0 ]}),
249+ GGML_TYPE_F32 );
250+ if (batch_value.shape .dims [2 ] != output_shape.dims [2 ]) {
251+ throw std::runtime_error (" test col2im path produced unexpected frame count" );
252+ }
253+ output = output.valid () ? engine::modules::ConcatModule ({0 }).build (ctx, output, batch_value) : batch_value;
254+ }
255+ return output;
256+ }
257+
141258RunResult run_conv_transpose_case (
142259 const ConvTransposeCase & test_case,
143260 engine::core::BackendType backend_type) {
@@ -171,6 +288,55 @@ RunResult run_conv_transpose_case(
171288 return runner.run_f32 (output);
172289}
173290
291+ RunResult run_conv_transpose_ab_case (
292+ const ConvTransposeCase & test_case,
293+ engine::core::BackendType backend_type,
294+ bool use_col2im,
295+ float input_phase) {
296+ BackendModuleRunner runner (
297+ use_col2im ? " conv_transpose_fast_path_test.col2im_ab" : " conv_transpose_fast_path_test.slow_ab" ,
298+ backend_type);
299+
300+ const auto input_shape = engine::core::TensorShape::from_dims (
301+ {test_case.batch , test_case.in_channels , test_case.frames });
302+ const auto weight_shape = engine::core::TensorShape::from_dims (
303+ {test_case.in_channels , test_case.out_channels , test_case.kernel_size });
304+ const auto bias_shape = engine::core::TensorShape::from_dims ({test_case.out_channels });
305+ const auto config = make_config (test_case);
306+ const auto output_shape = engine::core::TensorShape::from_dims (
307+ {test_case.batch , test_case.out_channels , conv_transpose1d_output_frames (config, test_case.frames )});
308+
309+ auto input = runner.make_f32 (input_shape);
310+ auto weight = runner.make_f32 (weight_shape);
311+ std::optional<engine::core::TensorValue> bias;
312+ if (test_case.use_bias ) {
313+ bias = runner.make_f32 (bias_shape);
314+ }
315+
316+ const auto output = use_col2im
317+ ? build_conv_transpose1d_col2im_test_path (
318+ runner.ctx ,
319+ config,
320+ input,
321+ engine::modules::ConvTranspose1dWeights{weight, bias},
322+ output_shape)
323+ : build_conv_transpose1d_slow_test_path (
324+ runner.ctx ,
325+ config,
326+ input,
327+ engine::modules::ConvTranspose1dWeights{weight, bias},
328+ output_shape);
329+
330+ runner.allocate_tensors ();
331+ write_tensor_f32 (input, make_patterned_f32 (static_cast <size_t >(input_shape.num_elements ()), input_phase, 0 .031f ), " input" );
332+ write_tensor_f32 (weight, make_patterned_f32 (static_cast <size_t >(weight_shape.num_elements ()), 0 .47f , 0 .017f ), " weight" );
333+ if (bias) {
334+ write_tensor_f32 (*bias, make_patterned_f32 (static_cast <size_t >(bias_shape.num_elements ()), 0 .83f , 0 .011f ), " bias" );
335+ }
336+
337+ return runner.run_f32 (output);
338+ }
339+
174340void require_fast_path_trigger_conditions (engine::core::BackendType backend_type) {
175341 BackendModuleRunner cuda_runner (" conv_transpose_fast_path_test.cuda_trigger" , backend_type);
176342 BackendModuleRunner cpu_runner (" conv_transpose_fast_path_test.cpu_trigger" , engine::core::BackendType::Cpu);
@@ -204,34 +370,38 @@ void require_same_shape(const engine::core::TensorShape & lhs, const engine::cor
204370 }
205371}
206372
207- void require_close (const RunResult & slow, const RunResult & fast, const ConvTransposeCase & test_case) {
373+ DiffStats compare_results (const RunResult & slow, const RunResult & fast, const ConvTransposeCase & test_case) {
208374 require_same_shape (slow.shape , fast.shape , test_case.name );
209375 if (slow.values .size () != fast.values .size ()) {
210376 throw std::runtime_error (std::string (test_case.name ) + " value count mismatch" );
211377 }
212378
213- float max_diff = 0 . 0f ;
379+ DiffStats stats{} ;
214380 size_t max_index = 0 ;
215- double mean_diff = 0.0 ;
216381 for (size_t i = 0 ; i < slow.values .size (); ++i) {
217382 const float diff = std::fabs (slow.values [i] - fast.values [i]);
218- mean_diff += diff;
219- if (diff > max_diff) {
220- max_diff = diff;
383+ stats. mean_diff += diff;
384+ if (diff > stats. max_diff ) {
385+ stats. max_diff = diff;
221386 max_index = i;
222387 }
223388 }
224- mean_diff /= static_cast <double >(slow.values .size ());
389+ stats. mean_diff /= static_cast <double >(slow.values .size ());
225390
226391 constexpr float kMaxAllowed = 2 .0e-5f ;
227392 constexpr double kMeanAllowed = 2.0e-6 ;
228- if (max_diff > kMaxAllowed || mean_diff > kMeanAllowed ) {
393+ if (stats. max_diff > kMaxAllowed || stats. mean_diff > kMeanAllowed ) {
229394 std::ostringstream oss;
230- oss << test_case.name << " slow/fast drift exceeds bounds: max diff=" << max_diff
395+ oss << test_case.name << " slow/fast drift exceeds bounds: max diff=" << stats. max_diff
231396 << " at " << max_index << " (slow=" << slow.values [max_index]
232- << " , fast=" << fast.values [max_index] << " ), mean diff=" << mean_diff;
397+ << " , fast=" << fast.values [max_index] << " ), mean diff=" << stats. mean_diff ;
233398 throw std::runtime_error (oss.str ());
234399 }
400+ return stats;
401+ }
402+
403+ void require_close (const RunResult & slow, const RunResult & fast, const ConvTransposeCase & test_case) {
404+ (void ) compare_results (slow, fast, test_case);
235405}
236406
237407void run_case (const ConvTransposeCase & test_case, engine::core::BackendType backend_type) {
@@ -241,18 +411,74 @@ void run_case(const ConvTransposeCase & test_case, engine::core::BackendType bac
241411 std::cout << " [PASS] " << test_case.name << " output " << reference.shape .to_string () << ' \n ' ;
242412}
243413
414+ const char * backend_name (engine::core::BackendType backend_type) {
415+ switch (backend_type) {
416+ case engine::core::BackendType::Cpu:
417+ return " cpu" ;
418+ case engine::core::BackendType::Cuda:
419+ return " cuda" ;
420+ case engine::core::BackendType::Vulkan:
421+ return " vulkan" ;
422+ default :
423+ return " unknown" ;
424+ }
425+ }
426+
427+ void run_ab_case (const ConvTransposeCase & test_case, engine::core::BackendType backend_type) {
428+ constexpr int kRounds = 6 ;
429+ double slow_total_ms = 0.0 ;
430+ double col2im_total_ms = 0.0 ;
431+ DiffStats worst{};
432+ engine::core::TensorShape output_shape;
433+ for (int round = 0 ; round < kRounds ; ++round) {
434+ const float phase = 0 .19f + 0 .07f * static_cast <float >(round);
435+ const auto slow = run_conv_transpose_ab_case (test_case, backend_type, false , phase);
436+ const auto col2im = run_conv_transpose_ab_case (test_case, backend_type, true , phase);
437+ const auto stats = compare_results (slow, col2im, test_case);
438+ slow_total_ms += slow.compute_ms ;
439+ col2im_total_ms += col2im.compute_ms ;
440+ worst.max_diff = std::max (worst.max_diff , stats.max_diff );
441+ worst.mean_diff = std::max (worst.mean_diff , stats.mean_diff );
442+ output_shape = slow.shape ;
443+ }
444+ const double slow_avg_ms = slow_total_ms / static_cast <double >(kRounds );
445+ const double col2im_avg_ms = col2im_total_ms / static_cast <double >(kRounds );
446+ std::cout << " [AB] " << backend_name (backend_type) << ' ' << test_case.name
447+ << " output " << output_shape.to_string ()
448+ << " rounds=" << kRounds
449+ << " slow_avg_ms=" << slow_avg_ms
450+ << " col2im_avg_ms=" << col2im_avg_ms
451+ << " speedup=" << (slow_avg_ms / col2im_avg_ms)
452+ << " max_diff=" << worst.max_diff
453+ << " mean_diff=" << worst.mean_diff << ' \n ' ;
454+ }
455+
244456} // namespace
245457
246458int main () {
247459 try {
460+ const ConvTransposeCase qwen3_case{" qwen3_decoder_mid_block_biased" , 1 , 256 , 128 , 96 , 10 , 5 , true };
461+ const ConvTransposeCase batched_case{" batched_decoder_block_no_bias" , 2 , 192 , 192 , 48 , 2 , 2 , false };
462+
248463 constexpr auto kBackend = engine::core::BackendType::Cuda;
249464 if (!backend_is_available (kBackend )) {
250465 std::cout << " [SKIP] CUDA backend is not available for conv transpose fast-path parity test\n " ;
251- return 0 ;
466+ } else {
467+ require_fast_path_trigger_conditions (kBackend );
468+ run_case (qwen3_case, kBackend );
469+ run_case (batched_case, kBackend );
470+ run_ab_case (qwen3_case, kBackend );
471+ run_ab_case (batched_case, kBackend );
472+ }
473+
474+ std::cout << " [SKIP] CPU backend does not implement ggml_col2im_1d for conv transpose A/B test\n " ;
475+
476+ if (backend_is_available (engine::core::BackendType::Vulkan)) {
477+ run_ab_case (qwen3_case, engine::core::BackendType::Vulkan);
478+ run_ab_case (batched_case, engine::core::BackendType::Vulkan);
479+ } else {
480+ std::cout << " [SKIP] Vulkan backend is not available for conv transpose A/B test\n " ;
252481 }
253- require_fast_path_trigger_conditions (kBackend );
254- run_case ({" qwen3_decoder_mid_block_biased" , 1 , 256 , 128 , 96 , 10 , 5 , true }, kBackend );
255- run_case ({" batched_decoder_block_no_bias" , 2 , 192 , 192 , 48 , 2 , 2 , false }, kBackend );
256482 } catch (const std::exception & ex) {
257483 std::cerr << " [FAIL] " << ex.what () << ' \n ' ;
258484 return 1 ;
0 commit comments