summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/CNN_V2_DEBUG_TOOLS.md126
-rw-r--r--tools/cnn_v2_test/index.html59
-rwxr-xr-xtraining/gen_identity_weights.py134
3 files changed, 319 insertions, 0 deletions
diff --git a/doc/CNN_V2_DEBUG_TOOLS.md b/doc/CNN_V2_DEBUG_TOOLS.md
new file mode 100644
index 0000000..0185dac
--- /dev/null
+++ b/doc/CNN_V2_DEBUG_TOOLS.md
@@ -0,0 +1,126 @@
+# CNN v2 Debugging Tools
+
+Tools for investigating CNN v2 mismatch between HTML tool and cnn_test.
+
+---
+
+## Identity Weight Generator
+
+**Purpose:** Generate trivial .bin files with identity passthrough for debugging.
+
+**Script:** `training/gen_identity_weights.py`
+
+**Usage:**
+```bash
+# 1×1 identity (default)
+./training/gen_identity_weights.py workspaces/main/weights/cnn_v2_identity.bin
+
+# 3×3 identity
+./training/gen_identity_weights.py workspaces/main/weights/cnn_v2_identity_3x3.bin --kernel-size 3
+
+# Custom mip level
+./training/gen_identity_weights.py output.bin --kernel-size 1 --mip-level 2
+```
+
+**Output:**
+- Single layer, 12D→4D (4 input channels + 8 static features)
+- Identity matrix: Output Ch{0,1,2,3} = Input Ch{0,1,2,3}
+- Static features (Ch 4-11) are zeroed
+- Minimal file size (~136 bytes for 1×1, ~904 bytes for 3×3)
+
+**Validation:**
+Load in HTML tool or cnn_test - output should match input (RGB only, ignoring static features).
+
+---
+
+## Composited Layer Visualization
+
+**Purpose:** Save current layer view as single composited image (4 channels side-by-side, grayscale).
+
+**Location:** HTML tool - "Layer Visualization" panel
+
+**Usage:**
+1. Load image + weights in HTML tool
+2. Select layer to visualize (Static 0-3, Static 4-7, Layer 0, Layer 1, etc.)
+3. Click "Save Composited" button
+4. Downloads PNG: `composited_layer{N}_{W}x{H}.png`
+
+**Output:**
+- 4 channels stacked horizontally
+- Grayscale representation
+- Useful for comparing layer activations across tools
+
+---
+
+## Debugging Strategy
+
+### Track a) Binary Conversion Chain
+
+**Hypothesis:** Conversion error in .bin ↔ base64 ↔ Float32Array
+
+**Test:**
+1. Generate identity weights:
+ ```bash
+ ./training/gen_identity_weights.py workspaces/main/weights/test_identity.bin
+ ```
+
+2. Load in HTML tool - output should match input RGB
+
+3. If mismatch:
+ - Check Python export: f16 packing in `export_cnn_v2_weights.py` line 105
+ - Check HTML parsing: `unpackF16()` in `index.html` line 805-815
+ - Check weight indexing: `get_weight()` shader function
+
+**Key locations:**
+- Python: `np.float16` → `view(np.uint32)` (line 105 of export script)
+- JS: `DataView` → `unpackF16()` → manual f16 decode (line 773-803)
+- WGSL: `unpack2x16float()` built-in (line 492 of shader)
+
+### Track b) Layer Visualization
+
+**Purpose:** Confirm layer outputs match between HTML and C++
+
+**Method:**
+1. Run identical input through both tools
+2. Save composited layers from HTML tool
+3. Compare with cnn_test output
+4. Use identity weights to isolate weight loading from computation
+
+### Track c) Trivial Test Case
+
+**Use identity weights to test:**
+- Weight loading (binary parsing)
+- Feature generation (static features)
+- Convolution (should be passthrough)
+- Output packing
+
+**Expected behavior:**
+- Input RGB → Output RGB (exact match)
+- Static features ignored (all zeros in identity matrix)
+
+---
+
+## Known Issues
+
+**Current mismatch:** HTML tool and cnn_test produce different outputs for same input/weights.
+
+**Suspects:**
+1. F16 unpacking difference (CPU vs GPU vs JS)
+2. Static feature generation (RGBD, UV, sin encoding)
+3. Convolution kernel iteration order
+4. Output packing/unpacking
+
+**Next steps:**
+1. Test with identity weights (eliminates weight loading)
+2. Compare composited layer outputs
+3. Add debug visualization for static features
+4. Hex dump comparison (first 8 pixels) - use `--debug-hex` flag in cnn_test
+
+---
+
+## Related Documentation
+
+- `doc/CNN_V2.md` - CNN v2 architecture
+- `doc/CNN_V2_WEB_TOOL.md` - HTML tool documentation
+- `doc/CNN_TEST_TOOL.md` - cnn_test CLI tool
+- `training/export_cnn_v2_weights.py` - Binary export format
diff --git a/tools/cnn_v2_test/index.html b/tools/cnn_v2_test/index.html
index ca89fb4..3977d9f 100644
--- a/tools/cnn_v2_test/index.html
+++ b/tools/cnn_v2_test/index.html
@@ -1395,6 +1395,7 @@ class CNNTester {
const label = `Layer ${i - 1}`;
html += `<button onclick="tester.visualizeLayer(${i})" id="layerBtn${i}">${label}</button>`;
}
+ html += `<button onclick="tester.saveCompositedLayer()" style="margin-left: 20px; background: #28a745;">Save Composited</button>`;
html += '</div>';
html += '<div class="layer-grid" id="layerGrid"></div>';
@@ -1836,6 +1837,64 @@ class CNNTester {
this.setStatus(`Save failed: ${err.message}`, true);
}
}
+
+ async saveCompositedLayer() {
+ if (!this.currentLayerIdx) {
+ this.log('No layer selected for compositing', 'error');
+ return;
+ }
+
+ try {
+ const canvases = [];
+ for (let i = 0; i < 4; i++) {
+ const canvas = document.getElementById(`layerCanvas${i}`);
+ if (!canvas) {
+ this.log(`Canvas layerCanvas${i} not found`, 'error');
+ return;
+ }
+ canvases.push(canvas);
+ }
+
+ const width = canvases[0].width;
+ const height = canvases[0].height;
+ const compositedWidth = width * 4;
+
+ // Create composited canvas
+ const compositedCanvas = document.createElement('canvas');
+ compositedCanvas.width = compositedWidth;
+ compositedCanvas.height = height;
+ const ctx = compositedCanvas.getContext('2d');
+
+ // Composite horizontally
+ for (let i = 0; i < 4; i++) {
+ ctx.drawImage(canvases[i], i * width, 0);
+ }
+
+ // Convert to grayscale
+ const imageData = ctx.getImageData(0, 0, compositedWidth, height);
+ const pixels = imageData.data;
+ for (let i = 0; i < pixels.length; i += 4) {
+ const gray = 0.299 * pixels[i] + 0.587 * pixels[i + 1] + 0.114 * pixels[i + 2];
+ pixels[i] = pixels[i + 1] = pixels[i + 2] = gray;
+ }
+ ctx.putImageData(imageData, 0, 0);
+
+ // Save as PNG
+ const blob = await new Promise(resolve => compositedCanvas.toBlob(resolve, 'image/png'));
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `composited_layer${this.currentLayerIdx}_${compositedWidth}x${height}.png`;
+ a.click();
+ URL.revokeObjectURL(url);
+
+ this.log(`Saved composited layer: ${a.download}`);
+ this.setStatus(`Saved: ${a.download}`);
+ } catch (err) {
+ this.log(`Failed to save composited layer: ${err.message}`, 'error');
+ this.setStatus(`Compositing failed: ${err.message}`, true);
+ }
+ }
}
const tester = new CNNTester();
diff --git a/training/gen_identity_weights.py b/training/gen_identity_weights.py
new file mode 100755
index 0000000..a84ea87
--- /dev/null
+++ b/training/gen_identity_weights.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+"""Generate Identity CNN v2 Weights
+
+Creates trivial .bin with 1 layer, 1×1 kernel, identity passthrough.
+Output Ch{0,1,2,3} = Input Ch{0,1,2,3} (ignores static features).
+
+Usage:
+ ./training/gen_identity_weights.py [output.bin]
+"""
+
+import argparse
+import numpy as np
+import struct
+from pathlib import Path
+
+
+def generate_identity_weights(output_path, kernel_size=1, mip_level=0):
+ """Generate identity weights: output = input (ignores static features).
+
+ Binary format:
+ Header (20 bytes):
+ uint32 magic ('CNN2')
+ uint32 version (2)
+ uint32 num_layers (1)
+ uint32 total_weights (f16 count)
+ uint32 mip_level
+
+ LayerInfo (20 bytes):
+ uint32 kernel_size
+ uint32 in_channels (12)
+ uint32 out_channels (4)
+ uint32 weight_offset (0)
+ uint32 weight_count
+
+ Weights (u32 packed f16):
+ Identity matrix for first 4 input channels
+ Zeros for static features (channels 4-11)
+ """
+ # Identity: 4 output channels, 12 input channels
+ # Weight shape: [out_ch, in_ch, kernel_h, kernel_w]
+ in_channels = 12 # 4 input + 8 static
+ out_channels = 4
+
+ # Identity matrix: diagonal 1.0 for first 4 channels, 0.0 for rest
+ weights = np.zeros((out_channels, in_channels, kernel_size, kernel_size), dtype=np.float32)
+
+ # Center position for kernel
+ center = kernel_size // 2
+
+ # Set diagonal to 1.0 (output ch i = input ch i)
+ for i in range(out_channels):
+ weights[i, i, center, center] = 1.0
+
+ # Flatten
+ weights_flat = weights.flatten()
+ weight_count = len(weights_flat)
+
+ print(f"Generating identity weights:")
+ print(f" Kernel size: {kernel_size}×{kernel_size}")
+ print(f" Channels: 12D→4D")
+ print(f" Weights: {weight_count}")
+ print(f" Mip level: {mip_level}")
+
+ # Convert to f16
+ weights_f16 = np.array(weights_flat, dtype=np.float16)
+
+ # Pad to even count
+ if len(weights_f16) % 2 == 1:
+ weights_f16 = np.append(weights_f16, np.float16(0.0))
+
+ # Pack f16 pairs into u32
+ weights_u32 = weights_f16.view(np.uint32)
+
+ print(f" Packed: {len(weights_u32)} u32")
+ print(f" Binary size: {20 + 20 + len(weights_u32) * 4} bytes")
+
+ # Write binary
+ output_path = Path(output_path)
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+
+ with open(output_path, 'wb') as f:
+ # Header (20 bytes)
+ f.write(struct.pack('<4sIIII',
+ b'CNN2', # magic
+ 2, # version
+ 1, # num_layers
+ len(weights_f16), # total_weights
+ mip_level)) # mip_level
+
+ # Layer info (20 bytes)
+ f.write(struct.pack('<IIIII',
+ kernel_size, # kernel_size
+ in_channels, # in_channels
+ out_channels, # out_channels
+ 0, # weight_offset
+ weight_count)) # weight_count
+
+ # Weights (u32 packed f16)
+ f.write(weights_u32.tobytes())
+
+ print(f" → {output_path}")
+
+ # Verify
+ print("\nVerification:")
+ with open(output_path, 'rb') as f:
+ data = f.read()
+ magic, version, num_layers, total_weights, mip = struct.unpack('<4sIIII', data[:20])
+ print(f" Magic: {magic}")
+ print(f" Version: {version}")
+ print(f" Layers: {num_layers}")
+ print(f" Total weights: {total_weights}")
+ print(f" Mip level: {mip}")
+ print(f" File size: {len(data)} bytes")
+
+
+def main():
+ parser = argparse.ArgumentParser(description='Generate identity CNN v2 weights')
+ parser.add_argument('output', type=str, nargs='?',
+ default='workspaces/main/weights/cnn_v2_identity.bin',
+ help='Output .bin file path')
+ parser.add_argument('--kernel-size', type=int, default=1,
+ help='Kernel size (default: 1×1)')
+ parser.add_argument('--mip-level', type=int, default=0,
+ help='Mip level for p0-p3 features (default: 0)')
+
+ args = parser.parse_args()
+
+ print("=== Identity Weight Generator ===\n")
+ generate_identity_weights(args.output, args.kernel_size, args.mip_level)
+ print("\nDone!")
+
+
+if __name__ == '__main__':
+ main()