Skip to content

Commit bbb11aa

Browse files
danmcleranclaude
andcommitted
Cover 2D pool backward paths and linearInterpolation
The forward path of AvgPool2D / GlobalAvgPool2D was tested, but the backward path was not — and it shares the same divisor() helper that silently produced wrong results in fixed-point before the previous fix. Adds: - AvgPool2D::backward in double and Q8.8 (validates that uniform upstream deltas distribute as delta/WindowArea per cell) - GlobalAvgPool2D::backward in double and Q8.8 - MaxPool2D::backward in Q8.8 (argmax routing of gradients) - linearInterpolation direct tests: endpoint cases on double and Q8.8, and the x1==x0 divide-by-zero short-circuit at interpolate.hpp:29 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 46005bf commit bbb11aa

2 files changed

Lines changed: 140 additions & 0 deletions

File tree

unit_test/lookuptable/lookuptable_unit_test.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include <limits>
3434

3535
#include "qformat.hpp"
36+
#include "interpolate.hpp"
3637
#include "lookupTable.hpp"
3738
#include "sigmoid.hpp"
3839
#include "tanh.hpp"
@@ -136,6 +137,39 @@ void verifyAccuracy(const Table& table, double (*ref)(double),
136137

137138
BOOST_AUTO_TEST_SUITE(test_suite_lookuptable)
138139

140+
// ----- linearInterpolation (interpolate.hpp) ---------------------------------
141+
// Two branches: x1 == x0 short-circuit, and the linear path.
142+
143+
BOOST_AUTO_TEST_CASE(linear_interpolation_endpoints_double)
144+
{
145+
using tinymind::linearInterpolation;
146+
BOOST_CHECK_CLOSE(linearInterpolation<double>(0.0, 0.0, 1.0, 10.0, 20.0), 10.0, 1e-9);
147+
BOOST_CHECK_CLOSE(linearInterpolation<double>(1.0, 0.0, 1.0, 10.0, 20.0), 20.0, 1e-9);
148+
BOOST_CHECK_CLOSE(linearInterpolation<double>(0.5, 0.0, 1.0, 10.0, 20.0), 15.0, 1e-9);
149+
}
150+
151+
BOOST_AUTO_TEST_CASE(linear_interpolation_degenerate_returns_y0)
152+
{
153+
// x1 == x0 hits the divide-by-zero guard at interpolate.hpp:29 and must
154+
// return y0 unchanged, not divide by zero.
155+
using tinymind::linearInterpolation;
156+
BOOST_CHECK_CLOSE(linearInterpolation<double>(5.0, 1.0, 1.0, 7.0, 99.0), 7.0, 1e-9);
157+
}
158+
159+
BOOST_AUTO_TEST_CASE(linear_interpolation_fixed_point)
160+
{
161+
// Same boundary checks in Q8.8.
162+
using tinymind::linearInterpolation;
163+
using Q = QValue<8, 8, true>;
164+
const Q result = linearInterpolation<Q>(Q(0, 128), // x = 0.5
165+
Q(0, 0), // x0 = 0
166+
Q(1, 0), // x1 = 1
167+
Q(0, 0), // y0 = 0
168+
Q(1, 0)); // y1 = 1
169+
// Expect 0.5 = raw 128.
170+
BOOST_CHECK_EQUAL(result.getValue(), Q(0, 128).getValue());
171+
}
172+
139173
// ----- Branch coverage on LookupTable<Q>::getValue ---------------------------
140174
//
141175
// All five reachable branches in cpp/lookupTable.hpp:35-89 are exercised on a

unit_test/nn/nn_unit_test.cpp

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6804,6 +6804,112 @@ BOOST_AUTO_TEST_CASE(test_case_globalavgpool2d_fixed_point)
68046804
BOOST_TEST(output[1].getValue() == ValueType(3, 0).getValue());
68056805
}
68066806

6807+
BOOST_AUTO_TEST_CASE(test_case_avgpool2d_backward)
6808+
{
6809+
// 4x4 single-channel, 2x2 window stride 2. Each output cell distributes
6810+
// its delta uniformly across the 4 input cells in its window: grad = d/4.
6811+
tinymind::AvgPool2D<double, 4, 4, 1, 2, 2, 2, 2> pool;
6812+
double outputDeltas[4] = {4.0, 8.0, 12.0, 16.0};
6813+
double inputDeltas[16];
6814+
pool.backward(outputDeltas, inputDeltas);
6815+
6816+
// Top-left window (output 0): grad = 1.0 across 4 cells.
6817+
BOOST_TEST(std::fabs(inputDeltas[0] - 1.0) < 1e-9);
6818+
BOOST_TEST(std::fabs(inputDeltas[1] - 1.0) < 1e-9);
6819+
BOOST_TEST(std::fabs(inputDeltas[4] - 1.0) < 1e-9);
6820+
BOOST_TEST(std::fabs(inputDeltas[5] - 1.0) < 1e-9);
6821+
// Top-right window (output 1): grad = 2.0
6822+
BOOST_TEST(std::fabs(inputDeltas[2] - 2.0) < 1e-9);
6823+
BOOST_TEST(std::fabs(inputDeltas[3] - 2.0) < 1e-9);
6824+
BOOST_TEST(std::fabs(inputDeltas[6] - 2.0) < 1e-9);
6825+
BOOST_TEST(std::fabs(inputDeltas[7] - 2.0) < 1e-9);
6826+
// Bot-right window (output 3): grad = 4.0
6827+
BOOST_TEST(std::fabs(inputDeltas[10] - 4.0) < 1e-9);
6828+
BOOST_TEST(std::fabs(inputDeltas[15] - 4.0) < 1e-9);
6829+
}
6830+
6831+
BOOST_AUTO_TEST_CASE(test_case_avgpool2d_backward_fixed_point)
6832+
{
6833+
// Same input pattern as above in Q8.8: confirms divisor() is correct on
6834+
// the backward path, not just forward.
6835+
typedef tinymind::QValue<8, 8, true, tinymind::RoundUpPolicy> ValueType;
6836+
tinymind::AvgPool2D<ValueType, 4, 4, 1, 2, 2, 2, 2> pool;
6837+
ValueType outputDeltas[4] = {ValueType(4, 0), ValueType(8, 0), ValueType(12, 0), ValueType(16, 0)};
6838+
ValueType inputDeltas[16];
6839+
pool.backward(outputDeltas, inputDeltas);
6840+
6841+
BOOST_TEST(inputDeltas[0].getValue() == ValueType(1, 0).getValue());
6842+
BOOST_TEST(inputDeltas[5].getValue() == ValueType(1, 0).getValue());
6843+
BOOST_TEST(inputDeltas[3].getValue() == ValueType(2, 0).getValue());
6844+
BOOST_TEST(inputDeltas[10].getValue() == ValueType(4, 0).getValue());
6845+
BOOST_TEST(inputDeltas[15].getValue() == ValueType(4, 0).getValue());
6846+
}
6847+
6848+
BOOST_AUTO_TEST_CASE(test_case_globalavgpool2d_backward)
6849+
{
6850+
// GAP backward distributes each output delta uniformly across its
6851+
// spatial extent (H*W positions per channel).
6852+
tinymind::GlobalAvgPool2D<double, 3, 3, 2> gap;
6853+
double outputDeltas[2] = {9.0, 18.0}; // chosen so per-cell grad is 1.0 and 2.0
6854+
double inputDeltas[18];
6855+
gap.backward(outputDeltas, inputDeltas);
6856+
6857+
for (size_t i = 0; i < 9; ++i)
6858+
{
6859+
BOOST_TEST(std::fabs(inputDeltas[i * 2 + 0] - 1.0) < 1e-9);
6860+
BOOST_TEST(std::fabs(inputDeltas[i * 2 + 1] - 2.0) < 1e-9);
6861+
}
6862+
}
6863+
6864+
BOOST_AUTO_TEST_CASE(test_case_globalavgpool2d_backward_fixed_point)
6865+
{
6866+
typedef tinymind::QValue<8, 8, true, tinymind::RoundUpPolicy> ValueType;
6867+
tinymind::GlobalAvgPool2D<ValueType, 3, 3, 2> gap;
6868+
ValueType outputDeltas[2] = {ValueType(9, 0), ValueType(18, 0)};
6869+
ValueType inputDeltas[18];
6870+
gap.backward(outputDeltas, inputDeltas);
6871+
6872+
for (size_t i = 0; i < 9; ++i)
6873+
{
6874+
BOOST_TEST(inputDeltas[i * 2 + 0].getValue() == ValueType(1, 0).getValue());
6875+
BOOST_TEST(inputDeltas[i * 2 + 1].getValue() == ValueType(2, 0).getValue());
6876+
}
6877+
}
6878+
6879+
BOOST_AUTO_TEST_CASE(test_case_maxpool2d_backward_fixed_point)
6880+
{
6881+
// Argmax-routed gradients: only the position holding each max receives
6882+
// the upstream delta. Other positions stay zero.
6883+
typedef tinymind::QValue<8, 8, true, tinymind::RoundUpPolicy> ValueType;
6884+
tinymind::MaxPool2D<ValueType, 4, 4, 1, 2, 2, 2, 2> pool;
6885+
ValueType input[16];
6886+
const int values[16] = {
6887+
1, 3, 2, 4,
6888+
5, 7, 6, 8,
6889+
9, 11, 10, 12,
6890+
13, 15, 14, 16
6891+
};
6892+
for (size_t i = 0; i < 16; ++i) input[i] = ValueType(values[i], 0);
6893+
6894+
ValueType output[4];
6895+
pool.forward(input, output); // populates argmax indices
6896+
6897+
ValueType outputDeltas[4] = {ValueType(1, 0), ValueType(1, 0), ValueType(1, 0), ValueType(1, 0)};
6898+
ValueType inputDeltas[16];
6899+
pool.backward(outputDeltas, inputDeltas);
6900+
6901+
typename ValueType::FullWidthValueType sum = 0;
6902+
size_t hits = 0;
6903+
for (size_t i = 0; i < 16; ++i)
6904+
{
6905+
sum += inputDeltas[i].getValue();
6906+
if (inputDeltas[i].getValue() != 0) ++hits;
6907+
}
6908+
// Exactly four argmax positions, each receiving raw=256 (= 1.0).
6909+
BOOST_TEST(hits == 4u);
6910+
BOOST_TEST(sum == 4 * ValueType(1, 0).getValue());
6911+
}
6912+
68076913
// ============================================================
68086914
// Benchmark harness tests
68096915
// ============================================================

0 commit comments

Comments
 (0)