@@ -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+
43554379BOOST_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+
52925341BOOST_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+
55205592BOOST_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+
67506872BOOST_AUTO_TEST_CASE (test_case_separable_pipeline)
67516873{
67526874 // Verify a depthwise-then-pointwise pipeline produces the same
0 commit comments