Skip to content

Commit 4b2b538

Browse files
danmcleranclaude
andcommitted
Cover quantized layer re-quantization and fixed-point pool/conv updates
Five small targeted cases that close remaining edge-case gaps: - BinaryDense::updateWeights sign-flip: latent crosses zero -> packed binary weight must flip on the next forward. - TernaryDense::updateWeights sign-flip: same idea for ternary, where re-quantization includes both sign and threshold. - MaxPool1D::backward in Q8.8: pins down argmax routing of fixed-point upstream deltas (only the position holding each max receives the delta). - DepthwiseConv2D::updateWeights in Q16.16: fixed-point weights[i] += lr*grad[i] across kernel and bias. - PointwiseConv2D::updateWeights in Q16.16: same path for the 1x1 cross-channel mix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1182165 commit 4b2b538

1 file changed

Lines changed: 122 additions & 0 deletions

File tree

unit_test/nn/nn_unit_test.cpp

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4352,6 +4352,30 @@ BOOST_AUTO_TEST_CASE(test_case_maxpool1d_backward)
43524352
BOOST_TEST(fabs(inputDeltas[5] - 0.3) < 0.001); // max of window 2
43534353
}
43544354

4355+
BOOST_AUTO_TEST_CASE(test_case_maxpool1d_backward_fixed_point)
4356+
{
4357+
// Same argmax routing in Q8.8: only the position holding each max
4358+
// should receive the upstream delta; others stay zero.
4359+
typedef tinymind::QValue<8, 8, true, tinymind::RoundUpPolicy> ValueType;
4360+
tinymind::MaxPool1D<ValueType, 6, 2, 2, 1> pool;
4361+
4362+
ValueType input[6] = {ValueType(1, 0), ValueType(3, 0), ValueType(2, 0),
4363+
ValueType(5, 0), ValueType(4, 0), ValueType(6, 0)};
4364+
ValueType output[3];
4365+
pool.forward(input, output);
4366+
4367+
ValueType outputDeltas[3] = {ValueType(1, 0), ValueType(2, 0), ValueType(3, 0)};
4368+
ValueType inputDeltas[6];
4369+
pool.backward(outputDeltas, inputDeltas);
4370+
4371+
BOOST_TEST(inputDeltas[0].getValue() == 0);
4372+
BOOST_TEST(inputDeltas[1].getValue() == ValueType(1, 0).getValue());
4373+
BOOST_TEST(inputDeltas[2].getValue() == 0);
4374+
BOOST_TEST(inputDeltas[3].getValue() == ValueType(2, 0).getValue());
4375+
BOOST_TEST(inputDeltas[4].getValue() == 0);
4376+
BOOST_TEST(inputDeltas[5].getValue() == ValueType(3, 0).getValue());
4377+
}
4378+
43554379
BOOST_AUTO_TEST_CASE(test_case_maxpool1d_multichannel)
43564380
{
43574381
// 2 channels, 4 elements each, pool size 2, stride 2
@@ -5289,6 +5313,31 @@ BOOST_AUTO_TEST_CASE(test_case_binary_dense_backward_ste)
52895313
BOOST_TEST(fabs(layer.getLatentWeight(0, 1) - (-0.2)) < 0.01);
52905314
}
52915315

5316+
BOOST_AUTO_TEST_CASE(test_case_binary_dense_updateweights_sign_flip)
5317+
{
5318+
// updateWeights() must call binarizeWeights() so that a latent weight
5319+
// crossing zero immediately flips the packed weight sign on the next
5320+
// forward pass.
5321+
tinymind::BinaryDense<double, 1, 1> layer;
5322+
layer.setLatentWeight(0, 0, 0.05); // small positive
5323+
layer.setBias(0, 0.0);
5324+
layer.binarizeWeights();
5325+
BOOST_TEST(layer.getBinaryWeight(0, 0) == 1.0);
5326+
5327+
// Run backward to populate gradient. With input=+1 and outputDelta=+1,
5328+
// mLatentGradients[0] = +1.0 (within STE clip range).
5329+
double input[1] = {1.0};
5330+
double output[1];
5331+
layer.forward(input, output);
5332+
double outputDeltas[1] = {1.0};
5333+
layer.backward(outputDeltas, input, nullptr);
5334+
5335+
// lr = -0.1 pushes latent to 0.05 - 0.1 = -0.05; re-binarize to -1.
5336+
layer.updateWeights(-0.1);
5337+
BOOST_TEST(fabs(layer.getLatentWeight(0, 0) - (-0.05)) < 1e-9);
5338+
BOOST_TEST(layer.getBinaryWeight(0, 0) == -1.0);
5339+
}
5340+
52925341
BOOST_AUTO_TEST_CASE(test_case_binary_dense_ste_clipping)
52935342
{
52945343
tinymind::BinaryDense<double, 2, 1> layer;
@@ -5517,6 +5566,29 @@ BOOST_AUTO_TEST_CASE(test_case_ternary_dense_backward_ste)
55175566
BOOST_TEST(fabs(layer.getLatentWeight(0, 1) - (-0.9)) < 0.01);
55185567
}
55195568

5569+
BOOST_AUTO_TEST_CASE(test_case_ternary_dense_updateweights_sign_flip)
5570+
{
5571+
// Verify updateWeights re-ternarizes after a latent weight crosses zero.
5572+
// With a single weight the threshold collapses to 0.5*|w|, so any nonzero
5573+
// latent maps to its sign. Initial latent +0.5 -> +1; gradient pushes it
5574+
// negative, packed value should flip to -1 immediately.
5575+
tinymind::TernaryDense<double, 1, 1, 50> layer;
5576+
layer.setLatentWeight(0, 0, 0.5);
5577+
layer.setBias(0, 0.0);
5578+
layer.ternarizeWeights();
5579+
BOOST_TEST(layer.getTernaryWeightValue(0, 0) == 1.0);
5580+
5581+
double input[1] = {2.0};
5582+
double output[1];
5583+
layer.forward(input, output);
5584+
double outputDeltas[1] = {1.0};
5585+
layer.backward(outputDeltas, input, nullptr);
5586+
// Gradient = delta * input = 2.0; lr = -0.4 -> latent becomes 0.5 - 0.8 = -0.3.
5587+
layer.updateWeights(-0.4);
5588+
BOOST_TEST(fabs(layer.getLatentWeight(0, 0) - (-0.3)) < 1e-6);
5589+
BOOST_TEST(layer.getTernaryWeightValue(0, 0) == -1.0);
5590+
}
5591+
55205592
BOOST_AUTO_TEST_CASE(test_case_ternary_dense_input_gradient_propagation)
55215593
{
55225594
tinymind::TernaryDense<double, 3, 1, 10> layer;
@@ -6747,6 +6819,56 @@ BOOST_AUTO_TEST_CASE(test_case_depthwiseconv2d_gradient_fixed_point)
67476819
BOOST_TEST(dw.getGradient(4).getValue() == ValueType(1, 0).getValue()); // bias
67486820
}
67496821

6822+
BOOST_AUTO_TEST_CASE(test_case_depthwiseconv2d_updateweights_fixed_point)
6823+
{
6824+
// After computeGradients, updateWeights must compute weights[i] += lr*grad[i]
6825+
// through Q-format multiply-add. Use Q16.16 for headroom.
6826+
typedef tinymind::QValue<16, 16, true, tinymind::RoundUpPolicy> ValueType;
6827+
tinymind::DepthwiseConv2D<ValueType, 2, 2, 1, 2, 2, 1, 1> dw;
6828+
for (size_t kh = 0; kh < 2; ++kh)
6829+
for (size_t kw = 0; kw < 2; ++kw)
6830+
dw.setChannelWeight(0, kh, kw, ValueType(0));
6831+
dw.setChannelBias(0, ValueType(0));
6832+
6833+
ValueType input[4] = {ValueType(2, 0), ValueType(4, 0), ValueType(6, 0), ValueType(8, 0)};
6834+
ValueType output[1];
6835+
dw.forward(input, output);
6836+
ValueType outputDeltas[1] = {ValueType(1, 0)};
6837+
dw.computeGradients(outputDeltas, input);
6838+
6839+
// lr = 0.5, gradients = (2, 4, 6, 8); expected new weights = (1, 2, 3, 4); bias = 0.5.
6840+
const ValueType lr(0, 1u << 15); // 0.5 in Q16.16
6841+
dw.updateWeights(lr);
6842+
6843+
BOOST_TEST(dw.getChannelWeight(0, 0, 0).getValue() == ValueType(1, 0).getValue());
6844+
BOOST_TEST(dw.getChannelWeight(0, 0, 1).getValue() == ValueType(2, 0).getValue());
6845+
BOOST_TEST(dw.getChannelWeight(0, 1, 0).getValue() == ValueType(3, 0).getValue());
6846+
BOOST_TEST(dw.getChannelWeight(0, 1, 1).getValue() == ValueType(4, 0).getValue());
6847+
BOOST_TEST(dw.getChannelBias(0).getValue() == ValueType(0, 1u << 15).getValue()); // 0.5
6848+
}
6849+
6850+
BOOST_AUTO_TEST_CASE(test_case_pointwiseconv2d_updateweights_fixed_point)
6851+
{
6852+
typedef tinymind::QValue<16, 16, true, tinymind::RoundUpPolicy> ValueType;
6853+
tinymind::PointwiseConv2D<ValueType, 1, 1, 3, 1> pw;
6854+
for (size_t ci = 0; ci < 3; ++ci) pw.setFilterWeight(0, ci, ValueType(0));
6855+
pw.setFilterBias(0, ValueType(0));
6856+
6857+
ValueType input[3] = {ValueType(2, 0), ValueType(4, 0), ValueType(6, 0)};
6858+
ValueType output[1];
6859+
pw.forward(input, output);
6860+
ValueType outputDeltas[1] = {ValueType(1, 0)};
6861+
pw.computeGradients(outputDeltas, input);
6862+
6863+
const ValueType lr(0, 1u << 15); // 0.5
6864+
pw.updateWeights(lr);
6865+
// Filter 0 weight gradients = input = (2, 4, 6); * 0.5 = (1, 2, 3).
6866+
BOOST_TEST(pw.getFilterWeight(0, 0).getValue() == ValueType(1, 0).getValue());
6867+
BOOST_TEST(pw.getFilterWeight(0, 1).getValue() == ValueType(2, 0).getValue());
6868+
BOOST_TEST(pw.getFilterWeight(0, 2).getValue() == ValueType(3, 0).getValue());
6869+
BOOST_TEST(pw.getFilterBias(0).getValue() == ValueType(0, 1u << 15).getValue());
6870+
}
6871+
67506872
BOOST_AUTO_TEST_CASE(test_case_separable_pipeline)
67516873
{
67526874
// Verify a depthwise-then-pointwise pipeline produces the same

0 commit comments

Comments
 (0)