Altifigence Academy

22 / 24 · Lab

Lab: challenge the corner cases

In the padding fixture, replace every kernel weight by one. What is the first output and why is a center-only test insufficient?

Run two files

Create model.mjs from the first code block and corner-lab.mjs from the second,then run node corner-lab.mjs. Only Node's built-in assert is needed. You may manually create and inspect these files through a supported installed-Digital-Design-Studio workflow. The execution evidence here is the Node command; it does not include DDS simulation,synthesis or remote-project connection.

model.mjs provides one-batch integer direct convolution, valid im2col, nearest-even rounding and unpadded 2×2 max pooling. It uses a common integer stride/dilation on both spatial axes, with four independent pads. It checks groups and rectangular shapes and rejects unsafe integer accumulators. This is not a general model-exchange tool or complete ONNX runtime. Input codes, weight codes and bias must be JavaScript safe integers (absolute value at most 9007199254740991). Every product and intermediate partial sum must also remain in that range; otherwise the calculation is rejected with an error. This integer contract applies to accumulation in conv2d and dot; it does not replace the floating-point scale and rounding rules of quantize and requantize.

Worked boundary fixtures

Input [1,2,3;4,5,6;7,8,9] with an asymmetric 2×2 kernel gives [37,47;67,77] by both paths. With pads top 1, left 0, bottom 0, right 1, the first window is [0,0;1,2], giving 11. With dilation two and no padding, the first window is [1,3;7,9], giving 1+6+21+36=64. Handwrite the changed coordinates.

Two channels containing 2 and 3 with groups two and weights 4 and 5 produce 8 and 15. Reusing that weight shape with groups one must throw: each output now needs two input channels but has only one stored kernel. Also check real-zero code 128 and both int8 clipping boundaries.

Run the original checks,then temporarily reverse kernel indices and observe the first assertion fail. Restore it. Rejecting the last valid row with an incorrect boundary test is another useful mutation. Removing group checks should affect the shape-rejection fixture. Save a passing run after restoration.

These fixtures begin a regression set; they are not exhaustive. Add multichannel 1×1, nonzero-zero-point padding, negative kernels, odd pooling size, empty output and large bias. State which contract each assertion observes. The capstone reuses this reference for the complete small path.

model.mjs

javascript
// Small, dependency-free teaching model. Layout: X[C][H][W], W[M][C/group][R][S].
export function outputSize(size, kernel, stride = 1, padBefore = 0, padAfter = 0, dilation = 1) {
  if (![size, kernel, stride, dilation].every(v => Number.isInteger(v) && v > 0) ||
      ![padBefore, padAfter].every(v => Number.isInteger(v) && v >= 0)) throw new RangeError('Invalid shape');
  return Math.max(0, Math.floor((size + padBefore + padAfter - dilation * (kernel - 1) - 1) / stride) + 1);
}
export function roundEven(value) {
  if (!Number.isFinite(value)) throw new RangeError('Finite number required');
  const floor = Math.floor(value), fraction = value - floor;
  return fraction < 0.5 ? floor : fraction > 0.5 ? floor + 1 : floor % 2 === 0 ? floor : floor + 1;
}
export const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
export function quantize(value, scale, zeroPoint = 0, low = -128, high = 127) {
  if (!(scale > 0) || !Number.isFinite(scale)) throw new RangeError('Positive finite scale required');
  return clamp(roundEven(value / scale) + zeroPoint, low, high);
}
export const dequantize = (value, scale, zeroPoint = 0) => (value - zeroPoint) * scale;
export function requantize(acc, multiplier, zeroPoint = 0, low = -128, high = 127) {
  return clamp(roundEven(acc * multiplier) + zeroPoint, low, high);
}
export function conv2d(x, weights, {stride = 1, pads = [0, 0, 0, 0], dilation = 1, groups = 1, bias = null} = {}) {
  const channels = x.length, height = x[0]?.length, width = x[0]?.[0]?.length;
  const outputs = weights.length, r = weights[0]?.[0]?.length, s = weights[0]?.[0]?.[0]?.length;
  if (!Number.isInteger(groups) || groups < 1 || channels % groups || outputs % groups) throw new RangeError('Invalid groups');
  if (pads.length !== 4) throw new RangeError('Four explicit pads required');
  if (!height || !width || !r || !s || x.some(c => c.length !== height || c.some(row => row.length !== width)) ||
      weights.some(m => m.length !== channels / groups || m.some(c => c.length !== r || c.some(row => row.length !== s))))
    throw new RangeError('Ragged or incompatible tensor');
  if (bias && bias.length !== outputs) throw new RangeError('Bias shape');
  if (x.flat(2).some(value => !Number.isSafeInteger(value)) ||
      weights.flat(3).some(value => !Number.isSafeInteger(value)) ||
      (bias && bias.some(value => !Number.isSafeInteger(value))))
    throw new RangeError('Safe integer operands and bias required');
  const [top, left, bottom, right] = pads;
  const outH = outputSize(height, r, stride, top, bottom, dilation);
  const outW = outputSize(width, s, stride, left, right, dilation);
  const y = Array.from({length: outputs}, () => Array.from({length: outH}, () => Array(outW).fill(0)));
  for (let m = 0; m < outputs; m++) for (let h = 0; h < outH; h++) for (let w = 0; w < outW; w++) {
    let acc = bias?.[m] ?? 0;
    const firstC = Math.floor(m / (outputs / groups)) * (channels / groups);
    for (let c = 0; c < channels / groups; c++) for (let kh = 0; kh < r; kh++) for (let kw = 0; kw < s; kw++) {
      const ih = h * stride - top + kh * dilation, iw = w * stride - left + kw * dilation;
      if (ih >= 0 && ih < height && iw >= 0 && iw < width) {
        const product = x[firstC + c][ih][iw] * weights[m][c][kh][kw];
        if (!Number.isSafeInteger(product)) throw new RangeError('Unsafe integer product');
        const next = acc + product;
        if (!Number.isSafeInteger(next)) throw new RangeError('Unsafe integer partial sum');
        acc = next;
      }
    }
    if (!Number.isSafeInteger(acc)) throw new RangeError('This teaching model requires safe integer accumulators');
    y[m][h][w] = acc;
  }
  return y;
}
export function im2col(x, kernelH, kernelW) {
  const height = x[0].length, width = x[0][0].length, rows = [];
  for (let h = 0; h <= height - kernelH; h++) for (let w = 0; w <= width - kernelW; w++) {
    const row = [];
    for (const channel of x) for (let r = 0; r < kernelH; r++) for (let s = 0; s < kernelW; s++) row.push(channel[h + r][w + s]);
    rows.push(row);
  }
  return rows;
}
export function dot(a, b) {
  if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
    throw new RangeError('Incompatible dot vectors');
  let acc = 0;
  for (let i = 0; i < a.length; i++) {
    if (!Number.isSafeInteger(a[i]) || !Number.isSafeInteger(b[i]))
      throw new RangeError('Safe integer dot operands required');
    const product = a[i] * b[i];
    if (!Number.isSafeInteger(product)) throw new RangeError('Unsafe integer dot product');
    const next = acc + product;
    if (!Number.isSafeInteger(next)) throw new RangeError('Unsafe integer dot partial sum');
    acc = next;
  }
  return acc;
}
export function maxPool2x2(matrix) {
  const output = [];
  for (let h = 0; h + 1 < matrix.length; h += 2) {
    const row = [];
    for (let w = 0; w + 1 < matrix[0].length; w += 2)
      row.push(Math.max(matrix[h][w], matrix[h][w + 1], matrix[h + 1][w], matrix[h + 1][w + 1]));
    output.push(row);
  }
  return output;
}

corner-lab.mjs

javascript
import assert from 'node:assert/strict';
import {conv2d, im2col, dot, outputSize, quantize, requantize} from './model.mjs';
const x = [[[1, 2, 3], [4, 5, 6], [7, 8, 9]]], w = [[[[1, 2], [3, 4]]]];
assert.deepEqual(conv2d(x, w), [[[37, 47], [67, 77]]]);
assert.deepEqual(im2col(x, 2, 2).map(row => dot(row, [1, 2, 3, 4])), [37, 47, 67, 77]);
assert.equal(conv2d(x, w, {pads:[1, 0, 0, 1]})[0][0][0], 11);
assert.equal(conv2d(x, w, {dilation:2})[0][0][0], 64);
assert.equal(outputSize(5, 3, 2, 1, 1, 1), 3);
assert.equal(outputSize(5, 3, 2, 1, 1, 2), 2);
assert.equal(quantize(0, 0.25, 128, 0, 255), 128);
assert.equal(requantize(300, 1), 127);
assert.equal(requantize(-300, 1), -128);
const channels = [[[2]], [[3]]], groupWeights = [[[[4]]], [[[5]]]];
assert.deepEqual(conv2d(channels, groupWeights, {groups:2}), [[[8]], [[15]]]);
assert.throws(() => conv2d(channels, groupWeights), /incompatible/);
// Guard every arithmetic step: cancellation must not hide lost precision.
const safe = Number.MAX_SAFE_INTEGER, pairWeights = [[[[1, 1]]]];
assert.throws(() => conv2d([[[2, -2]]], pairWeights, {bias:[safe]}), /partial sum/);
assert.throws(() => conv2d([[[-2, 2]]], pairWeights, {bias:[-safe]}), /partial sum/);
assert.throws(() => conv2d([[[safe + 1]]], [[[[1]]]]), /operands/);
assert.throws(() => conv2d([[[0]]], [[[[safe + 1]]]]), /operands/);
assert.throws(() => conv2d([[[-1]]], [[[[1]]]], {bias:[safe + 1]}), /bias/);
assert.throws(() => conv2d([[[0.5]]], [[[[2]]]]), /operands/);
assert.throws(() => conv2d([[[100000000]]], [[[[100000000]]]]), /product/);
// Safe boundary arithmetic and zero output dimensions remain supported.
assert.deepEqual(conv2d([[[2, -2]]], pairWeights, {bias:[safe - 2]}), [[[safe - 2]]]);
assert.deepEqual(conv2d([[[1]]], [[[[1, 1], [1, 1]]]]), [[]]);
assert.deepEqual(conv2d([[[1]]], pairWeights), [[[]]]);
// The independent patch-dot path uses the same integer safety contract.
assert.throws(() => dot([safe, 2, -2], [1, 1, 1]), /partial sum/);
assert.throws(() => dot([safe + 1], [0]), /operands/);
assert.throws(() => dot([100000000], [100000000]), /product/);
assert.throws(() => dot([1], [1, 2]), /vectors/);
assert.equal(dot([safe - 2, 2, -2], [1, 1, 1]), safe - 2);
assert.equal(dot([], []), 0);
console.log('corner-lab: 27 checks passed');

Try it yourself

In the padding fixture, replace every kernel weight by one. What is the first output and why is a center-only test insufficient?

Read the explanation

The first window [0,0;1,2] sums to three. A center window has no out-of-bounds access and may miss padding-value or top/left-coordinate errors.

Lesson files

model.mjs ↓corner-lab.mjs ↓

Your choice applies to this browser. Change it any time using the footer.