From 5fe7bfe07968b8c803b7c1e2860d08b4c40ca23d Mon Sep 17 00:00:00 2001 From: noelEOS <127597880+noelEOS@users.noreply.github.com> Date: Tue, 3 Nov 2026 09:40:24 +0000 Subject: [PATCH 1/2] test: make fusion rounding probe independent of accumulation order --- tests/reference/upernet/helpers.py | 22 +++++++------- tests/test_upernet_fusion_optimized.py | 40 +++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/tests/reference/upernet/helpers.py b/tests/reference/upernet/helpers.py index bdd1bfb..d2c0266 100644 --- a/tests/reference/upernet/helpers.py +++ b/tests/reference/upernet/helpers.py @@ -1,11 +1,11 @@ -"""Small NumPy and PyTorch helpers the ported decoder tests use, copied unchanged (apart from -their imports) from research modules archived at the tag ``research-archive-2026-10-12``: +"""Small NumPy and PyTorch helpers adapted from research modules archived at the tag +``research-archive-2026-10-12``: - ``pixel_ids_numpy``: ``upernet/earth_engine/feature_interface.py``; - ``fold``: ``upernet/earth_engine/normalization.py`` (BatchNorm folded into a conv weight; the decoder runs explicit BatchNorm instead, and this shows why); -- ``grouped_probe_inputs``: ``upernet/earth_engine/validate_fusion.py`` (the Phase 16 fixture - that separates the fusion formulations); +- ``grouped_probe_inputs``: ``upernet/earth_engine/validate_fusion.py`` (the Phase 16 fixture, + with smaller magnitudes so its double sums are independent of accumulation order); - ``torch_resize_matrix``: ``upernet/reference/m12_architecture_contract.py`` (PyTorch's own bilinear resize matrix, read off one-hot inputs). """ @@ -35,16 +35,18 @@ def fold(weight: np.ndarray, gamma, beta, mean, var, eps: float = BN_EPS) -> tup def grouped_probe_inputs(): """A one-window fixture (7 x 7 tokens, four groups of two channels) whose fusion conv - reveals each formulation's rounding. Output channel 0 at the centre token is - 2^40 (group 1) + 1 (group 1) - 2^40 (group 2); channel 1 is 2^53 + 1 (group 3) + 1 - - 2^53 (group 4). Monolithic (one sequential double dot): 1, 0; A (groups rounded to - float32): 0, 0; B (group dots in double, summed in double): 1, 1.""" + separates A's float32 partials from B and the monolithic convolution. Channel 0 + is 2^40 + 1 (group 1) - 2^40 (group 2); channel 1 is 2^40 + 1 (group 3) + 1 - + 2^40 (group 4). All products and every subtotal are integers of magnitude at most + 2^41 + 2, exactly representable in double in any accumulation order. Monolithic + and B give (1, 2); A rounds away the units in each partial and gives (0, 0). + This is an offline probe, not the historical live fixture's 2^53 example.""" x = np.zeros((7, 7, 8), np.float32) x[3, 3, [0, 1, 2]] = [2.0 ** 20, 1.0, -(2.0 ** 20)] - x[3, 3, [4, 5, 6, 7]] = [2.0 ** 27, 1.0, 1.0, -(2.0 ** 27)] + x[3, 3, [4, 5, 6, 7]] = [2.0 ** 20, 1.0, 1.0, -(2.0 ** 20)] w = np.zeros((2, 8, 3, 3), np.float32) w[0, [0, 1, 2], 1, 1] = [2.0 ** 20, 1.0, 2.0 ** 20] - w[1, [4, 5, 6, 7], 1, 1] = [2.0 ** 26, 1.0, 1.0, 2.0 ** 26] + w[1, [4, 5, 6, 7], 1, 1] = [2.0 ** 20, 1.0, 1.0, 2.0 ** 20] params = {R.WEIGHT: w, f"{R.PREFIX}.1.weight": np.ones(2, np.float32), f"{R.PREFIX}.1.bias": np.zeros(2, np.float32), f"{R.PREFIX}.1.running_mean": np.zeros(2, np.float32), diff --git a/tests/test_upernet_fusion_optimized.py b/tests/test_upernet_fusion_optimized.py index 6b51c1b..f7d4e7e 100644 --- a/tests/test_upernet_fusion_optimized.py +++ b/tests/test_upernet_fusion_optimized.py @@ -66,14 +66,46 @@ def test_grouped_double_differs_only_at_rounding_boundaries(case): def test_rounding_probe_fixture_separates_the_formulations(): - """Phase 16 probe (run live by the archived validate_fusion): channel 0 = 2^40 + 1 - 2^40 across groups, - channel 1 = 2^53 + 1 | + 1 - 2^53: monolithic (1, 0), A (0, 0), B (1, 1).""" + """Exact double sums in any order: monolithic and B (1, 2), rounded A (0, 0). + + Each product is an integer and the sum of absolute products is at most 2^41 + 2, + below double's exact-integer limit. Float32 partials still lose the unit terms. + """ from reference.upernet.helpers import grouped_probe_inputs x, w, params = grouped_probe_inputs() resized = {R.RESIZED[n]: x[..., 2 * g:2 * g + 2].astype(np.float64) for g, n in enumerate(R.PYRAMID)} - assert list(MF.conv3x3(x, w)[3, 3]) == [1.0, 0.0] + assert list(MF.conv3x3(x, w)[3, 3]) == [1.0, 2.0] assert list(MU.grouped_rounded(resized, params)["grouped_sum"][3, 3]) == [0.0, 0.0] - assert list(MU.grouped_double(resized, params)["double_sum"][3, 3]) == [1.0, 1.0] + assert list(MU.grouped_double(resized, params)["double_sum"][3, 3]) == [1.0, 2.0] + + +def test_historical_rounding_probe_with_explicit_sequential_accumulation(): + """Historical Phase 16 example: monolithic (1, 0), A (0, 0), B (1, 1). + + This result depends on accumulation order: sequential 2^53 + 1 + 1 - 2^53 + loses both units, while regrouping can retain them. Use fixed scalar additions, + never a BLAS/MKL reduction whose SIMD path may reassociate the double sum. + """ + channels = ( + ((2.0 ** 40, 1.0), (-(2.0 ** 40),), (), ()), + ((), (), (2.0 ** 53, 1.0), (1.0, -(2.0 ** 53))), + ) + + def sequential(terms, rounding=float): + total = 0.0 + for term in terms: + total = float(rounding(total + float(term))) + return total + + monolithic, rounded, double = [], [], [] + for groups in channels: + partials = [sequential(group) for group in groups] + monolithic.append(float(np.float32(sequential(term for group in groups for term in group)))) + rounded.append(sequential([float(np.float32(p)) for p in partials], np.float32)) + double.append(float(np.float32(sequential(partials)))) + assert monolithic == [1.0, 0.0] + assert rounded == [0.0, 0.0] + assert double == [1.0, 1.0] def test_order_study(case): From 08b907a748a0e4f94912e07b3e35ed3f6ee4ab28 Mon Sep 17 00:00:00 2001 From: noelEOS <127597880+noelEOS@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:06:49 +0000 Subject: [PATCH 2/2] test: retain historical grouped fusion rounding coverage --- tests/reference/upernet/helpers.py | 30 +++++++++++++++--- tests/test_upernet_fusion_optimized.py | 43 ++++++++++++++------------ 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/tests/reference/upernet/helpers.py b/tests/reference/upernet/helpers.py index d2c0266..d34667e 100644 --- a/tests/reference/upernet/helpers.py +++ b/tests/reference/upernet/helpers.py @@ -1,11 +1,14 @@ -"""Small NumPy and PyTorch helpers adapted from research modules archived at the tag -``research-archive-2026-10-12``: +"""Small NumPy and PyTorch helpers copied unchanged (apart from imports and the historical +fixture's name) from research modules archived at ``research-archive-2026-10-12``, +plus a new portable rounding probe: - ``pixel_ids_numpy``: ``upernet/earth_engine/feature_interface.py``; - ``fold``: ``upernet/earth_engine/normalization.py`` (BatchNorm folded into a conv weight; the decoder runs explicit BatchNorm instead, and this shows why); -- ``grouped_probe_inputs``: ``upernet/earth_engine/validate_fusion.py`` (the Phase 16 fixture, - with smaller magnitudes so its double sums are independent of accumulation order); +- ``grouped_probe_inputs_historical``: ``upernet/earth_engine/validate_fusion.py`` + (the original Phase 16 fixture, unchanged); +- ``grouped_probe_inputs``: a new portable fixture whose double sums are independent + of accumulation order; - ``torch_resize_matrix``: ``upernet/reference/m12_architecture_contract.py`` (PyTorch's own bilinear resize matrix, read off one-hot inputs). """ @@ -54,6 +57,25 @@ def grouped_probe_inputs(): return x, w, params +def grouped_probe_inputs_historical(): + """A one-window fixture (7 x 7 tokens, four groups of two channels) whose fusion conv + reveals each formulation's rounding. Output channel 0 at the centre token is + 2^40 (group 1) + 1 (group 1) - 2^40 (group 2); channel 1 is 2^53 + 1 (group 3) + 1 - + 2^53 (group 4). Monolithic (one sequential double dot): 1, 0; A (groups rounded to + float32): 0, 0; B (group dots in double, summed in double): 1, 1.""" + x = np.zeros((7, 7, 8), np.float32) + x[3, 3, [0, 1, 2]] = [2.0 ** 20, 1.0, -(2.0 ** 20)] + x[3, 3, [4, 5, 6, 7]] = [2.0 ** 27, 1.0, 1.0, -(2.0 ** 27)] + w = np.zeros((2, 8, 3, 3), np.float32) + w[0, [0, 1, 2], 1, 1] = [2.0 ** 20, 1.0, 2.0 ** 20] + w[1, [4, 5, 6, 7], 1, 1] = [2.0 ** 26, 1.0, 1.0, 2.0 ** 26] + params = {R.WEIGHT: w, f"{R.PREFIX}.1.weight": np.ones(2, np.float32), + f"{R.PREFIX}.1.bias": np.zeros(2, np.float32), + f"{R.PREFIX}.1.running_mean": np.zeros(2, np.float32), + f"{R.PREFIX}.1.running_var": np.ones(2, np.float32)} + return x, w, params + + def torch_resize_matrix(n_in: int, n_out: int, align_corners: bool) -> np.ndarray: import torch.nn.functional as F eye = torch.eye(n_in, dtype=torch.float32).reshape(n_in, 1, n_in, 1) diff --git a/tests/test_upernet_fusion_optimized.py b/tests/test_upernet_fusion_optimized.py index f7d4e7e..509cc80 100644 --- a/tests/test_upernet_fusion_optimized.py +++ b/tests/test_upernet_fusion_optimized.py @@ -79,33 +79,38 @@ def test_rounding_probe_fixture_separates_the_formulations(): assert list(MU.grouped_double(resized, params)["double_sum"][3, 3]) == [1.0, 2.0] +def test_historical_rounding_probe_grouped_formulations(): + """Original 2^53 probe: real grouped A (0, 0), B (1, 1). + + Each group dot has at most two nonzero terms, so its result is independent of + the BLAS accumulation order. The four partials are added in MU's fixed order. + """ + from reference.upernet.helpers import grouped_probe_inputs_historical + x, _, params = grouped_probe_inputs_historical() + resized = {R.RESIZED[n]: x[..., 2 * g:2 * g + 2].astype(np.float64) for g, n in enumerate(R.PYRAMID)} + assert list(MU.grouped_rounded(resized, params)["grouped_sum"][3, 3]) == [0.0, 0.0] + assert list(MU.grouped_double(resized, params)["double_sum"][3, 3]) == [1.0, 1.0] + + def test_historical_rounding_probe_with_explicit_sequential_accumulation(): - """Historical Phase 16 example: monolithic (1, 0), A (0, 0), B (1, 1). + """Historical Phase 16 monolithic dot in sequential order gives (1, 0). This result depends on accumulation order: sequential 2^53 + 1 + 1 - 2^53 loses both units, while regrouping can retain them. Use fixed scalar additions, never a BLAS/MKL reduction whose SIMD path may reassociate the double sum. + A and B are checked through the real grouped code in the test above. """ - channels = ( - ((2.0 ** 40, 1.0), (-(2.0 ** 40),), (), ()), - ((), (), (2.0 ** 53, 1.0), (1.0, -(2.0 ** 53))), - ) - - def sequential(terms, rounding=float): + from reference.upernet.helpers import grouped_probe_inputs_historical + x, w, _ = grouped_probe_inputs_historical() + # Only the centre tap is nonzero; preserve its channel order explicitly. + products = x[3, 3].astype(np.float64) * w[:, :, 1, 1].astype(np.float64) + monolithic = [] + for channel in products: total = 0.0 - for term in terms: - total = float(rounding(total + float(term))) - return total - - monolithic, rounded, double = [], [], [] - for groups in channels: - partials = [sequential(group) for group in groups] - monolithic.append(float(np.float32(sequential(term for group in groups for term in group)))) - rounded.append(sequential([float(np.float32(p)) for p in partials], np.float32)) - double.append(float(np.float32(sequential(partials)))) + for term in channel: + total = total + float(term) + monolithic.append(float(np.float32(total))) assert monolithic == [1.0, 0.0] - assert rounded == [0.0, 0.0] - assert double == [1.0, 1.0] def test_order_study(case):