diff --git a/docs/building-blocks.md b/docs/building-blocks.md index d957e29..929d7b4 100644 --- a/docs/building-blocks.md +++ b/docs/building-blocks.md @@ -155,7 +155,7 @@ their own. | [`class_token_attention`](../tensor_ee/blocks/window/attention.py) | W → W | window | composite | no | Ready — window 3, heads 3, dim 12; window 14, heads 12, dim 768; scale_query=True: window 14, heads 12, dim 768; scale_query=True: window 3, heads 3, dim 18 | [evidence/class-token-offline.json](evidence/class-token-offline.json); [evidence/live-fixtures/20261029T174042Z-class_token.json](evidence/live-fixtures/20261029T174042Z-class_token.json); [evidence/class-token-query-scale-offline.json](evidence/class-token-query-scale-offline.json); [evidence/live-fixtures/20261102T122934Z-class_token_query_scale.json](evidence/live-fixtures/20261102T122934Z-class_token_query_scale.json) | | [`naive_attention`](../tensor_ee/blocks/window/attention.py) | W → W | window | `round_each_op` | no | Ready | [building-blocks-2026-10-11.md#36-linear-layers-and-attention](history/building-blocks-2026-10-11.md#36-linear-layers-and-attention); [ee_attention/notes/findings.md](history/ee_attention/notes/findings.md); [evidence/segformer_b0-live.json](evidence/segformer_b0-live.json) | | [`split_heads`](../tensor_ee/blocks/window/attention.py) | W → W | pointwise | none (no arithmetic) | no | Ready | [building-blocks-2026-10-11.md#36-linear-layers-and-attention](history/building-blocks-2026-10-11.md#36-linear-layers-and-attention); [swin_block/notes/findings.md](history/swin_block/notes/findings.md) | -| [`relative_position_bias`](../tensor_ee/blocks/window/attention.py) | V → V | pointwise | `double_round_once` | no | Ready — windows 5, 7, 8, 12 | [building-blocks-2026-10-11.md#36-linear-layers-and-attention](history/building-blocks-2026-10-11.md#36-linear-layers-and-attention); [swin_block/notes/findings.md](history/swin_block/notes/findings.md); [evidence/p01-offline.json](evidence/p01-offline.json); [evidence/live-fixtures/20261013T074301Z-p01a.json](evidence/live-fixtures/20261013T074301Z-p01a.json); [evidence/live-fixtures/20261013T074330Z-p01b.json](evidence/live-fixtures/20261013T074330Z-p01b.json); [evidence/live-fixtures/20261013T075542Z-window-relative_position_bias-w12.json](evidence/live-fixtures/20261013T075542Z-window-relative_position_bias-w12.json) | +| [`relative_position_bias`](../tensor_ee/blocks/window/attention.py) | V → V | pointwise | `double_round_once` | no | Ready — windows 5, 7, 8, 12; gather=slices: windows 7, 12, 14, heads 2 | [building-blocks-2026-10-11.md#36-linear-layers-and-attention](history/building-blocks-2026-10-11.md#36-linear-layers-and-attention); [swin_block/notes/findings.md](history/swin_block/notes/findings.md); [evidence/p01-offline.json](evidence/p01-offline.json); [evidence/live-fixtures/20261013T074301Z-p01a.json](evidence/live-fixtures/20261013T074301Z-p01a.json); [evidence/live-fixtures/20261013T074330Z-p01b.json](evidence/live-fixtures/20261013T074330Z-p01b.json); [evidence/live-fixtures/20261013T075542Z-window-relative_position_bias-w12.json](evidence/live-fixtures/20261013T075542Z-window-relative_position_bias-w12.json); [evidence/window-relative-bias-slices-offline.json](evidence/window-relative-bias-slices-offline.json); [evidence/live-fixtures/20261103T173919Z-relative_bias_slices.json](evidence/live-fixtures/20261103T173919Z-relative_bias_slices.json) | | [`window_attention`](../tensor_ee/blocks/window/attention.py) | W → W | window | composite | no | Ready — windows 5, 7, 8, 12 | [building-blocks-2026-10-11.md#36-linear-layers-and-attention](history/building-blocks-2026-10-11.md#36-linear-layers-and-attention); *parts: linear (double_round_once), naive_attention (round_each_op)*; [swin_block/notes/findings.md](history/swin_block/notes/findings.md); [swin_pair/notes/findings.md](history/swin_pair/notes/findings.md); [evidence/p01-offline.json](evidence/p01-offline.json); [evidence/live-fixtures/20261013T074301Z-p01a.json](evidence/live-fixtures/20261013T074301Z-p01a.json); [evidence/live-fixtures/20261013T074330Z-p01b.json](evidence/live-fixtures/20261013T074330Z-p01b.json); [evidence/live-fixtures/20261013T075542Z-window-relative_position_bias-w12.json](evidence/live-fixtures/20261013T075542Z-window-relative_position_bias-w12.json) | | [`shifted_window_mask`](../tensor_ee/blocks/window/attention.py) | W → W | window | `double_round_once` | yes | Ready — windows 5, 7, 8, 12; shift ⌊w/2⌋ | [building-blocks-2026-10-11.md#36-linear-layers-and-attention](history/building-blocks-2026-10-11.md#36-linear-layers-and-attention); [swin_pair/notes/findings.md](history/swin_pair/notes/findings.md); [evidence/p01-offline.json](evidence/p01-offline.json); [evidence/live-fixtures/20261013T074301Z-p01a.json](evidence/live-fixtures/20261013T074301Z-p01a.json); [evidence/live-fixtures/20261013T074330Z-p01b.json](evidence/live-fixtures/20261013T074330Z-p01b.json); [evidence/live-fixtures/20261013T075542Z-window-relative_position_bias-w12.json](evidence/live-fixtures/20261013T075542Z-window-relative_position_bias-w12.json) | | [`depthwise_conv3x3_window`](../tensor_ee/blocks/window/conv.py) | W → W | window + ring of 1 | `double_round_once` | no | Ready — 3 × 3, stride1, padding1, dilation1, multiplier1; window7; D4 optional bias, D1024 biased; D16 biased in C4/D16 Mix-FFN | [evidence/segformer_b0-offline.json](evidence/segformer_b0-offline.json); [evidence/segformer_b0-live.json](evidence/segformer_b0-live.json) | @@ -256,7 +256,7 @@ Each block's `validated` record, the status in machine-readable form: 88 records | [`class_token_attention`](../tensor_ee/blocks/window/attention.py) | W → W | {dim 12; heads 3; scale_query False; window 3} ∪ {dim 768; heads 12; scale_query False; window 14} ∪ {dim 768; heads 12; scale_query True; window 14} ∪ {dim 18; heads 3; scale_query True; window 3} | — | | [`naive_attention`](../tensor_ee/blocks/window/attention.py) | W → W | any accepted call | — | | [`split_heads`](../tensor_ee/blocks/window/attention.py) | W → W | any accepted call | — | -| [`relative_position_bias`](../tensor_ee/blocks/window/attention.py) | V → V | {window 5 or 7 or 8 or 12} | — | +| [`relative_position_bias`](../tensor_ee/blocks/window/attention.py) | V → V | {gather 'one_hot'; window 5 or 7 or 8 or 12} ∪ {gather 'slices'; heads 2; window 7 or 12 or 14} | — | | [`window_attention`](../tensor_ee/blocks/window/attention.py) | W → W | {window 5 or 7 or 8 or 12} | — | | [`shifted_window_mask`](../tensor_ee/blocks/window/attention.py) | W → W | {shift 2 or None; value -100.0; window 5} ∪ {shift 3 or None; value -100.0; window 7} ∪ {shift 4 or None; value -100.0; window 8} ∪ {shift 6 or None; value -100.0; window 12} | — | | [`depthwise_conv3x3_window`](../tensor_ee/blocks/window/conv.py) | W → W | {bias True or False; channels 4; dilation 1; k 3; multiplier 1; padding 1; stride 1; window 7} ∪ {bias True; channels 16 or 1024; dilation 1; k 3; multiplier 1; padding 1; stride 1; window 7} | `DepthwiseConv3x3Constants` | diff --git a/docs/evidence/live-fixtures/20261103T173919Z-relative_bias_slices.json b/docs/evidence/live-fixtures/20261103T173919Z-relative_bias_slices.json new file mode 100644 index 0000000..ac860a4 --- /dev/null +++ b/docs/evidence/live-fixtures/20261103T173919Z-relative_bias_slices.json @@ -0,0 +1,1630 @@ +{ + "schema_version": 1, + "kind": "live_fixture_run", + "status": "passed", + "command": "python tools/live_fixtures.py --run relative_bias_slices --project tensor-ee --budget requests=8", + "selection": [ + "window/relative_position_bias/slices-w7", + "window/relative_position_bias/slices-w12", + "window/relative_position_bias/slices-w14", + "window/swin_block/bias-slices", + "window/swin_block/bias-slices-exact", + "window/shifted_swin_block/bias-slices", + "window/shifted_swin_block/bias-slices-exact" + ], + "project": "tensor-ee", + "credentials": "adc", + "revision": "d0f72e812797720db16acabbda0bb72f0b297c30", + "dirty": false, + "tensor_ee": "tensor_ee/__init__.py", + "versions": { + "python": "3.12.3", + "earthengine-api": "1.7.45", + "torch": "2.14.0+cpu", + "timm": "1.0.30", + "segmentation-models-pytorch": "0.5.0", + "numpy": "2.5.3" + }, + "platform": { + "system": "Linux", + "machine": "x86_64" + }, + "started_utc": "2026-11-03T17:39:19+00:00", + "budget": { + "requests": 8, + "raster_side": 64, + "window_side": 8, + "values": 1000000, + "request_bytes": 2000000, + "in_flight": 4, + "assets": 0, + "tasks": 0 + }, + "plan": { + "fixtures": 7, + "requests": 8, + "values": 163868, + "payload_bytes": 655472, + "request_bytes": 840710, + "max_values": 41760, + "max_request_bytes": 193069, + "max_bands": 290, + "embedded_constants": 113128, + "inline_values": 12544, + "assets": 0, + "tasks": 0, + "in_flight": 1 + }, + "max_in_flight": 1, + "errors": [], + "fixtures": [ + { + "spec": { + "id": "window/relative_position_bias/slices-w7", + "lane": "window", + "block": "relative_position_bias", + "parts": [], + "settings": { + "window": 7, + "gather": "slices" + }, + "policy": "double_round_once", + "shapes": { + "table": [ + 169, + 2 + ], + "heads": 2, + "output": [ + 2, + 49, + 49 + ] + }, + "seed": 20261103, + "tolerance": 0, + "requests": 1, + "reference": "timm WindowAttention._get_rel_pos_bias, fixed dyadic table", + "mirror": { + "source": "reference.swin_block.ee_mirror.Mirror.bias", + "exact": true, + "bound": null + }, + "history": "docs/swin.md", + "calibration": false, + "notes": "Every entry of both heads; optional gather, original status unchanged." + }, + "inputs_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "weights_sha256": "e9cbe836846d56b220f702373b89f7df7ce1acfee1681c6915ea2f62c7ea684a", + "status": "passed", + "wall_seconds": 0.354, + "failures": [], + "offline": { + "bias_h0": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 2401, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.1923682689666748, + "expected": -0.1923682689666748 + } + } + }, + "bias_h1": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 2401, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.32509613037109375, + "expected": -0.32509613037109375 + } + } + } + }, + "requests": [ + { + "index": 0, + "grid": { + "dimensions": { + "width": 7, + "height": 7 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 7, + "height": 7, + "readouts": [ + { + "name": "bias_h0", + "kind": "output", + "channels": 49 + }, + { + "name": "bias_h1", + "kind": "output", + "channels": 49 + } + ], + "bands": 100, + "values": 4900, + "payload_bytes": 19600, + "request_bytes": 15026, + "graph_sha256": "1b091c9436fbe472c347972ca0c2cbfbb93ad6c1b7d290985bae2a1344a9b1ea", + "graph_bytes": 14800, + "embedded_constants": 338, + "inline_values": 0, + "status": "ok", + "wall_seconds": 0.353 + } + ], + "outputs": { + "bias_h0": { + "kind": "output", + "request": 0, + "shape": [ + 7, + 7, + 49 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 2401, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.1923682689666748, + "expected": -0.1923682689666748 + } + }, + "mirror": { + "exact": true, + "bound": null, + "values": 2401, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + }, + "bias_h1": { + "kind": "output", + "request": 0, + "shape": [ + 7, + 7, + 49 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 2401, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.32509613037109375, + "expected": -0.32509613037109375 + } + }, + "mirror": { + "exact": true, + "bound": null, + "values": 2401, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + } + } + }, + { + "spec": { + "id": "window/relative_position_bias/slices-w12", + "lane": "window", + "block": "relative_position_bias", + "parts": [], + "settings": { + "window": 12, + "gather": "slices" + }, + "policy": "double_round_once", + "shapes": { + "table": [ + 529, + 2 + ], + "heads": 2, + "output": [ + 2, + 144, + 144 + ] + }, + "seed": 20261103, + "tolerance": 0, + "requests": 1, + "reference": "timm WindowAttention._get_rel_pos_bias, fixed dyadic table", + "mirror": { + "source": "reference.swin_block.ee_mirror.Mirror.bias", + "exact": true, + "bound": null + }, + "history": "docs/swin.md", + "calibration": false, + "notes": "Every entry of both heads; optional gather, original status unchanged." + }, + "inputs_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "weights_sha256": "1b8985d84d8bc189791bb06a495dc4c6599ada95ed12821a307a500725357dcf", + "status": "passed", + "wall_seconds": 0.469, + "failures": [], + "offline": { + "bias_h0": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 20736, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.16656339168548584, + "expected": -0.16656339168548584 + } + } + }, + "bias_h1": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 20736, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.9111435413360596, + "expected": -0.9111435413360596 + } + } + } + }, + "requests": [ + { + "index": 0, + "grid": { + "dimensions": { + "width": 12, + "height": 12 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 12, + "height": 12, + "readouts": [ + { + "name": "bias_h0", + "kind": "output", + "channels": 144 + }, + { + "name": "bias_h1", + "kind": "output", + "channels": 144 + } + ], + "bands": 290, + "values": 41760, + "payload_bytes": 167040, + "request_bytes": 27974, + "graph_sha256": "0dda7b014042c388f983b45cd2abdc640a37d36876283b022775c727b8d246dd", + "graph_bytes": 27746, + "embedded_constants": 1058, + "inline_values": 0, + "status": "ok", + "wall_seconds": 0.465 + } + ], + "outputs": { + "bias_h0": { + "kind": "output", + "request": 0, + "shape": [ + 12, + 12, + 144 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 20736, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.16656339168548584, + "expected": -0.16656339168548584 + } + }, + "mirror": { + "exact": true, + "bound": null, + "values": 20736, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + }, + "bias_h1": { + "kind": "output", + "request": 0, + "shape": [ + 12, + 12, + 144 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 20736, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.9111435413360596, + "expected": -0.9111435413360596 + } + }, + "mirror": { + "exact": true, + "bound": null, + "values": 20736, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + } + } + }, + { + "spec": { + "id": "window/relative_position_bias/slices-w14", + "lane": "window", + "block": "relative_position_bias", + "parts": [], + "settings": { + "window": 14, + "gather": "slices" + }, + "policy": "double_round_once", + "shapes": { + "table": [ + 729, + 2 + ], + "heads": 2, + "output": [ + 2, + 196, + 196 + ] + }, + "seed": 20261103, + "tolerance": 0, + "requests": 2, + "reference": "timm WindowAttention._get_rel_pos_bias, fixed dyadic table", + "mirror": { + "source": "reference.swin_block.ee_mirror.Mirror.bias", + "exact": true, + "bound": null + }, + "history": "docs/swin.md", + "calibration": false, + "notes": "Every entry of both heads; optional gather, original status unchanged." + }, + "inputs_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "weights_sha256": "7c75f8323b8ad09021e7f590b9346fbb3c3c888f7e6b96b8cb8c22219bc68a94", + "status": "passed", + "wall_seconds": 0.867, + "failures": [], + "offline": { + "bias_h0": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 38416, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.9654431343078613, + "expected": -0.9654431343078613 + } + } + }, + "bias_h1": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 38416, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.3381950855255127, + "expected": -0.3381950855255127 + } + } + } + }, + "requests": [ + { + "index": 0, + "grid": { + "dimensions": { + "width": 14, + "height": 14 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 14, + "height": 14, + "readouts": [ + { + "name": "bias_h0", + "kind": "output", + "channels": 196 + } + ], + "bands": 197, + "values": 38612, + "payload_bytes": 154448, + "request_bytes": 30067, + "graph_sha256": "055e41523eccfadef70be0ea89ea547c8811bc4240bea82b5045305fab858c6c", + "graph_bytes": 29839, + "embedded_constants": 1458, + "inline_values": 0, + "status": "ok", + "wall_seconds": 0.466 + }, + { + "index": 1, + "grid": { + "dimensions": { + "width": 14, + "height": 14 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 14, + "height": 14, + "readouts": [ + { + "name": "bias_h1", + "kind": "output", + "channels": 196 + } + ], + "bands": 197, + "values": 38612, + "payload_bytes": 154448, + "request_bytes": 30067, + "graph_sha256": "a8818f1cfbc1ad98dedba037bff5664b2ce590cacfa032f588679abeb780cdae", + "graph_bytes": 29839, + "embedded_constants": 1458, + "inline_values": 0, + "status": "ok", + "wall_seconds": 0.396 + } + ], + "outputs": { + "bias_h0": { + "kind": "output", + "request": 0, + "shape": [ + 14, + 14, + 196 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 38416, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.9654431343078613, + "expected": -0.9654431343078613 + } + }, + "mirror": { + "exact": true, + "bound": null, + "values": 38416, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + }, + "bias_h1": { + "kind": "output", + "request": 1, + "shape": [ + 14, + 14, + 196 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 38416, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.3381950855255127, + "expected": -0.3381950855255127 + } + }, + "mirror": { + "exact": true, + "bound": null, + "values": 38416, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + } + } + }, + { + "spec": { + "id": "window/swin_block/bias-slices", + "lane": "window", + "block": "swin_block", + "parts": [ + "relative_position_bias", + "window_attention", + "linear", + "layer_norm", + "gelu" + ], + "settings": { + "window": 7, + "relative_bias_gather": "slices" + }, + "policy": "composite", + "shapes": { + "tokens": [ + 14, + 14 + ], + "dim": 16, + "heads": 2, + "window": 7, + "shifted": false + }, + "seed": 20261103, + "tolerance": 0.0001, + "requests": 1, + "reference": "timm SwinTransformerBlock, fixed dyadic parameters, non-fused attention", + "mirror": { + "source": "reference.swin_block.Mirror / swin_pair.ShiftedMirror, sequential sums", + "exact": true, + "bound": null + }, + "history": "docs/swin.md", + "calibration": false, + "notes": "Both full-block outputs and optimized-minus-original. The difference must be exactly zero." + }, + "inputs_sha256": "14bec19999c54c6942666ec7804f4ad90e38658995d27683dcee230d513eb982", + "weights_sha256": "163d8d40880d69180a4e378ebf83bd56021d1816e3b41f56d76a3590d2f8782c", + "status": "passed", + "wall_seconds": 0.56, + "failures": [], + "offline": { + "original": { + "mirror_vs_pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 1.7881393432617188e-07, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 11, + "col": 3, + "channel": 8, + "actual": -0.6505010724067688, + "expected": -0.6505012512207031 + } + } + }, + "optimized": { + "mirror_vs_pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 1.7881393432617188e-07, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 11, + "col": 3, + "channel": 8, + "actual": -0.6505010724067688, + "expected": -0.6505012512207031 + } + } + }, + "difference": { + "mirror_vs_pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": 0.0, + "expected": 0.0 + } + } + } + }, + "requests": [ + { + "index": 0, + "grid": { + "dimensions": { + "width": 14, + "height": 14 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 14, + "height": 14, + "readouts": [ + { + "name": "original", + "kind": "output", + "channels": 16 + }, + { + "name": "optimized", + "kind": "output", + "channels": 16 + }, + { + "name": "difference", + "kind": "output", + "channels": 16 + }, + { + "name": "input", + "kind": "input", + "channels": 16 + } + ], + "bands": 68, + "values": 13328, + "payload_bytes": 53312, + "request_bytes": 180299, + "graph_sha256": "406410fc941ce0d0eac84e96978e71064bafef56aee14fda13a2ce6756652d38", + "graph_bytes": 180071, + "embedded_constants": 27204, + "inline_values": 3136, + "status": "ok", + "wall_seconds": 0.559 + } + ], + "outputs": { + "original": { + "kind": "output", + "request": 0, + "shape": [ + 14, + 14, + 16 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 1.7881393432617188e-07, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 11, + "col": 3, + "channel": 8, + "actual": -0.6505010724067688, + "expected": -0.6505012512207031 + } + }, + "mirror": { + "exact": true, + "bound": null, + "values": 3136, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + }, + "optimized": { + "kind": "output", + "request": 0, + "shape": [ + 14, + 14, + 16 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 1.7881393432617188e-07, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 11, + "col": 3, + "channel": 8, + "actual": -0.6505010724067688, + "expected": -0.6505012512207031 + } + }, + "mirror": { + "exact": true, + "bound": null, + "values": 3136, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + }, + "difference": { + "kind": "output", + "request": 0, + "shape": [ + 14, + 14, + 16 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": 0.0, + "expected": 0.0 + } + }, + "mirror": { + "exact": true, + "bound": null, + "values": 3136, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + }, + "input": { + "kind": "input", + "request": 0, + "shape": [ + 14, + 14, + 16 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "input": { + "exact": true, + "bound": null, + "values": 3136, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + } + } + }, + { + "spec": { + "id": "window/swin_block/bias-slices-exact", + "lane": "window", + "block": "swin_block", + "parts": [ + "relative_position_bias", + "window_attention", + "linear", + "layer_norm", + "gelu" + ], + "settings": { + "window": 7, + "relative_bias_gather": "slices" + }, + "policy": "composite", + "shapes": { + "tokens": [ + 14, + 14 + ], + "dim": 16, + "heads": 2, + "window": 7, + "shifted": false + }, + "seed": 20261103, + "tolerance": 0, + "requests": 1, + "reference": "timm SwinTransformerBlock, fixed dyadic parameters, non-fused attention", + "mirror": { + "source": "exact difference of the original and optimized full-block outputs", + "exact": true, + "bound": null + }, + "history": "docs/swin.md", + "calibration": false, + "notes": "Strict zero-tolerance equality of the two EE block outputs, independently of PyTorch rounding." + }, + "inputs_sha256": "14bec19999c54c6942666ec7804f4ad90e38658995d27683dcee230d513eb982", + "weights_sha256": "163d8d40880d69180a4e378ebf83bd56021d1816e3b41f56d76a3590d2f8782c", + "status": "passed", + "wall_seconds": 0.448, + "failures": [], + "offline": { + "difference": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 3136, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": 0.0, + "expected": 0.0 + } + } + } + }, + "requests": [ + { + "index": 0, + "grid": { + "dimensions": { + "width": 14, + "height": 14 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 14, + "height": 14, + "readouts": [ + { + "name": "difference", + "kind": "output", + "channels": 16 + }, + { + "name": "input", + "kind": "input", + "channels": 16 + } + ], + "bands": 34, + "values": 6664, + "payload_bytes": 26656, + "request_bytes": 175720, + "graph_sha256": "01fa01ce437490959a69cd83786cbceddbe2f3637cd2b9b2ebf3b4b186f605ef", + "graph_bytes": 175492, + "embedded_constants": 27204, + "inline_values": 3136, + "status": "ok", + "wall_seconds": 0.448 + } + ], + "outputs": { + "difference": { + "kind": "output", + "request": 0, + "shape": [ + 14, + 14, + 16 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 3136, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": 0.0, + "expected": 0.0 + } + }, + "mirror": { + "exact": true, + "bound": null, + "values": 3136, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + }, + "input": { + "kind": "input", + "request": 0, + "shape": [ + 14, + 14, + 16 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "input": { + "exact": true, + "bound": null, + "values": 3136, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + } + } + }, + { + "spec": { + "id": "window/shifted_swin_block/bias-slices", + "lane": "window", + "block": "shifted_swin_block", + "parts": [ + "relative_position_bias", + "window_attention", + "linear", + "layer_norm", + "gelu" + ], + "settings": { + "window": 7, + "relative_bias_gather": "slices" + }, + "policy": "composite", + "shapes": { + "tokens": [ + 14, + 14 + ], + "dim": 16, + "heads": 2, + "window": 7, + "shifted": true + }, + "seed": 20261103, + "tolerance": 0.0001, + "requests": 1, + "reference": "timm SwinTransformerBlock, fixed dyadic parameters, non-fused attention", + "mirror": { + "source": "reference.swin_block.Mirror / swin_pair.ShiftedMirror, sequential sums", + "exact": true, + "bound": null + }, + "history": "docs/swin.md", + "calibration": false, + "notes": "Both full-block outputs and optimized-minus-original. The difference must be exactly zero." + }, + "inputs_sha256": "14bec19999c54c6942666ec7804f4ad90e38658995d27683dcee230d513eb982", + "weights_sha256": "163d8d40880d69180a4e378ebf83bd56021d1816e3b41f56d76a3590d2f8782c", + "status": "passed", + "wall_seconds": 1.453, + "failures": [], + "offline": { + "original": { + "mirror_vs_pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 1.1920928955078125e-07, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 1, + "col": 5, + "channel": 9, + "actual": 0.7293606996536255, + "expected": 0.7293605804443359 + } + } + }, + "optimized": { + "mirror_vs_pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 1.1920928955078125e-07, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 1, + "col": 5, + "channel": 9, + "actual": 0.7293606996536255, + "expected": 0.7293605804443359 + } + } + }, + "difference": { + "mirror_vs_pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": 0.0, + "expected": 0.0 + } + } + } + }, + "requests": [ + { + "index": 0, + "grid": { + "dimensions": { + "width": 14, + "height": 14 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 14, + "height": 14, + "readouts": [ + { + "name": "original", + "kind": "output", + "channels": 16 + }, + { + "name": "optimized", + "kind": "output", + "channels": 16 + }, + { + "name": "difference", + "kind": "output", + "channels": 16 + }, + { + "name": "input", + "kind": "input", + "channels": 16 + } + ], + "bands": 68, + "values": 13328, + "payload_bytes": 53312, + "request_bytes": 193069, + "graph_sha256": "d7a11039768d574cf86b7f4d529171e692ae52d1eaee415354ed70569ee7dc7a", + "graph_bytes": 192841, + "embedded_constants": 27204, + "inline_values": 3136, + "status": "ok", + "wall_seconds": 1.451 + } + ], + "outputs": { + "original": { + "kind": "output", + "request": 0, + "shape": [ + 14, + 14, + 16 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 1.1920928955078125e-07, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 1, + "col": 5, + "channel": 9, + "actual": 0.7293606996536255, + "expected": 0.7293605804443359 + } + }, + "mirror": { + "exact": true, + "bound": null, + "values": 3136, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + }, + "optimized": { + "kind": "output", + "request": 0, + "shape": [ + 14, + 14, + 16 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 1.1920928955078125e-07, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 1, + "col": 5, + "channel": 9, + "actual": 0.7293606996536255, + "expected": 0.7293605804443359 + } + }, + "mirror": { + "exact": true, + "bound": null, + "values": 3136, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + }, + "difference": { + "kind": "output", + "request": 0, + "shape": [ + 14, + 14, + 16 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": 0.0, + "expected": 0.0 + } + }, + "mirror": { + "exact": true, + "bound": null, + "values": 3136, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + }, + "input": { + "kind": "input", + "request": 0, + "shape": [ + 14, + 14, + 16 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "input": { + "exact": true, + "bound": null, + "values": 3136, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + } + } + }, + { + "spec": { + "id": "window/shifted_swin_block/bias-slices-exact", + "lane": "window", + "block": "shifted_swin_block", + "parts": [ + "relative_position_bias", + "window_attention", + "linear", + "layer_norm", + "gelu" + ], + "settings": { + "window": 7, + "relative_bias_gather": "slices" + }, + "policy": "composite", + "shapes": { + "tokens": [ + 14, + 14 + ], + "dim": 16, + "heads": 2, + "window": 7, + "shifted": true + }, + "seed": 20261103, + "tolerance": 0, + "requests": 1, + "reference": "timm SwinTransformerBlock, fixed dyadic parameters, non-fused attention", + "mirror": { + "source": "exact difference of the original and optimized full-block outputs", + "exact": true, + "bound": null + }, + "history": "docs/swin.md", + "calibration": false, + "notes": "Strict zero-tolerance equality of the two EE block outputs, independently of PyTorch rounding." + }, + "inputs_sha256": "14bec19999c54c6942666ec7804f4ad90e38658995d27683dcee230d513eb982", + "weights_sha256": "163d8d40880d69180a4e378ebf83bd56021d1816e3b41f56d76a3590d2f8782c", + "status": "passed", + "wall_seconds": 0.299, + "failures": [], + "offline": { + "difference": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 3136, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": 0.0, + "expected": 0.0 + } + } + } + }, + "requests": [ + { + "index": 0, + "grid": { + "dimensions": { + "width": 14, + "height": 14 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 14, + "height": 14, + "readouts": [ + { + "name": "difference", + "kind": "output", + "channels": 16 + }, + { + "name": "input", + "kind": "input", + "channels": 16 + } + ], + "bands": 34, + "values": 6664, + "payload_bytes": 26656, + "request_bytes": 188488, + "graph_sha256": "cf57e65fa2c9ae45886d61dac0e06a1750ea497b4550bb9cca41e06cba3818f6", + "graph_bytes": 188260, + "embedded_constants": 27204, + "inline_values": 3136, + "status": "ok", + "wall_seconds": 0.298 + } + ], + "outputs": { + "difference": { + "kind": "output", + "request": 0, + "shape": [ + 14, + 14, + 16 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 3136, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": 0.0, + "expected": 0.0 + } + }, + "mirror": { + "exact": true, + "bound": null, + "values": 3136, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + }, + "input": { + "kind": "input", + "request": 0, + "shape": [ + 14, + 14, + 16 + ], + "dtypes": [ + "float32" + ], + "float32": true, + "invalid_pixels": 0, + "input": { + "exact": true, + "bound": null, + "values": 3136, + "differing": 0, + "max_abs": 0.0, + "first": [], + "pass": true + } + } + } + } + ], + "cloud_calls": { + "requests": 8, + "computePixels": 8, + "algorithms_list": 1, + "refused": [] + }, + "finished_utc": "2026-11-03T17:39:31+00:00", + "wall_seconds": 11.978 +} diff --git a/docs/evidence/window-relative-bias-slices-offline.json b/docs/evidence/window-relative-bias-slices-offline.json new file mode 100644 index 0000000..5c4e42e --- /dev/null +++ b/docs/evidence/window-relative-bias-slices-offline.json @@ -0,0 +1,947 @@ +{ + "schema_version": 1, + "kind": "offline_fixture_check", + "cloud_calls": 0, + "status": "passed", + "revision": "9d55827e0f6753695f8e6b57bbcd2a3eb3138dda", + "dirty": false, + "tensor_ee": "tensor_ee/__init__.py", + "versions": { + "python": "3.12.3", + "earthengine-api": "1.7.45", + "torch": "2.14.0+cpu", + "timm": "1.0.30", + "segmentation-models-pytorch": "0.5.0", + "numpy": "2.5.3" + }, + "platform": { + "system": "Linux", + "machine": "x86_64" + }, + "plan": { + "totals": { + "fixtures": 7, + "requests": 8, + "values": 163868, + "payload_bytes": 655472, + "request_bytes": 840710, + "max_values": 41760, + "max_request_bytes": 193069, + "max_bands": 290, + "embedded_constants": 113128, + "inline_values": 12544, + "assets": 0, + "tasks": 0, + "in_flight": 1 + }, + "violations": [] + }, + "fixtures": [ + { + "spec": { + "id": "window/relative_position_bias/slices-w7", + "lane": "window", + "block": "relative_position_bias", + "parts": [], + "settings": { + "window": 7, + "gather": "slices" + }, + "policy": "double_round_once", + "shapes": { + "table": [ + 169, + 2 + ], + "heads": 2, + "output": [ + 2, + 49, + 49 + ] + }, + "seed": 20261103, + "tolerance": 0, + "requests": 1, + "reference": "timm WindowAttention._get_rel_pos_bias, fixed dyadic table", + "mirror": { + "source": "reference.swin_block.ee_mirror.Mirror.bias", + "exact": true, + "bound": null + }, + "history": "docs/swin.md", + "calibration": false, + "notes": "Every entry of both heads; optional gather, original status unchanged." + }, + "inputs_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "weights_sha256": "e9cbe836846d56b220f702373b89f7df7ce1acfee1681c6915ea2f62c7ea684a", + "requests": [ + { + "index": 0, + "grid": { + "dimensions": { + "width": 7, + "height": 7 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 7, + "height": 7, + "readouts": [ + { + "name": "bias_h0", + "kind": "output", + "channels": 49 + }, + { + "name": "bias_h1", + "kind": "output", + "channels": 49 + } + ], + "bands": 100, + "values": 4900, + "payload_bytes": 19600, + "request_bytes": 15026, + "graph_sha256": "1b091c9436fbe472c347972ca0c2cbfbb93ad6c1b7d290985bae2a1344a9b1ea", + "graph_bytes": 14800, + "embedded_constants": 338, + "inline_values": 0 + } + ], + "errors": [], + "seconds": 2.296, + "outputs": { + "bias_h0": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 2401, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.1923682689666748, + "expected": -0.1923682689666748 + } + } + }, + "bias_h1": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 2401, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.32509613037109375, + "expected": -0.32509613037109375 + } + } + } + }, + "status": "passed" + }, + { + "spec": { + "id": "window/relative_position_bias/slices-w12", + "lane": "window", + "block": "relative_position_bias", + "parts": [], + "settings": { + "window": 12, + "gather": "slices" + }, + "policy": "double_round_once", + "shapes": { + "table": [ + 529, + 2 + ], + "heads": 2, + "output": [ + 2, + 144, + 144 + ] + }, + "seed": 20261103, + "tolerance": 0, + "requests": 1, + "reference": "timm WindowAttention._get_rel_pos_bias, fixed dyadic table", + "mirror": { + "source": "reference.swin_block.ee_mirror.Mirror.bias", + "exact": true, + "bound": null + }, + "history": "docs/swin.md", + "calibration": false, + "notes": "Every entry of both heads; optional gather, original status unchanged." + }, + "inputs_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "weights_sha256": "1b8985d84d8bc189791bb06a495dc4c6599ada95ed12821a307a500725357dcf", + "requests": [ + { + "index": 0, + "grid": { + "dimensions": { + "width": 12, + "height": 12 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 12, + "height": 12, + "readouts": [ + { + "name": "bias_h0", + "kind": "output", + "channels": 144 + }, + { + "name": "bias_h1", + "kind": "output", + "channels": 144 + } + ], + "bands": 290, + "values": 41760, + "payload_bytes": 167040, + "request_bytes": 27974, + "graph_sha256": "0dda7b014042c388f983b45cd2abdc640a37d36876283b022775c727b8d246dd", + "graph_bytes": 27746, + "embedded_constants": 1058, + "inline_values": 0 + } + ], + "errors": [], + "seconds": 0.076, + "outputs": { + "bias_h0": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 20736, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.16656339168548584, + "expected": -0.16656339168548584 + } + } + }, + "bias_h1": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 20736, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.9111435413360596, + "expected": -0.9111435413360596 + } + } + } + }, + "status": "passed" + }, + { + "spec": { + "id": "window/relative_position_bias/slices-w14", + "lane": "window", + "block": "relative_position_bias", + "parts": [], + "settings": { + "window": 14, + "gather": "slices" + }, + "policy": "double_round_once", + "shapes": { + "table": [ + 729, + 2 + ], + "heads": 2, + "output": [ + 2, + 196, + 196 + ] + }, + "seed": 20261103, + "tolerance": 0, + "requests": 2, + "reference": "timm WindowAttention._get_rel_pos_bias, fixed dyadic table", + "mirror": { + "source": "reference.swin_block.ee_mirror.Mirror.bias", + "exact": true, + "bound": null + }, + "history": "docs/swin.md", + "calibration": false, + "notes": "Every entry of both heads; optional gather, original status unchanged." + }, + "inputs_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "weights_sha256": "7c75f8323b8ad09021e7f590b9346fbb3c3c888f7e6b96b8cb8c22219bc68a94", + "requests": [ + { + "index": 0, + "grid": { + "dimensions": { + "width": 14, + "height": 14 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 14, + "height": 14, + "readouts": [ + { + "name": "bias_h0", + "kind": "output", + "channels": 196 + } + ], + "bands": 197, + "values": 38612, + "payload_bytes": 154448, + "request_bytes": 30067, + "graph_sha256": "055e41523eccfadef70be0ea89ea547c8811bc4240bea82b5045305fab858c6c", + "graph_bytes": 29839, + "embedded_constants": 1458, + "inline_values": 0 + }, + { + "index": 1, + "grid": { + "dimensions": { + "width": 14, + "height": 14 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 14, + "height": 14, + "readouts": [ + { + "name": "bias_h1", + "kind": "output", + "channels": 196 + } + ], + "bands": 197, + "values": 38612, + "payload_bytes": 154448, + "request_bytes": 30067, + "graph_sha256": "a8818f1cfbc1ad98dedba037bff5664b2ce590cacfa032f588679abeb780cdae", + "graph_bytes": 29839, + "embedded_constants": 1458, + "inline_values": 0 + } + ], + "errors": [], + "seconds": 0.075, + "outputs": { + "bias_h0": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 38416, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.9654431343078613, + "expected": -0.9654431343078613 + } + } + }, + "bias_h1": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 38416, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": -0.3381950855255127, + "expected": -0.3381950855255127 + } + } + } + }, + "status": "passed" + }, + { + "spec": { + "id": "window/swin_block/bias-slices", + "lane": "window", + "block": "swin_block", + "parts": [ + "relative_position_bias", + "window_attention", + "linear", + "layer_norm", + "gelu" + ], + "settings": { + "window": 7, + "relative_bias_gather": "slices" + }, + "policy": "composite", + "shapes": { + "tokens": [ + 14, + 14 + ], + "dim": 16, + "heads": 2, + "window": 7, + "shifted": false + }, + "seed": 20261103, + "tolerance": 0.0001, + "requests": 1, + "reference": "timm SwinTransformerBlock, fixed dyadic parameters, non-fused attention", + "mirror": { + "source": "reference.swin_block.Mirror / swin_pair.ShiftedMirror, sequential sums", + "exact": true, + "bound": null + }, + "history": "docs/swin.md", + "calibration": false, + "notes": "Both full-block outputs and optimized-minus-original. The difference must be exactly zero." + }, + "inputs_sha256": "14bec19999c54c6942666ec7804f4ad90e38658995d27683dcee230d513eb982", + "weights_sha256": "163d8d40880d69180a4e378ebf83bd56021d1816e3b41f56d76a3590d2f8782c", + "requests": [ + { + "index": 0, + "grid": { + "dimensions": { + "width": 14, + "height": 14 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 14, + "height": 14, + "readouts": [ + { + "name": "original", + "kind": "output", + "channels": 16 + }, + { + "name": "optimized", + "kind": "output", + "channels": 16 + }, + { + "name": "difference", + "kind": "output", + "channels": 16 + }, + { + "name": "input", + "kind": "input", + "channels": 16 + } + ], + "bands": 68, + "values": 13328, + "payload_bytes": 53312, + "request_bytes": 180299, + "graph_sha256": "406410fc941ce0d0eac84e96978e71064bafef56aee14fda13a2ce6756652d38", + "graph_bytes": 180071, + "embedded_constants": 27204, + "inline_values": 3136 + } + ], + "errors": [], + "seconds": 0.094, + "outputs": { + "original": { + "mirror_vs_pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 1.7881393432617188e-07, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 11, + "col": 3, + "channel": 8, + "actual": -0.6505010724067688, + "expected": -0.6505012512207031 + } + } + }, + "optimized": { + "mirror_vs_pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 1.7881393432617188e-07, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 11, + "col": 3, + "channel": 8, + "actual": -0.6505010724067688, + "expected": -0.6505012512207031 + } + } + }, + "difference": { + "mirror_vs_pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": 0.0, + "expected": 0.0 + } + } + } + }, + "status": "passed" + }, + { + "spec": { + "id": "window/swin_block/bias-slices-exact", + "lane": "window", + "block": "swin_block", + "parts": [ + "relative_position_bias", + "window_attention", + "linear", + "layer_norm", + "gelu" + ], + "settings": { + "window": 7, + "relative_bias_gather": "slices" + }, + "policy": "composite", + "shapes": { + "tokens": [ + 14, + 14 + ], + "dim": 16, + "heads": 2, + "window": 7, + "shifted": false + }, + "seed": 20261103, + "tolerance": 0, + "requests": 1, + "reference": "timm SwinTransformerBlock, fixed dyadic parameters, non-fused attention", + "mirror": { + "source": "exact difference of the original and optimized full-block outputs", + "exact": true, + "bound": null + }, + "history": "docs/swin.md", + "calibration": false, + "notes": "Strict zero-tolerance equality of the two EE block outputs, independently of PyTorch rounding." + }, + "inputs_sha256": "14bec19999c54c6942666ec7804f4ad90e38658995d27683dcee230d513eb982", + "weights_sha256": "163d8d40880d69180a4e378ebf83bd56021d1816e3b41f56d76a3590d2f8782c", + "requests": [ + { + "index": 0, + "grid": { + "dimensions": { + "width": 14, + "height": 14 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 14, + "height": 14, + "readouts": [ + { + "name": "difference", + "kind": "output", + "channels": 16 + }, + { + "name": "input", + "kind": "input", + "channels": 16 + } + ], + "bands": 34, + "values": 6664, + "payload_bytes": 26656, + "request_bytes": 175720, + "graph_sha256": "01fa01ce437490959a69cd83786cbceddbe2f3637cd2b9b2ebf3b4b186f605ef", + "graph_bytes": 175492, + "embedded_constants": 27204, + "inline_values": 3136 + } + ], + "errors": [], + "seconds": 0.078, + "outputs": { + "difference": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 3136, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": 0.0, + "expected": 0.0 + } + } + } + }, + "status": "passed" + }, + { + "spec": { + "id": "window/shifted_swin_block/bias-slices", + "lane": "window", + "block": "shifted_swin_block", + "parts": [ + "relative_position_bias", + "window_attention", + "linear", + "layer_norm", + "gelu" + ], + "settings": { + "window": 7, + "relative_bias_gather": "slices" + }, + "policy": "composite", + "shapes": { + "tokens": [ + 14, + 14 + ], + "dim": 16, + "heads": 2, + "window": 7, + "shifted": true + }, + "seed": 20261103, + "tolerance": 0.0001, + "requests": 1, + "reference": "timm SwinTransformerBlock, fixed dyadic parameters, non-fused attention", + "mirror": { + "source": "reference.swin_block.Mirror / swin_pair.ShiftedMirror, sequential sums", + "exact": true, + "bound": null + }, + "history": "docs/swin.md", + "calibration": false, + "notes": "Both full-block outputs and optimized-minus-original. The difference must be exactly zero." + }, + "inputs_sha256": "14bec19999c54c6942666ec7804f4ad90e38658995d27683dcee230d513eb982", + "weights_sha256": "163d8d40880d69180a4e378ebf83bd56021d1816e3b41f56d76a3590d2f8782c", + "requests": [ + { + "index": 0, + "grid": { + "dimensions": { + "width": 14, + "height": 14 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 14, + "height": 14, + "readouts": [ + { + "name": "original", + "kind": "output", + "channels": 16 + }, + { + "name": "optimized", + "kind": "output", + "channels": 16 + }, + { + "name": "difference", + "kind": "output", + "channels": 16 + }, + { + "name": "input", + "kind": "input", + "channels": 16 + } + ], + "bands": 68, + "values": 13328, + "payload_bytes": 53312, + "request_bytes": 193069, + "graph_sha256": "d7a11039768d574cf86b7f4d529171e692ae52d1eaee415354ed70569ee7dc7a", + "graph_bytes": 192841, + "embedded_constants": 27204, + "inline_values": 3136 + } + ], + "errors": [], + "seconds": 0.094, + "outputs": { + "original": { + "mirror_vs_pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 1.1920928955078125e-07, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 1, + "col": 5, + "channel": 9, + "actual": 0.7293606996536255, + "expected": 0.7293605804443359 + } + } + }, + "optimized": { + "mirror_vs_pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 1.1920928955078125e-07, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 1, + "col": 5, + "channel": 9, + "actual": 0.7293606996536255, + "expected": 0.7293605804443359 + } + } + }, + "difference": { + "mirror_vs_pytorch": { + "tolerance": 0.0001, + "comparison": "absolute", + "values": 3136, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": 0.0, + "expected": 0.0 + } + } + } + }, + "status": "passed" + }, + { + "spec": { + "id": "window/shifted_swin_block/bias-slices-exact", + "lane": "window", + "block": "shifted_swin_block", + "parts": [ + "relative_position_bias", + "window_attention", + "linear", + "layer_norm", + "gelu" + ], + "settings": { + "window": 7, + "relative_bias_gather": "slices" + }, + "policy": "composite", + "shapes": { + "tokens": [ + 14, + 14 + ], + "dim": 16, + "heads": 2, + "window": 7, + "shifted": true + }, + "seed": 20261103, + "tolerance": 0, + "requests": 1, + "reference": "timm SwinTransformerBlock, fixed dyadic parameters, non-fused attention", + "mirror": { + "source": "exact difference of the original and optimized full-block outputs", + "exact": true, + "bound": null + }, + "history": "docs/swin.md", + "calibration": false, + "notes": "Strict zero-tolerance equality of the two EE block outputs, independently of PyTorch rounding." + }, + "inputs_sha256": "14bec19999c54c6942666ec7804f4ad90e38658995d27683dcee230d513eb982", + "weights_sha256": "163d8d40880d69180a4e378ebf83bd56021d1816e3b41f56d76a3590d2f8782c", + "requests": [ + { + "index": 0, + "grid": { + "dimensions": { + "width": 14, + "height": 14 + }, + "affineTransform": { + "scaleX": 10.0, + "shearX": 0, + "translateX": 0, + "shearY": 0, + "scaleY": -10.0, + "translateY": 0 + }, + "crsCode": "EPSG:3857" + }, + "width": 14, + "height": 14, + "readouts": [ + { + "name": "difference", + "kind": "output", + "channels": 16 + }, + { + "name": "input", + "kind": "input", + "channels": 16 + } + ], + "bands": 34, + "values": 6664, + "payload_bytes": 26656, + "request_bytes": 188488, + "graph_sha256": "cf57e65fa2c9ae45886d61dac0e06a1750ea497b4550bb9cca41e06cba3818f6", + "graph_bytes": 188260, + "embedded_constants": 27204, + "inline_values": 3136 + } + ], + "errors": [], + "seconds": 0.081, + "outputs": { + "difference": { + "mirror_vs_pytorch": { + "tolerance": 0, + "comparison": "bits", + "values": 3136, + "max_abs": 0.0, + "over_tolerance": 0, + "pass": true, + "worst": { + "row": 0, + "col": 0, + "channel": 0, + "actual": 0.0, + "expected": 0.0 + } + } + } + }, + "status": "passed" + } + ] +} diff --git a/docs/swin.md b/docs/swin.md index 881b608..96b818a 100644 --- a/docs/swin.md +++ b/docs/swin.md @@ -12,6 +12,20 @@ window-7 architecture and UPerNet decoder. It is not a generic transformer compiler. A different grid changes execution coordinates; it does not establish trained-model performance on a different sensor or resolution. +The library's `relative_position_bias(..., gather="slices")` is an opt-in for +larger windows, also exposed by `swin_block_constants` and `swin_stage_blocks` +as `relative_bias_gather="slices"`. The slices gather uses much less memory at +windows 12 and 14 and is slower at window 7, so it is for large windows only; +the default remains `"one_hot"`, including in the converter and fixed Swin-T workflow. +The [live fixture run](evidence/live-fixtures/20261103T173919Z-relative_bias_slices.json) +validated two-head bias tables at W7/W12/W14 bit for bit. Its eight requests +also compared dim-16, two-head W7 blocks, unshifted and shifted by 3: both gather +paths matched the mirror bit for bit and their difference was exactly zero; +maximum block error against PyTorch was 1.79e-7. These block parity results remain +in the live record; block-level promotion waits for a fix to the validation +report's handling of tied nearest records. Existing block/stage statuses and +validation records remain unchanged, and the converter keeps the default gather. + ## Supply the model and weight assets The wheel contains code, not weights, personal manifests, prepared images or diff --git a/tensor_ee/blocks/window/attention.py b/tensor_ee/blocks/window/attention.py index 7432991..35d4194 100644 --- a/tensor_ee/blocks/window/attention.py +++ b/tensor_ee/blocks/window/attention.py @@ -32,7 +32,11 @@ (timm get_relative_position_index), and bias[h, t1, t2] = table[index, h]. Both steps run on the server from the table alone: the index from integer sequences, the gather as a one-hot [N*N, (2w-1)^2] matrix times the table. Each one-hot row has a -single 1, so every gathered value is exactly a table entry. +single 1, so every gathered value is exactly a table entry. The opt-in +``gather="slices"`` reverses the coefficient table once, reshapes it to +[2w-1, 2w-1, heads], and concatenates a w x w slice for each row-major query. +Both paths return [w*w, w*w] per head with the same final float32 cast; +Slices is validated live at windows 7, 12 and 14 with two heads. **Shifted-window mask.** timm (SwinTransformerBlock.get_attn_mask) labels the *shifted* token raster with nine regions, rows [0, H-w), [H-w, H-s), [H-s, H) times @@ -177,19 +181,43 @@ def key(values): # varies along the columns return dy.multiply(2 * window - 1).add(dx) -def relative_position_bias(ee, table, window: int = WINDOW, *, policy: str = DOUBLE_ROUND_ONCE) -> list: +def relative_position_bias(ee, table, window: int = WINDOW, *, policy: str = DOUBLE_ROUND_ONCE, + gather: str = "one_hot") -> list: """Per-head float32 [N, N] ee.Array biases from a [(2w-1)^2, heads] table (a NumPy array or a ServerArray). The gather is exact; one float32 rounding. - Windows 5, 7, 8 and 12 are validated.""" + Windows 5, 7, 8 and 12 are validated with the default ``gather="one_hot"``. + + ``gather="slices"`` reverses the small table once and concatenates one w x w + table slice per query. It avoids the [w**4, (2*w-1)**2] one-hot temporary and + moves the same finite coefficient values, preserving the final float32 cast. + Slices is validated live at windows 7, 12 and 14 with two heads. The + default's validated settings and model graphs are unchanged. + """ validate_policy(policy, allowed=DOUBLE_ROUND_ONCE) + if not isinstance(gather, str) or gather not in ("one_hot", "slices"): + reject(f"relative-position gather must be 'one_hot' or 'slices'; received {gather!r}") entries, heads = table.shape if isinstance(table, ServerArray) else np.asarray(table).shape if entries != (2 * window - 1) ** 2: raise ValueError("table rows must be (2w-1)^2") n = window * window - index = relative_position_index(ee, window).reshape([n * n, 1]).repeat(1, entries) - rows = ee.Array(ee.List.sequence(0, entries - 1)).double().reshape([1, entries]).repeat(0, n * n) - one_hot = index.eq(rows) # [N*N, entries] of 0/1 - gathered = one_hot.matrixMultiply(constant_value(ee, table)) # [N*N, heads], exact + if gather == "one_hot": + index = relative_position_index(ee, window).reshape([n * n, 1]).repeat(1, entries) + rows = ee.Array(ee.List.sequence(0, entries - 1)).double().reshape([1, entries]).repeat(0, n * n) + one_hot = index.eq(rows) # [N*N, entries] of 0/1 + gathered = one_hot.matrixMultiply(constant_value(ee, table)) # [N*N, heads], exact + else: + side = 2 * window - 1 + reversed_table = ee.Array(constant_value(ee, table).toList().reverse()).reshape([side, side, heads]) + + def query_rows(query): + query = ee.Number(query) + row = ee.Number(window - 1).subtract(query.divide(window).floor()) + col = ee.Number(window - 1).subtract(query.mod(window)) + # Query (a,b), key (c,d): table[a-c+w-1, b-d+w-1]. + return (reversed_table.slice(0, row, row.add(window)) + .slice(1, col, col.add(window)).reshape([n, heads])) + + gathered = ee.Array.cat(ee.List.sequence(0, n - 1).map(query_rows), 0) return [gathered.slice(1, h, h + 1).reshape([n, n]).float() for h in range(heads)] @@ -358,7 +386,7 @@ def shifted_window_mask(ee, coarse, window: int = WINDOW, shift: int | None = No # --------------------------------------------------------------------------- def relative_bias_values(heads: int, *, window: int = WINDOW) -> int: """Values embedded for a NumPy table: [(2w-1)^2, heads] (the index and the one-hot - gather are built on the server from integer sequences).""" + gather, or the slice bounds, are built on the server from integer sequences).""" return (2 * window - 1) ** 2 * heads @@ -425,11 +453,14 @@ def class_token_attention_arrays() -> int: "relative_position_bias": BlockMetadata( name="relative_position_bias", layout_in="V", layout_out="V", locality=Locality.POINTWISE, policy=DOUBLE_ROUND_ONCE, embedded_constants=relative_bias_values, asset_arrays=relative_bias_arrays, - status="Ready — windows 5, 7, 8, 12", + status="Ready — windows 5, 7, 8, 12; gather=slices: windows 7, 12, 14, heads 2", evidence=("docs/history/building-blocks-2026-10-11.md#36-linear-layers-and-attention", "docs/history/swin_block/notes/findings.md", "docs/evidence/p01-offline.json", - "docs/evidence/live-fixtures/20261013T074301Z-p01a.json", "docs/evidence/live-fixtures/20261013T074330Z-p01b.json", "docs/evidence/live-fixtures/20261013T075542Z-window-relative_position_bias-w12.json"), - validated=Validated(({"window": _WINDOWS},))), + "docs/evidence/live-fixtures/20261013T074301Z-p01a.json", "docs/evidence/live-fixtures/20261013T074330Z-p01b.json", "docs/evidence/live-fixtures/20261013T075542Z-window-relative_position_bias-w12.json", + "docs/evidence/window-relative-bias-slices-offline.json", + "docs/evidence/live-fixtures/20261103T173919Z-relative_bias_slices.json"), + validated=Validated(({"window": _WINDOWS, "gather": ("one_hot",)}, + {"window": (7, 12, 14), "gather": ("slices",), "heads": (2,)}))), "window_attention": BlockMetadata( name="window_attention", layout_in="W", layout_out="W", locality=Locality.WINDOW, policy=COMPOSITE, embedded_constants=window_attention_values, asset_arrays=window_attention_arrays, diff --git a/tensor_ee/blocks/window/swin.py b/tensor_ee/blocks/window/swin.py index 94d4be6..2b0f0d1 100644 --- a/tensor_ee/blocks/window/swin.py +++ b/tensor_ee/blocks/window/swin.py @@ -137,14 +137,19 @@ def _shape(value) -> tuple: return value.shape if isinstance(value, ServerArray) else np.shape(value) -def swin_block_constants(ops, parameters: dict, *, layout=WINDOW_LAYOUT) -> SwinBlockConstants: +def swin_block_constants(ops, parameters: dict, *, layout=WINDOW_LAYOUT, + relative_bias_gather: str = "one_hot") -> SwinBlockConstants: """parameters: timm state_dict names -> float32 NumPy arrays or ServerArrays. ``layout``: the window layout the block runs in (``WINDOW_LAYOUT``, window 7 and 49 rows, by default; ``swin_stage_blocks`` passes the configuration's). The LayerNorm constants are ``RowConstant``s and the biases are repeated to its rows; the relative-position bias uses its window, which the constants record - (``SwinBlockConstants.window``).""" + (``SwinBlockConstants.window``). ``relative_bias_gather="slices"`` opts into + the structured table gather; the default preserves the validated model graphs. + It changes only the relative-position constants, not attention arithmetic. + At block level, slices remains Fixed — offline only; the small live parity + fixtures do not promote the block's validated settings.""" p = parameters dim = _shape(p["norm1.weight"])[0] heads = _shape(p["attn.relative_position_bias_table"])[1] @@ -155,7 +160,7 @@ def swin_block_constants(ops, parameters: dict, *, layout=WINDOW_LAYOUT) -> Swin norm1_bias=row_constant(ops, p["norm1.bias"], layout), qkv=LinearConstants(ops, p["attn.qkv.weight"], p["attn.qkv.bias"], layout), relative_bias=[ops.wrap(b) for b in relative_position_bias( - ops.ee, p["attn.relative_position_bias_table"], layout.window)], + ops.ee, p["attn.relative_position_bias_table"], layout.window, gather=relative_bias_gather)], proj=LinearConstants(ops, p["attn.proj.weight"], p["attn.proj.bias"], layout), norm2_weight=row_constant(ops, p["norm2.weight"], layout), norm2_bias=row_constant(ops, p["norm2.bias"], layout), @@ -266,10 +271,13 @@ def _group_shapes(config: SwinUPerNetConfig, group: str) -> dict: def swin_stage_blocks(ops, parameters: dict, s: int, count: int | None = None, *, - config: SwinUPerNetConfig = SWIN_T224) -> list: + config: SwinUPerNetConfig = SWIN_T224, + relative_bias_gather: str = "one_hot") -> list: """SwinBlockConstants of the first `count` blocks of stage s (``config.depths[s - 1]`` when not given), built for the configuration's window layout. Parameters whose shapes - differ from the configuration's (stage width, heads, window) are refused.""" + differ from the configuration's (stage width, heads, window) are refused. + ``relative_bias_gather`` selects the block constants' gather for every block. + The default converter and fixed adapter continue to use ``"one_hot"``.""" depth = config.depths[s - 1] count = depth if count is None else count if not 0 <= count <= depth: @@ -278,7 +286,8 @@ def swin_stage_blocks(ops, parameters: dict, s: int, count: int | None = None, * for b in range(count): group = f"s{s}b{b}" check_shapes(parameters[group], _group_shapes(config, group), group) - blocks.append(swin_block_constants(ops, parameters[group], layout=config.layout)) + blocks.append(swin_block_constants(ops, parameters[group], layout=config.layout, + relative_bias_gather=relative_bias_gather)) return blocks diff --git a/tests/test_relative_bias_gather.py b/tests/test_relative_bias_gather.py new file mode 100644 index 0000000..7c22e6f --- /dev/null +++ b/tests/test_relative_bias_gather.py @@ -0,0 +1,135 @@ +"""The optional relative-bias gather keeps defaults and reaches whole Swin blocks.""" +import json + +import ee +import numpy as np +import pytest +from ee.apitestcase import ApiTestCase + +from tensor_ee.blocks.window.attention import METADATA, relative_position_bias +from tensor_ee.blocks.window.swin import swin_block_constants, swin_stage_blocks +from tensor_ee.blocks.window.swin_upernet_config import SwinUPerNetConfig +from tensor_ee.converter.request import deep_recursion +from tensor_ee.core.metadata import check_settings +from tensor_ee.core.ops import WindowOps +from tensor_ee.core.tensor import ServerArray +from tensor_ee.weights.constants import WindowLayout +from tools.fixtures.relative_bias import _block_data + + +@pytest.fixture +def offline(): + case = ApiTestCase() + case.setUp() + try: + with deep_recursion(): + yield + finally: + case.tearDown() + + +def _functions(value): + names = set() + def visit(item): + if isinstance(item, dict): + if 'functionName' in item: + names.add(item['functionName']) + for child in item.values(): + visit(child) + elif isinstance(item, list): + for child in item: + visit(child) + visit(json.loads(value.serialize())) + return names + + +def test_default_retains_one_hot_graph_and_opt_in_removes_large_product(offline): + table = np.arange(169 * 2, dtype=np.float32).reshape(169, 2) / 128 + default = relative_position_bias(ee, table) + original = relative_position_bias(ee, table, gather='one_hot') + optimized = relative_position_bias(ee, table, gather='slices') + assert [x.serialize() for x in default] == [x.serialize() for x in original] + assert 'Array.matrixMultiply' in _functions(default[0]) + assert 'Array.matrixMultiply' not in _functions(optimized[0]) + assert {'Array.slice', 'Array.cat', 'List.map'} <= _functions(optimized[0]) + + +@pytest.mark.parametrize('invalid', ['auto', 'list', None, True, 1, ['slices']]) +def test_unknown_gather_is_refused(offline, invalid): + with pytest.raises(ValueError, match='gather'): + relative_position_bias(ee, np.zeros((169, 2), np.float32), gather=invalid) + + +@pytest.mark.parametrize('gather', ['one_hot', 'slices']) +def test_wrong_table_extent_is_refused_for_both_paths(offline, gather): + with pytest.raises(ValueError, match='table rows'): + relative_position_bias(ee, np.zeros((168, 2), np.float32), gather=gather) + + +def test_server_array_transport_remains_lazy_and_does_not_embed_coefficients(offline): + table = ServerArray(ee.Array(ee.FeatureCollection('projects/example/assets/bias').first().get('table')), (169, 2)) + biases = relative_position_bias(ee, table, gather='slices') + assert len(biases) == 2 + assert 'projects/example/assets/bias' in biases[0].serialize() + assert 'Array.matrixMultiply' not in _functions(biases[0]) + + +def test_block_and_stage_builders_propagate_opt_in(offline): + parameters = _block_data(12).weights + ops = WindowOps(ee) + original = swin_block_constants(ops, parameters) + optimized = swin_block_constants(ops, parameters, relative_bias_gather='slices') + assert 'Array.matrixMultiply' in _functions(original.relative_bias[0]) + assert 'Array.matrixMultiply' not in _functions(optimized.relative_bias[0]) + config = SwinUPerNetConfig(embed_dim=16, depths=(1, 1, 1, 1), + stage_dims=(16, 32, 64, 128), num_heads=(2, 4, 8, 16)) + stage = swin_stage_blocks(ops, {'s1b0': parameters}, 1, config=config, relative_bias_gather='slices') + assert stage[0].relative_bias[0].serialize() == optimized.relative_bias[0].serialize() + assert optimized.qkv.weight_t.serialize() == original.qkv.weight_t.serialize() + assert optimized.fc1.weight_t.serialize() == original.fc1.weight_t.serialize() + + +def test_bias_validation_preserves_default_and_limits_slices_to_live_settings(): + metadata = METADATA['relative_position_bias'] + assert check_settings(metadata, {'window': 7, 'gather': 'one_hot'}).inside + for window in (7, 12, 14): + assert check_settings(metadata, {'window': window, 'gather': 'slices', 'heads': 2}).inside + assert not check_settings(metadata, {'window': 5, 'gather': 'slices', 'heads': 2}).inside + assert not check_settings(metadata, {'window': 7, 'gather': 'slices', 'heads': 3}).inside + + +@pytest.mark.parametrize('block', ['swin_block', 'shifted_swin_block', 'swin_stage']) +def test_composite_record_keeps_main_settings_without_a_gather_field(block): + from tensor_ee.blocks.window.swin import METADATA as SWIN_METADATA + metadata = SWIN_METADATA[block] + settings = {key: values[0] for key, values in metadata.validated.configurations[0]} + assert check_settings(metadata, settings).inside + expected_names = { + 'swin_block': {'dim', 'heads', 'window'}, + 'shifted_swin_block': {'dim', 'heads', 'window', 'shift'}, + 'swin_stage': {'dim', 'heads', 'depth', 'window', 'shift', 'shifted'}, + } + assert metadata.validated.names == expected_names[block] + assert metadata.validated.sources == () + # As on main, a setting absent from the record is ignored by the query. + assert check_settings(metadata, {**settings, 'relative_bias_gather': 'slices'}).inside + + +def test_full_block_difference_fixtures_require_exact_equality(): + from tools.fixtures.relative_bias import fixtures + strict = [fixture for fixture in fixtures() if fixture.id.endswith('-exact')] + assert len(strict) == 2 + assert all(fixture.tolerance == 0 and fixture.mirror.exact for fixture in strict) + + +@pytest.mark.parametrize('block', ['swin_block', 'shifted_swin_block']) +def test_live_block_parity_does_not_promote_slice_block_settings(block): + from tensor_ee.blocks.window.swin import METADATA as SWIN_METADATA + metadata = SWIN_METADATA[block] + settings = {'dim': 16, 'heads': 2, 'window': 7, 'relative_bias_gather': 'slices'} + if block == 'shifted_swin_block': + settings['shift'] = 3 + assert not check_settings(metadata, settings).inside + assert 'relative_bias_gather' not in metadata.validated.names + assert metadata.validated.sources == () + assert 'slices' not in metadata.status diff --git a/tests/test_window_blocks.py b/tests/test_window_blocks.py index 3823555..8ca7945 100644 --- a/tests/test_window_blocks.py +++ b/tests/test_window_blocks.py @@ -495,7 +495,8 @@ def test_constants_built_for_one_layout_are_refused_by_another(offline_ee): "batch_norm": (Locality.POINTWISE, None, False, DRO, "Ready", "pointwise"), "naive_attention": (Locality.WINDOW, None, False, REO, "Ready", "window"), "split_heads": (Locality.POINTWISE, None, False, M.NO_ARITHMETIC, "Ready", "per token"), - "relative_position_bias": (Locality.POINTWISE, None, False, DRO, "Ready — windows 5, 7, 8, 12", + "relative_position_bias": (Locality.POINTWISE, None, False, DRO, + "Ready — windows 5, 7, 8, 12; gather=slices: windows 7, 12, 14, heads 2", "— (non-spatial V)"), "window_attention": (Locality.WINDOW, None, False, M.COMPOSITE, "Ready — windows 5, 7, 8, 12", "window"), "shifted_window_mask": (Locality.WINDOW, None, True, DRO, "Ready — windows 5, 7, 8, 12; shift ⌊w/2⌋", diff --git a/tests/test_x01b_swin_upernet_composites.py b/tests/test_x01b_swin_upernet_composites.py index 401da39..b817974 100644 --- a/tests/test_x01b_swin_upernet_composites.py +++ b/tests/test_x01b_swin_upernet_composites.py @@ -170,16 +170,18 @@ def _form(positional, keyword=()): def test_moved_signatures_are_unchanged(): - """The parameters the adapter, the capstone tool and the tests pass keep their names, kinds, - order and defaults (the renamed composites' under their library names).""" + """Original parameters retain names, kinds, order and defaults; the constants + builders additionally expose the trailing relative-bias gather opt-in.""" t224, config = K.SWIN_T224, ("config", K.SWIN_T224) expected = { - SW: {"swin_block_constants": _form(("ops", "parameters"), (("layout", WINDOW_LAYOUT),)), + SW: {"swin_block_constants": _form(("ops", "parameters"), (("layout", WINDOW_LAYOUT), + ("relative_bias_gather", "one_hot"))), "swin_mlp_branch": _form(("ops", "x1", "c")), "swin_block": _form(("ops", "x", "c")), "shifted_swin_block": _form(("ops", "x", "c", "coarse"), (("mask", None), ("shift", None))), "swin_stage": _form(("ops", "x", "blocks", "coarse"), (("shifted", True), ("shift", None))), - "swin_stage_blocks": _form(("ops", "parameters", "s", ("count", None)), (config,)), + "swin_stage_blocks": _form(("ops", "parameters", "s", ("count", None)), + (config, ("relative_bias_gather", "one_hot"))), "swin_merge_constants": _form(("ops", "parameters", "n"), (config,)), "swin_patch_embedding_constants": _form(("ops", "parameters"), (config,)), "swin_patch_embedding": _form(("ops", "x", "grid", "c"), (config,)), diff --git a/tests/test_x02b_validated_settings.py b/tests/test_x02b_validated_settings.py index 4ea091f..de2dcce 100644 --- a/tests/test_x02b_validated_settings.py +++ b/tests/test_x02b_validated_settings.py @@ -477,6 +477,16 @@ def test_the_defaults_are_inside_the_record(name): "window/concat_rows/vit-encoder-reduced", "window/concat_rows/vit-dpt-reduced"} """Whole-model checks whose compatibility block field does not identify block evidence.""" +RELATIVE_BIAS_PARITY_FIXTURES = { + "window/swin_block/bias-slices", + "window/swin_block/bias-slices-exact", + "window/shifted_swin_block/bias-slices", + "window/shifted_swin_block/bias-slices-exact", +} +"""Their live evidence is recorded, but block-level promotion waits for a fix to +validation reports' handling of tied nearest records. Only the lookup is promoted. +""" + def _live_fixtures() -> list[dict]: found = [] @@ -485,7 +495,7 @@ def _live_fixtures() -> list[dict]: if isinstance(data, dict) and data.get("kind") == "live_fixture_run": found += [dict(fixture["spec"], record=str(path.relative_to(ROOT))) for fixture in data["fixtures"] if fixture["status"] == "passed" - and fixture["spec"]["id"] not in COMPOSITION_FIXTURES] + and fixture["spec"]["id"] not in COMPOSITION_FIXTURES | RELATIVE_BIAS_PARITY_FIXTURES] return found @@ -520,6 +530,9 @@ def _fixture_settings(spec: dict) -> dict: if spec["block"] == "conv3x3_window" and settings.get("stride") == 2: # The stride-2 live record stores widths in shapes; retain its exact bytes. settings["cin"], settings["cout"] = shapes["input"][-1], shapes["cout"] + if spec["block"] == "relative_position_bias" and settings.get("gather") == "slices": + # The lookup record restricts heads to two; the live spec stores it in shapes. + settings["heads"] = shapes["heads"] if "bands" in settings: settings["channels"] = len(settings["bands"]) if "tokens" in shapes: diff --git a/tools/fixtures/relative_bias.py b/tools/fixtures/relative_bias.py new file mode 100644 index 0000000..6961fde --- /dev/null +++ b/tools/fixtures/relative_bias.py @@ -0,0 +1,143 @@ +"""Opt-in structured relative-position gather, including full Swin blocks. + +The bias uses the existing timm reference and NumPy mirror. Full-block fixtures +check both the original and optimized constants, with and without shifted masks, +against the same PyTorch model and expose their difference separately. +""" +from functools import partial + +import numpy as np + +from .core import Fixture, FixtureData, MirrorSpec, dyadic +from . import window as lane + + +def _bias_build(ee, data, *, window): + from tensor_ee.blocks.window.attention import relative_position_bias + from .core import Readout + + n = window * window + biases = relative_position_bias(ee, data.weights["table"], window, gather="slices") + readouts = [] + for h, bias in enumerate(biases): + image, grid = lane._value_grid(ee, bias, window, n) + readouts.append(Readout(f"bias_h{h}", image, n, grid)) + return readouts + + +def _block_data(seed): + dim, heads, hidden = 16, 2, 64 + shapes = { + "norm1.weight": (dim,), "norm1.bias": (dim,), + "norm2.weight": (dim,), "norm2.bias": (dim,), + "attn.qkv.weight": (3 * dim, dim), "attn.qkv.bias": (3 * dim,), + "attn.proj.weight": (dim, dim), "attn.proj.bias": (dim,), + "attn.relative_position_bias_table": (169, heads), + "mlp.fc1.weight": (hidden, dim), "mlp.fc1.bias": (hidden,), + "mlp.fc2.weight": (dim, hidden), "mlp.fc2.bias": (dim,), + } + weights = {name: dyadic(seed, name, shape, scale=0.125) for name, shape in shapes.items()} + for name in ("norm1.weight", "norm2.weight"): + weights[name] = dyadic(seed, name, (dim,), scale=0.25, offset=1, bits=16) + return FixtureData(inputs={"x": dyadic(seed, "x", (14, 14, dim))}, weights=weights) + + +def _block_build(ee, data, *, shifted, exact_only=False): + from tensor_ee.core.ops import WindowOps + from tensor_ee.blocks.window.swin import swin_block_constants, swin_block, shifted_swin_block + from tensor_ee.layouts.window import as_row, to_windows + from tensor_ee.core.tensor import band_names + + ops, tokens, dim = WindowOps(ee), lane._tokens(2, 2, 7), 16 + dense = lane._dense(ee, tokens, data.inputs["x"], periodic=True) + x = to_windows(ee, as_row(ee, dense.select(band_names(dim)), dim), tokens) + values = {} + for name, gather in (("original", "one_hot"), ("optimized", "slices")): + c = swin_block_constants(ops, data.weights, relative_bias_gather=gather) + result = (shifted_swin_block(ops, x, c, tokens.coarse(7)) if shifted else swin_block(ops, x, c)) + values[name] = result["out"] + values["difference"] = values["optimized"].subtract(values["original"]) + if exact_only: + values = {"difference": values["difference"]} + return [lane._read(name, ee, tokens, value, dim) for name, value in values.items()] + [lane._echo(dense, tokens, dim)] + + +def _expected(output): + output = np.asarray(output, dtype=np.float32) + return {"original": output, "optimized": output, "difference": np.zeros_like(output)} + + +def _block_reference(data, *, shifted, exact_only=False): + import torch + from timm.models.swin_transformer import SwinTransformerBlock + + with lane._seeded(): + block = SwinTransformerBlock(dim=16, input_resolution=(14, 14), num_heads=2, + window_size=7, shift_size=3 if shifted else 0).eval() + block.attn.fused_attn = False + block.load_state_dict({name: torch.from_numpy(value) for name, value in data.weights.items()}) + with torch.no_grad(): + output = block(torch.from_numpy(data.inputs["x"])[None])[0].numpy() + result = _expected(output) + return {"difference": result["difference"]} if exact_only else result + + +def _block_mirror(data, *, shifted, exact_only=False): + from reference.swin_block.ee_mirror import Mirror + from reference.swin_pair.ee_mirror_shifted import ShiftedMirror + from reference.swin_block.pytorch_swin_block import to_window_layout, from_window_layout + + x = to_window_layout(data.inputs["x"]) + mirror = ShiftedMirror(data.weights, 2, 2) if shifted else Mirror(data.weights) + mirror.sequential_sums = True + result = mirror.shifted_forward(x) if shifted else mirror.forward(x) + expected = _expected(from_window_layout(result["out"], 14, 14)) + return {"difference": expected["difference"]} if exact_only else expected + + +def fixtures(): + from tensor_ee.blocks.window.attention import relative_position_bias, relative_bias_values + from tensor_ee.blocks.window.swin import swin_block_constants, METADATA + from tensor_ee.weights.constants import WindowLayout + + result = [] + for w in (7, 12, 14): + result.append(Fixture( + id=f"window/relative_position_bias/slices-w{w}", lane="window", block="relative_position_bias", + settings={"window": w, "gather": "slices"}, policy="double_round_once", + shapes={"table": [(2 * w - 1) ** 2, 2], "heads": 2, "output": [2, w*w, w*w]}, + seed=20261103, data=partial(lane._bias_data, window=w), build=partial(_bias_build, window=w), + reference=partial(lane._bias_reference, window=w, grid=True), + reference_source="timm WindowAttention._get_rel_pos_bias, fixed dyadic table", + mirror=MirrorSpec(partial(lane._bias_mirror, window=w, grid=True), True, + "reference.swin_block.ee_mirror.Mirror.bias"), + requests=2 if w == 14 else 1, + constants=lambda data, w=w: {"relative_position_bias": relative_bias_values(2, window=w)}, + history="docs/swin.md", accepts=(relative_position_bias,), window=w, + tolerance=0, notes="Every entry of both heads; optional gather, original status unchanged.")) + for shifted in (False, True): + block = "shifted_swin_block" if shifted else "swin_block" + result.append(Fixture( + id=f"window/{block}/bias-slices", lane="window", block=block, + parts=("relative_position_bias", "window_attention", "linear", "layer_norm", "gelu"), + settings={"window": 7, "relative_bias_gather": "slices"}, policy="composite", + shapes={"tokens": [14, 14], "dim": 16, "heads": 2, "window": 7, "shifted": shifted}, + seed=20261103, data=_block_data, build=partial(_block_build, shifted=shifted), + reference=partial(_block_reference, shifted=shifted), + reference_source="timm SwinTransformerBlock, fixed dyadic parameters, non-fused attention", + mirror=MirrorSpec(partial(_block_mirror, shifted=shifted), True, + "reference.swin_block.Mirror / swin_pair.ShiftedMirror, sequential sums"), + requests=1, constants=lambda data, block=block: { + block: 2 * METADATA[block].embedded_constants(16, 2, window=7)}, + history="docs/swin.md", accepts=(swin_block_constants, WindowLayout), window=7, + notes="Both full-block outputs and optimized-minus-original. The difference must be exactly zero.")) + from dataclasses import replace + + result.append(replace( + result[-1], id=f"window/{block}/bias-slices-exact", tolerance=0, + build=partial(_block_build, shifted=shifted, exact_only=True), + reference=partial(_block_reference, shifted=shifted, exact_only=True), + mirror=MirrorSpec(partial(_block_mirror, shifted=shifted, exact_only=True), True, + "exact difference of the original and optimized full-block outputs"), + notes="Strict zero-tolerance equality of the two EE block outputs, independently of PyTorch rounding.")) + return result diff --git a/tools/fixtures/window.py b/tools/fixtures/window.py index 6fea4ae..f6029e3 100644 --- a/tools/fixtures/window.py +++ b/tools/fixtures/window.py @@ -1218,8 +1218,10 @@ def _crop() -> list[Fixture]: def fixtures() -> list[Fixture]: + from . import relative_bias + return (_calibration() + [_p01_single(WINDOW)] + [f for w in P01_WINDOWS for f in _p01_window(w)] - + _crop()) + + _crop() + relative_bias.fixtures()) def _p01a() -> tuple[str, ...]: @@ -1237,6 +1239,7 @@ def _p01() -> tuple[str, ...]: SELECTIONS = {"p01": _p01, "p01a": _p01a, "p01b": _p01b, - "conv3x3_crop": lambda: tuple(f.id for f in _crop())} + "conv3x3_crop": lambda: tuple(f.id for f in _crop()), + "relative_bias_slices": lambda: tuple(f.id for f in fixtures() if "/slices-w" in f.id or "/bias-slices" in f.id)} """Named selections of this lane (``tools.fixtures.select``): name -> a callable returning the fixture ids. A run sends its fixtures in registry order."""