Skip to content

Commit cd7e404

Browse files
committed
Extend conv transpose fast path unit test
1 parent b61ae68 commit cd7e404

1 file changed

Lines changed: 240 additions & 14 deletions

File tree

tests/unittests/test_conv_transpose_fast_path.cpp

Lines changed: 240 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
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 {
3134
struct 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

3645
std::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+
141258
RunResult 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+
174340
void 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

237407
void 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

246458
int 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

Comments
 (0)