Add a tested PyTorch shape lesson

#1
Files changed (12) hide show
  1. .gitattributes +1 -0
  2. DESIGN.md +39 -0
  3. LICENSE +21 -0
  4. README.md +85 -6
  5. VALIDATION.md +68 -0
  6. app.mjs +82 -0
  7. index.html +102 -17
  8. logo.png +3 -0
  9. shape-math.mjs +43 -0
  10. shape-math.test.mjs +45 -0
  11. styles.css +108 -0
  12. verify_pytorch.py +90 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ logo.png filter=lfs diff=lfs merge=lfs -text
DESIGN.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Design and acceptance notes
2
+
3
+ This is an interactive educational page, not an installation landing page.
4
+ The design uses the existing tensorViz white background, dark green mark and
5
+ system fonts. The canonical supplied logo is copied unchanged and never redrawn.
6
+ The [Hugging Face configuration reference](https://huggingface.co/docs/hub/spaces-config-reference)
7
+ supports a `thumbnail` URL for social sharing. The README points that field at
8
+ the immutable public copy of the supplied logo; the lesson loads its local copy.
9
+
10
+ ## Surface and controls
11
+
12
+ - Header: supplied logo, `tensorViz / shape lab`, runnable-example link.
13
+ - Introduction: one problem statement and an explicit arithmetic-only boundary.
14
+ - Main lesson: batch/height/width inputs, three spatial presets and three
15
+ classifier choices alongside an ordered shape trace. Mismatch/success uses
16
+ both text and restrained color. Linear weight/transposed dimensions are explicit.
17
+ - Reshape counterexample: compare original feature-map rows with the inferred
18
+ first dimension. Distinguish changed batch, impossible reshape and redundant
19
+ reshape when the width already matches.
20
+ - Conclusion: preserve batch, declare the intended input contract, and separate
21
+ shape compatibility from numerical correctness. Optional public source and
22
+ recorded-model links are available without an embedded remote page.
23
+
24
+ Main content is at most 1,040 pixels wide inside a 1,136-pixel outer container.
25
+ Below 760 pixels, controls precede results. Below 390 pixels, shape labels and
26
+ values stack. Interactive targets have native labels and keyboard focus styles;
27
+ validation/results use status announcements. No animation or external fonts.
28
+
29
+ ## Rendered acceptance
30
+
31
+ The design follows the existing brand directly; no generated concept is used.
32
+ Automated arithmetic checks do not establish rendered acceptance.
33
+
34
+ Check the actual page on desktop and at 320-pixel width before publication:
35
+ logo proportions, legible controls/shapes, no horizontal
36
+ overflow, all presets/modes/reset, empty and invalid fields, keyboard focus,
37
+ long error wrapping and the optional links. The exact example must begin with
38
+ `(2x512 and 128x5)`, and the unsafe reshape must show two images becoming eight
39
+ rows. Record observed results separately from arithmetic tests.
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tensorViz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,12 +1,91 @@
1
  ---
2
- title: Pytorch Shape Lab
3
- emoji: 🐠
4
- colorFrom: blue
5
- colorTo: purple
6
  sdk: static
 
7
  pinned: false
8
  license: mit
9
- short_description: Follow PyTorch shapes and fix a flatten-to-Linear mismatch.
 
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: PyTorch Shape Lab
3
+ emoji: 🧮
4
+ colorFrom: green
5
+ colorTo: gray
6
  sdk: static
7
+ app_file: index.html
8
  pinned: false
9
  license: mit
10
+ short_description: Follow a CNN's shapes without losing the batch dimension.
11
+ thumbnail: https://raw.githubusercontent.com/tensorviz-org/tensorviz-examples/6e5cdcfc8ec3ab39c0bb1264bf534210ae5c1981/assets/tensorviz-logo-white.png
12
+ tags:
13
+ - pytorch
14
+ - education
15
+ - tensor-shapes
16
  ---
17
 
18
+ # PyTorch shape lab
19
+
20
+ A small interactive lesson by **tensorViz**. Change the batch and image size,
21
+ follow a fixed convolution/pooling stack, and find a mismatch at `Linear`.
22
+ Compare a correctly sized fixed head with an explicit architecture change using
23
+ adaptive average pooling. See how `reshape(-1, 128)` can silently change the
24
+ number of samples.
25
+
26
+ **This is shape arithmetic, not model execution.** The browser runs no Python,
27
+ inference, training or tensorViz backend. Compatible shapes do not establish
28
+ prediction quality or numerical equivalence. The lesson code has no tracking,
29
+ third-party script, iframe, runtime fetch or account requirement. External
30
+ resources load only when the reader follows a link. This describes the lesson
31
+ code, not the hosting platform's ordinary request handling or platform analytics.
32
+
33
+ The original executable example is
34
+ [available with its checks and explanation](https://github.com/tensorviz-org/tensorviz-examples/blob/1baecebf25ff1067a9112bde382c0a398fb99747/examples/shape-mismatch/README.md).
35
+ The static arithmetic was independently checked against actual CPU PyTorch
36
+ operations; see [validation](VALIDATION.md).
37
+
38
+ ## Run locally
39
+
40
+ No package installation or build is needed. Serve this folder using an existing
41
+ Python installation:
42
+
43
+ ```sh
44
+ python3 -m http.server 4177 --bind 127.0.0.1
45
+ ```
46
+
47
+ Open `http://127.0.0.1:4177`. ES modules need an HTTP server; opening `index.html`
48
+ as a local `file:` URL is not the supported preview path.
49
+
50
+ ## Lesson boundaries
51
+
52
+ - The fixed feature stack is `Conv2d(3, 8, kernel_size=3, padding=1)`, `ReLU`,
53
+ `MaxPool2d(2)`, then `Flatten(start_dim=1)`.
54
+ - Inputs are NCHW with three channels. This teaching UI accepts batch 1–64 and
55
+ integer height/width 2–256. These UI bounds are not a general PyTorch limit.
56
+ - Pooling uses the default stride of two, padding zero and `ceil_mode=False`.
57
+ Odd spatial dimensions round down.
58
+ - The original head expects 128 features. A 16 × 16 image yields 512. A fixed
59
+ 512-feature head fits that example, but not every other input size.
60
+ - The optional adaptive pool reduces each of the eight spatial channels to one
61
+ value and uses an eight-feature head. That changes computation and parameters;
62
+ it is not an interchangeable pretrained checkpoint repair.
63
+ - The unsafe reshape demonstration uses the original feature map before any
64
+ adaptive pool and the original 128-feature head.
65
+
66
+ For real model exploration, [tensorViz's recorded browser examples](https://tensorviz.dev/explore/)
67
+ are an optional next step. Capturing your own PyTorch model requires the separate
68
+ tensorViz extension and your model environment. This repository contains only
69
+ the lesson, not extension implementation.
70
+
71
+ ## Validate changes
72
+
73
+ Use existing Node.js and a Python environment with PyTorch installed. Nothing in
74
+ the checks downloads dependencies or enables telemetry.
75
+
76
+ ```sh
77
+ node --test shape-math.test.mjs
78
+ python verify_pytorch.py
79
+ ```
80
+
81
+ `verify_pytorch.py` also invokes Node.js to compare the JavaScript results with
82
+ PyTorch. Visual and accessibility acceptance remain separate from these checks.
83
+ The intended layout and review cases are in [design notes](DESIGN.md).
84
+
85
+ ## License and attribution
86
+
87
+ Original lesson code and documentation are [MIT licensed](LICENSE), including
88
+ the small test model derived from the public tensorViz example linked above.
89
+ PyTorch is a separate dependency under its own license. The supplied tensorViz
90
+ logo identifies the project; this license does not grant trademark rights.
91
+ The extension and its dependencies retain their own licenses.
VALIDATION.md ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Validation
2
+
3
+ This page models a fixed CPU-compatible PyTorch example. It does not execute
4
+ Python in the browser or provide a general-purpose shape inference engine.
5
+
6
+ The JavaScript tests cover the canonical mismatch, batch-preserving fixed head,
7
+ non-square inputs, floor pooling for odd dimensions, adaptive-pooling dimensions,
8
+ unsafe reshape, and invalid input rejection. The separate Python check invokes
9
+ actual PyTorch Conv2d, ReLU, MaxPool2d, Flatten, Linear and AdaptiveAvgPool2d,
10
+ then compares their shapes and errors with the JavaScript calculator for 21 cases.
11
+ For successful paths it also compares full-batch and per-image outputs.
12
+
13
+ ## Executed checks — October 11, 2026
14
+
15
+ Environment: Node.js **26.5.0**, Python **3.12.14**, PyTorch **2.7.1**, CPU.
16
+ No dependencies were downloaded or installed for this check.
17
+
18
+ - **5 JavaScript tests passed**, zero failures and zero skips.
19
+ - **21 actual PyTorch comparisons passed**: all three heads for batch/image
20
+ combinations `(1,16,16)`, `(2,16,16)`, `(5,16,16)`, `(2,16,20)`,
21
+ `(3,17,19)`, `(2,8,8)` and `(1,3,3)`.
22
+ - Successful paths preserved the number of samples, and full-batch outputs
23
+ matched concatenated per-image outputs (`atol=1e-6`, `rtol=1e-5`).
24
+ - JavaScript syntax checks passed for both application modules.
25
+
26
+ Representative observations:
27
+
28
+ | Input NCHW | Flatten before optional adaptive pool | Original 128 head | Fixed 512 head | Adaptive pool + 8 head |
29
+ | --- | --- | --- | --- | --- |
30
+ | `[2,3,16,16]` | `[2,512]` | Exact error `(2x512 and 128x5)` | `[2,5]` | `[2,5]` |
31
+ | `[2,3,16,20]` | `[2,640]` | Mismatch | Exact error `(2x640 and 512x5)` | `[2,5]` |
32
+ | `[3,3,17,19]` | `[3,576]` | Mismatch | Mismatch | `[3,5]` |
33
+ | `[2,3,8,8]` | `[2,128]` | `[2,5]` | Mismatch | `[2,5]` |
34
+ | `[1,3,3,3]` | `[1,8]` | Mismatch | Mismatch | `[1,5]` |
35
+
36
+ For the default input, `reshape(-1,128)` changes `[2,512]` to `[8,128]` and
37
+ produces `[8,5]` through the original head. The operation runs but does not
38
+ preserve one output row per image. Odd-sized pooling and impossible/redundant
39
+ reshape cases were also checked against PyTorch.
40
+
41
+ Browser rendering and interaction acceptance are not established by these tests.
42
+ No model accuracy, checkpoint compatibility or extension capture result is
43
+ claimed by this standalone lesson.
44
+
45
+ ## Browser QA — October 11, 2026
46
+
47
+ A separate in-app browser pass checked the locally served lesson at **1280 × 720**
48
+ and **390 × 844**. It verified these interactions:
49
+
50
+ - The default input displays `512 ≠ 128` at the original classifier.
51
+ - Choosing the 512-feature head produces the calculated output `[2,5]`.
52
+ - Changing the image to 16 × 20 produces `640 ≠ 512` with that fixed head.
53
+ - Adding adaptive pooling produces `[2,5]` and shows the explicit warning that
54
+ the architecture and computation change.
55
+ - A 17 × 19 image pools to spatial size 8 × 9.
56
+ - Batch zero shows the validation message; Reset restores the starting lesson.
57
+
58
+ The 390-pixel-wide page had readable text and no horizontal overflow. A full-page
59
+ mobile screenshot was saved as `tensorviz-shape-lab-mobile-2026-10-11.png`.
60
+ No lesson-origin console errors were observed. These results cover the two
61
+ listed viewports; 320-pixel rendering and hosted Hugging Face behavior were not
62
+ established by this local pass.
63
+
64
+ A source review also confirmed the visible arithmetic-only notice, batch-preserving
65
+ flatten, explicit adaptive-pooling semantics, unsafe-reshape sample-count warning,
66
+ and distinction between this lesson and actual tensorViz model capture. The code
67
+ adds no runtime fetch or tracking, and the footer limits its claim to calculations
68
+ staying in the reader's browser.
app.mjs ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { inspectShapes } from './shape-math.mjs';
2
+
3
+ const byId = (id) => document.getElementById(id);
4
+ const shape = (dimensions) => `[${dimensions.join(', ')}]`;
5
+ const text = (id, value) => { byId(id).textContent = value; };
6
+ const form = byId('shape-form');
7
+
8
+ function render() {
9
+ const result = inspectShapes({
10
+ batch: byId('batch').valueAsNumber,
11
+ height: byId('height').valueAsNumber,
12
+ width: byId('width').valueAsNumber,
13
+ mode: form.elements.mode.value,
14
+ });
15
+ byId('invalid').hidden = result.valid;
16
+ byId('calculation').hidden = !result.valid;
17
+ byId('reshape-result').hidden = !result.valid;
18
+ if (!result.valid) { text('invalid', result.message); return; }
19
+
20
+ const r = result;
21
+ text('shape-input', shape(r.input));
22
+ text('shape-conv', shape(r.conv));
23
+ text('shape-pool', shape(r.pool));
24
+ text('shape-flat', shape(r.flattened));
25
+ byId('adaptive-row').hidden = !r.adaptive;
26
+ if (r.adaptive) text('shape-adaptive', shape(r.adaptive));
27
+ const linearName = byId('linear-name');
28
+ linearName.replaceChildren(document.createTextNode(`Linear(${r.inFeatures}, 5)`));
29
+ const small = document.createElement('small');
30
+ small.textContent = `Expects ${r.inFeatures} features per image`;
31
+ linearName.append(small);
32
+ text('shape-output', r.output ? shape(r.output) : `${r.features} ≠ ${r.inFeatures}`);
33
+ byId('linear-row').className = r.matches ? 'match' : 'mismatch';
34
+ byId('verdict').className = `verdict ${r.matches ? 'match' : 'mismatch'}`;
35
+ text('verdict-title', r.matches ? `The widths match. Batch stays ${r.batch}.` : `${r.features} features arrived. The head expects ${r.inFeatures}.`);
36
+ text('feature-math', r.adaptive
37
+ ? '8 channels × 1 height × 1 width = 8 features per image after adaptive pooling.'
38
+ : `8 channels × ${r.pool[2]} height × ${r.pool[3]} width = ${r.features} features per image.`);
39
+ text('matrix-left', shape(r.flattened));
40
+ text('matrix-right', shape([r.inFeatures, 5]));
41
+ text('matrix-answer', r.output ? `= ${shape(r.output)}` : 'incompatible');
42
+ text('weight-shape', shape(r.weight));
43
+ byId('error-message').hidden = r.matches;
44
+ text('error-message', r.error ?? '');
45
+ const notes = {
46
+ broken: r.matches
47
+ ? 'This input produces exactly 128 features per image. Try 16 × 16: the same fixed head will no longer fit.'
48
+ : 'The fixed head expects 128 features per image. Match the head to the intended input contract; changing the batch does not change this width.',
49
+ fixed: r.matches
50
+ ? '512 fits the 16 × 16 example. Try a wider image: changing the input can break a fixed-width head again.'
51
+ : '512 is fixed for the original 16 × 16 example. This input has a different feature width. A successful earlier capture does not cover every input.',
52
+ pooled: 'Adaptive pooling averages each spatial channel to one value, so this head receives 8 features. This changes the architecture and the classifier weights; it is not the same computation as the fixed-width model.',
53
+ };
54
+ text('mode-note', notes[r.mode]);
55
+ text('reshape-before', shape([r.batch, r.originalFeatures]));
56
+ const unsafe = r.unsafeReshape;
57
+ text('reshape-after', unsafe.possible ? shape(unsafe.shape) : 'cannot reshape');
58
+ const warning = byId('reshape-warning');
59
+ warning.replaceChildren();
60
+ const strong = document.createElement('strong');
61
+ strong.textContent = !unsafe.possible
62
+ ? `${unsafe.elements} elements cannot fill rows of 128.`
63
+ : unsafe.preservesBatch
64
+ ? `The feature width was already 128.`
65
+ : `${r.batch} ${r.batch === 1 ? 'image became' : 'images became'} ${unsafe.shape[0]} ${unsafe.shape[0] === 1 ? 'row' : 'rows'}.`;
66
+ warning.append(strong, document.createTextNode(!unsafe.possible
67
+ ? ' This reshape would fail before Linear. Use a batch-preserving flatten and inspect the feature width.'
68
+ : unsafe.preservesBatch
69
+ ? ' This reshape is redundant here. It does not repair a mismatch or make other image sizes safe.'
70
+ : ` Linear can now return ${shape(unsafe.output)}, but that is not one prediction per image.`));
71
+ }
72
+
73
+ form.addEventListener('input', render);
74
+ form.addEventListener('change', render);
75
+ form.addEventListener('submit', (event) => event.preventDefault());
76
+ document.querySelectorAll('[data-height]').forEach((button) => button.addEventListener('click', () => {
77
+ byId('height').value = button.dataset.height;
78
+ byId('width').value = button.dataset.width;
79
+ render();
80
+ }));
81
+ byId('reset').addEventListener('click', () => { form.reset(); render(); });
82
+ render();
index.html CHANGED
@@ -1,19 +1,104 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="description" content="An interactive PyTorch shape-arithmetic lesson: follow a small CNN, find a Linear mismatch, and see why a reshape can change your batch.">
7
+ <meta name="theme-color" content="#ffffff">
8
+ <title>PyTorch shape lab · tensorViz</title>
9
+ <link rel="icon" href="logo.png">
10
+ <link rel="stylesheet" href="styles.css">
11
+ </head>
12
+ <body>
13
+ <a class="skip-link" href="#lab">Skip to the lesson</a>
14
+ <main>
15
+ <header class="brand">
16
+ <img src="logo.png" width="34" height="34" alt="tensorViz logo">
17
+ <span>tensorViz <span class="brand-separator">/</span> <span class="brand-label">shape lab</span></span>
18
+ <a href="https://github.com/tensorviz-org/tensorviz-examples/blob/1baecebf25ff1067a9112bde382c0a398fb99747/examples/shape-mismatch/README.md">Runnable example</a>
19
+ </header>
20
+
21
+ <section class="intro" aria-labelledby="page-title">
22
+ <h1 id="page-title">Your batch is fine.<br>Check the features.</h1>
23
+ <p>A small image model reaches <code>Linear</code> with the wrong width.
24
+ Change the input, follow the shapes, and find the contract that broke.</p>
25
+ <p class="boundary">Shape arithmetic only. This page does not run Python, a model or inference.</p>
26
+ </section>
27
+
28
+ <section id="lab" class="lab" aria-labelledby="lab-title">
29
+ <div class="controls">
30
+ <h2 id="lab-title">Try an input</h2>
31
+ <p class="section-note">NCHW · 3 input channels</p>
32
+ <form id="shape-form" novalidate>
33
+ <div class="dimensions">
34
+ <label for="batch">Batch <input id="batch" name="batch" type="number" min="1" max="64" step="1" value="2" inputmode="numeric" aria-describedby="input-bounds"></label>
35
+ <label for="height">Height <input id="height" name="height" type="number" min="2" max="256" step="1" value="16" inputmode="numeric" aria-describedby="input-bounds"></label>
36
+ <label for="width">Width <input id="width" name="width" type="number" min="2" max="256" step="1" value="16" inputmode="numeric" aria-describedby="input-bounds"></label>
37
+ </div>
38
+ <p id="input-bounds" class="field-help">Batch 1–64. Height and width 2–256.</p>
39
+ <div class="presets" aria-label="Example image sizes">
40
+ <button type="button" data-height="16" data-width="16">16 × 16</button>
41
+ <button type="button" data-height="16" data-width="20">16 × 20</button>
42
+ <button type="button" data-height="17" data-width="19">17 × 19</button>
43
+ </div>
44
+ <fieldset>
45
+ <legend>Choose the classifier</legend>
46
+ <label class="mode"><input type="radio" name="mode" value="broken" checked><span>Original head<strong>Linear(128, 5)</strong></span></label>
47
+ <label class="mode"><input type="radio" name="mode" value="fixed"><span>Match the 16 × 16 input<strong>Linear(512, 5)</strong></span></label>
48
+ <label class="mode"><input type="radio" name="mode" value="pooled"><span>Add adaptive average pooling<strong>Pool to 1 × 1 → Linear(8, 5)</strong></span></label>
49
+ </fieldset>
50
+ </form>
51
+ <button id="reset" type="button" class="reset">Reset lesson</button>
52
+ </div>
53
+
54
+ <div class="results" aria-label="Calculated tensor shapes">
55
+ <div id="invalid" class="invalid" role="status" hidden></div>
56
+ <div id="calculation">
57
+ <ol class="flow" aria-label="Shape at each layer">
58
+ <li><div><span class="step">01</span><span>Input</span></div><output id="shape-input">[2, 3, 16, 16]</output></li>
59
+ <li><div><span class="step">02</span><span>Conv2d + ReLU<small>3 → 8 channels · kernel 3 · padding 1</small></span></div><output id="shape-conv">[2, 8, 16, 16]</output></li>
60
+ <li><div><span class="step">03</span><span>MaxPool2d(2)<small>Spatial sizes round down</small></span></div><output id="shape-pool">[2, 8, 8, 8]</output></li>
61
+ <li id="adaptive-row" hidden><div><span class="step">+</span><span>AdaptiveAvgPool2d((1, 1))<small>New architecture: average each channel</small></span></div><output id="shape-adaptive"></output></li>
62
+ <li><div><span class="step">04</span><span>Flatten(start_dim=1)<small>Keep the first dimension: batch</small></span></div><output id="shape-flat">[2, 512]</output></li>
63
+ <li id="linear-row" class="mismatch"><div><span class="step">05</span><span id="linear-name">Linear(128, 5)<small>Expects 128 features per image</small></span></div><output id="shape-output">512 ≠ 128</output></li>
64
+ </ol>
65
+
66
+ <section id="verdict" class="verdict mismatch" aria-live="polite" aria-atomic="true">
67
+ <h3 id="verdict-title">512 features arrived. The head expects 128.</h3>
68
+ <p id="feature-math">8 channels × 8 height × 8 width = 512 features per image.</p>
69
+ <p class="matrix"><span id="matrix-left">[2, 512]</span><span aria-label="multiplied by">×</span><span id="matrix-right">[128, 5]</span><span id="matrix-answer">incompatible</span></p>
70
+ <p class="matrix-note">Linear stores its weight as <code id="weight-shape">[5, 128]</code> and multiplies by its transpose.</p>
71
+ <code id="error-message" class="error">mat1 and mat2 shapes cannot be multiplied (2x512 and 128x5)</code>
72
+ </section>
73
+ <p id="mode-note" class="mode-note">Change the head to 512 to match this 16 × 16 example. Then try a wider image: the fixed width is a contract, not a universal fix.</p>
74
+ </div>
75
+ </div>
76
+ </section>
77
+
78
+ <section class="reshape-section" aria-labelledby="reshape-title">
79
+ <div>
80
+ <h2 id="reshape-title">A reshape can hide the error.</h2>
81
+ <p>On the original feature map, <code>reshape(-1, 128)</code> asks PyTorch to infer a new first dimension. That dimension may no longer count your images.</p>
82
+ <p class="field-help">This comparison uses the feature map before any optional adaptive pooling.</p>
83
+ </div>
84
+ <div id="reshape-result" class="reshape-result" aria-live="polite">
85
+ <p class="reshape-flow"><code id="reshape-before">[2, 512]</code><span>→ reshape →</span><code id="reshape-after">[8, 128]</code></p>
86
+ <p id="reshape-warning"><strong>2 images became 8 rows.</strong> Linear can now return [8, 5], but that is not one prediction per image.</p>
87
+ </div>
88
+ </section>
89
+
90
+ <section class="takeaway" aria-labelledby="takeaway-title">
91
+ <h2 id="takeaway-title">Make the contract explicit.</h2>
92
+ <p>Use <code>Flatten(start_dim=1)</code> to preserve the batch. Match the classifier width to the feature extractor and intended input size. Adaptive pooling is another design choice: it changes the computation and needs a different classifier.</p>
93
+ <p>Matching shapes only makes these operations compatible. It does not establish prediction quality or numerical equivalence.</p>
94
+ <div class="resource-links">
95
+ <a href="https://github.com/tensorviz-org/tensorviz-examples/blob/1baecebf25ff1067a9112bde382c0a398fb99747/examples/shape-mismatch/README.md">Read and run the PyTorch example</a>
96
+ <a href="https://tensorviz.dev/explore/">Explore recorded models in tensorViz</a>
97
+ </div>
98
+ </section>
99
+ <footer>Made by tensorViz with AI assistance. Original lesson code is MIT licensed. Calculations stay in your browser.</footer>
100
+ <noscript><p class="invalid">Enable JavaScript to change the inputs. The displayed default example shows a batch of two 16 × 16 images failing at Linear(128, 5).</p></noscript>
101
+ </main>
102
+ <script type="module" src="app.mjs"></script>
103
+ </body>
104
  </html>
logo.png ADDED

Git LFS Details

  • SHA256: f317e4f3e7a9ece03685ff10628712a9855b39009686c680cd715263d26cd3ce
  • Pointer size: 131 Bytes
  • Size of remote file: 183 kB
shape-math.mjs ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const MODES = Object.freeze({ broken: 128, fixed: 512, pooled: 8 });
2
+
3
+ // This lesson has a fixed Conv2d/ReLU/MaxPool2d stack, not a general shape engine.
4
+ export function inspectShapes({ batch, height, width, mode = 'broken' }) {
5
+ const values = { batch, height, width };
6
+ for (const [name, value] of Object.entries(values)) {
7
+ const min = name === 'batch' ? 1 : 2;
8
+ const max = name === 'batch' ? 64 : 256;
9
+ if (!Number.isInteger(value) || value < min || value > max) {
10
+ return { valid: false, message: `${name === 'batch' ? 'Batch size' : name === 'height' ? 'Height' : 'Width'} must be a whole number from ${min} to ${max}.` };
11
+ }
12
+ }
13
+ if (!Object.hasOwn(MODES, mode)) return { valid: false, message: 'Choose one of the three classifier heads.' };
14
+
15
+ const pooledHeight = Math.floor(height / 2);
16
+ const pooledWidth = Math.floor(width / 2);
17
+ const originalFeatures = 8 * pooledHeight * pooledWidth;
18
+ const features = mode === 'pooled' ? 8 : originalFeatures;
19
+ const inFeatures = MODES[mode];
20
+ const matches = features === inFeatures;
21
+ const reshapedBatch = batch * originalFeatures / 128;
22
+ const canReshape = Number.isInteger(reshapedBatch);
23
+ return {
24
+ valid: true,
25
+ batch, height, width, mode,
26
+ input: [batch, 3, height, width],
27
+ conv: [batch, 8, height, width],
28
+ pool: [batch, 8, pooledHeight, pooledWidth],
29
+ adaptive: mode === 'pooled' ? [batch, 8, 1, 1] : null,
30
+ flattened: [batch, features],
31
+ weight: [5, inFeatures],
32
+ output: matches ? [batch, 5] : null,
33
+ features, originalFeatures, inFeatures, matches,
34
+ error: matches ? null : `mat1 and mat2 shapes cannot be multiplied (${batch}x${features} and ${inFeatures}x5)`,
35
+ unsafeReshape: {
36
+ possible: canReshape,
37
+ shape: canReshape ? [reshapedBatch, 128] : null,
38
+ output: canReshape ? [reshapedBatch, 5] : null,
39
+ preservesBatch: canReshape && reshapedBatch === batch,
40
+ elements: batch * originalFeatures,
41
+ },
42
+ };
43
+ }
shape-math.test.mjs ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { inspectShapes } from './shape-math.mjs';
4
+
5
+ test('the canonical mismatch keeps two images and locates the feature error', () => {
6
+ const r = inspectShapes({ batch: 2, height: 16, width: 16 });
7
+ assert.deepEqual(r.flattened, [2, 512]);
8
+ assert.deepEqual(r.weight, [5, 128]);
9
+ assert.equal(r.error, 'mat1 and mat2 shapes cannot be multiplied (2x512 and 128x5)');
10
+ assert.equal(r.output, null);
11
+ });
12
+
13
+ test('a fixed-width repair preserves batch but does not promise arbitrary inputs', () => {
14
+ for (const batch of [1, 2, 5]) {
15
+ assert.deepEqual(inspectShapes({ batch, height: 16, width: 16, mode: 'fixed' }).output, [batch, 5]);
16
+ }
17
+ assert.equal(inspectShapes({ batch: 2, height: 16, width: 20, mode: 'fixed' }).matches, false);
18
+ });
19
+
20
+ test('odd dimensions use floor pooling and the explicit adaptive stage changes the head', () => {
21
+ const r = inspectShapes({ batch: 3, height: 17, width: 19, mode: 'pooled' });
22
+ assert.deepEqual(r.pool, [3, 8, 8, 9]);
23
+ assert.deepEqual(r.adaptive, [3, 8, 1, 1]);
24
+ assert.deepEqual(r.flattened, [3, 8]);
25
+ assert.deepEqual(r.output, [3, 5]);
26
+ });
27
+
28
+ test('making Linear run by reshape must not be mistaken for preserving samples', () => {
29
+ const r = inspectShapes({ batch: 2, height: 16, width: 16 });
30
+ assert.deepEqual(r.unsafeReshape.shape, [8, 128]);
31
+ assert.deepEqual(r.unsafeReshape.output, [8, 5]);
32
+ assert.equal(r.unsafeReshape.preservesBatch, false);
33
+ assert.equal(inspectShapes({ batch: 1, height: 3, width: 3 }).unsafeReshape.possible, false);
34
+ assert.equal(inspectShapes({ batch: 2, height: 8, width: 8 }).unsafeReshape.preservesBatch, true);
35
+ });
36
+
37
+ test('invalid inputs are rejected instead of showing stale or impossible shapes', () => {
38
+ const base = { batch: 2, height: 16, width: 16 };
39
+ for (const [name, value] of [['batch', 0], ['batch', 65], ['height', 1], ['width', 257], ['width', 2.5], ['height', NaN], ['batch', Infinity], ['mode', 'other']]) {
40
+ const r = inspectShapes({ ...base, [name]: value });
41
+ assert.equal(r.valid, false, `${name}=${value}`);
42
+ assert.ok(r.message);
43
+ assert.equal(r.flattened, undefined);
44
+ }
45
+ });
styles.css ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ color-scheme: light;
3
+ --ink: #1b2520;
4
+ --muted: #5f6e65;
5
+ --line: #dce4de;
6
+ --green: #176047;
7
+ --green-light: #edf6f0;
8
+ --red: #a1392a;
9
+ --red-light: #fff3ee;
10
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
11
+ color: var(--ink);
12
+ background: #ffffff;
13
+ }
14
+ * { box-sizing: border-box; }
15
+ body { margin: 0; }
16
+ main { max-width: 1136px; padding: 28px 48px 24px; margin: auto; }
17
+ a { color: var(--green); text-underline-offset: 4px; text-decoration-thickness: 1px; }
18
+ a:hover { text-decoration-thickness: 2px; }
19
+ button, input { font: inherit; }
20
+ button { cursor: pointer; }
21
+ button:focus-visible, input:focus-visible, a:focus-visible { outline: 3px solid #76aa90; outline-offset: 4px; }
22
+ [hidden] { display: none !important; }
23
+ code, output { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
24
+ code { font-size: .9em; }
25
+ .brand { display: flex; gap: 12px; align-items: center; font-size: 18px; font-weight: 650; }
26
+ .brand img { object-fit: contain; flex: 0 0 auto; }
27
+ .brand-separator { color: #a4b0a8; margin: 0 8px; font-weight: 400; }
28
+ .brand-label { font-weight: 450; }
29
+ .brand a { margin-left: auto; font-size: 13px; font-weight: 500; }
30
+ .intro { padding: 58px 0 32px; max-width: 730px; }
31
+ h1 { font-size: clamp(32px, 4.5vw, 50px); line-height: 1.06; letter-spacing: -.045em; margin: 0 0 20px; font-weight: 650; }
32
+ p { font-size: 16px; line-height: 1.6; margin: 0 0 12px; }
33
+ .intro > p { max-width: 650px; color: var(--muted); }
34
+ .intro .boundary { font-size: 12px; color: var(--muted); margin-top: 18px; }
35
+ .lab { display: grid; grid-template-columns: 285px minmax(0, 1fr); border-block: 1px solid var(--line); }
36
+ .controls { padding: 28px 26px 28px 0; border-right: 1px solid var(--line); }
37
+ h2 { font-size: 21px; letter-spacing: -.025em; line-height: 1.25; margin: 0 0 9px; font-weight: 630; }
38
+ .section-note, .field-help { font-size: 12px; color: var(--muted); }
39
+ .section-note { margin-bottom: 21px; }
40
+ .dimensions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
41
+ .dimensions label { font-size: 12px; font-weight: 600; }
42
+ .dimensions input { display: block; margin-top: 8px; width: 100%; border: 1px solid #b8c7bd; border-radius: 7px; background: #fff; padding: 10px 8px; font-size: 19px; color: var(--ink); }
43
+ .field-help { margin-top: 8px; line-height: 1.5; }
44
+ .presets { display: flex; gap: 6px; margin: 16px 0 29px; flex-wrap: wrap; }
45
+ .presets button { font-size: 11px; padding: 7px 9px; background: white; border: 1px solid var(--line); border-radius: 5px; color: var(--muted); }
46
+ .presets button:hover { border-color: var(--green); color: var(--green); }
47
+ fieldset { border: 0; padding: 0; margin: 0; }
48
+ legend { font-size: 12px; font-weight: 650; margin-bottom: 10px; }
49
+ .mode { display: flex; align-items: flex-start; gap: 9px; padding: 12px 7px; border-bottom: 1px solid var(--line); font-size: 12px; line-height: 1.4; cursor: pointer; }
50
+ .mode:has(input:checked) { background: var(--green-light); border-color: transparent; border-radius: 6px; }
51
+ .mode input { accent-color: var(--green); margin: 2px 0 0; flex: 0 0 auto; }
52
+ .mode strong { display: block; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; font-weight: 500; margin-top: 5px; color: var(--muted); }
53
+ .reset { border: 0; background: none; padding: 0; color: var(--muted); text-decoration: underline; text-underline-offset: 4px; margin-top: 22px; font-size: 12px; }
54
+ .results { padding: 19px 0 26px 30px; min-width: 0; }
55
+ .flow { list-style: none; padding: 0; margin: 0; }
56
+ .flow li { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 13px 0; border-bottom: 1px solid var(--line); font-size: 13px; }
57
+ .flow li > div { display: flex; gap: 11px; align-items: baseline; }
58
+ .step { color: #89988e; font-size: 10px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
59
+ .flow small { display: block; color: var(--muted); font-size: 11px; line-height: 1.5; margin-top: 3px; }
60
+ .flow output { font-size: 12px; white-space: nowrap; }
61
+ .flow .mismatch output { color: var(--red); font-weight: 600; }
62
+ .flow .match output { color: var(--green); font-weight: 600; }
63
+ .verdict { padding: 18px 20px; margin-top: 20px; border-radius: 8px; }
64
+ .verdict.mismatch { background: var(--red-light); }
65
+ .verdict.match { background: var(--green-light); }
66
+ h3 { margin: 0 0 8px; font-size: 16px; line-height: 1.4; font-weight: 650; }
67
+ .verdict p { font-size: 12px; line-height: 1.55; }
68
+ .matrix { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; padding: 11px 0 2px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
69
+ .matrix span:nth-child(1), .matrix span:nth-child(3) { font-size: 20px; letter-spacing: -.04em; }
70
+ .matrix span:last-child { font-size: 11px; color: var(--muted); }
71
+ .matrix-note { color: var(--muted); margin-top: 5px; }
72
+ .error { display: block; font-size: 11px; line-height: 1.6; color: var(--red); overflow-wrap: anywhere; margin-top: 10px; }
73
+ .mode-note { font-size: 12px; color: var(--muted); margin: 14px 0 0; }
74
+ .invalid { padding: 18px; border-left: 3px solid var(--red); background: var(--red-light); color: var(--red); font-size: 14px; }
75
+ .reshape-section { display: grid; grid-template-columns: 1fr 1fr; gap: 44px; padding: 38px 0; border-bottom: 1px solid var(--line); }
76
+ .reshape-section p, .takeaway p { font-size: 14px; color: var(--muted); }
77
+ .reshape-section .field-help { font-size: 11px; }
78
+ .reshape-result { align-self: center; background: #f5f7f5; padding: 22px; border-radius: 8px; }
79
+ .reshape-result p { font-size: 13px; }
80
+ .reshape-result strong { color: var(--ink); }
81
+ .reshape-flow { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
82
+ .reshape-flow code { font-size: 20px; color: var(--ink); }
83
+ .reshape-flow span { font-size: 11px; }
84
+ .takeaway { max-width: 800px; padding: 32px 0 28px; }
85
+ .resource-links { display: flex; flex-wrap: wrap; gap: 12px 28px; margin-top: 22px; font-size: 13px; }
86
+ footer { border-top: 1px solid var(--line); padding-top: 17px; color: var(--muted); font-size: 11px; line-height: 1.6; }
87
+ .skip-link { position: absolute; left: 12px; top: -100px; background: #fff; padding: 10px; z-index: 1; }
88
+ .skip-link:focus { top: 10px; }
89
+ @media (max-width: 760px) {
90
+ main { padding: 22px 24px; }
91
+ .brand a { display: none; }
92
+ .intro { padding-top: 40px; }
93
+ .lab { grid-template-columns: 1fr; }
94
+ .controls { border-right: 0; border-bottom: 1px solid var(--line); padding-right: 0; }
95
+ .dimensions { max-width: 380px; }
96
+ .presets { margin-bottom: 24px; }
97
+ .results { padding-left: 0; padding-top: 12px; }
98
+ .reshape-section { grid-template-columns: 1fr; gap: 10px; padding: 30px 0; }
99
+ }
100
+ @media (max-width: 390px) {
101
+ main { padding: 18px 18px; }
102
+ .brand { font-size: 16px; }
103
+ .brand-separator { margin: 0 4px; }
104
+ h1 { font-size: 32px; }
105
+ .flow li { align-items: flex-start; flex-direction: column; gap: 7px; }
106
+ .flow output { padding-left: 25px; }
107
+ .verdict { padding: 16px; }
108
+ }
verify_pytorch.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compare the teaching arithmetic with real CPU operations; no downloads or UI.
2
+
3
+ Model derived from the MIT tensorViz shape-mismatch example, commit
4
+ 1baecebf25ff1067a9112bde382c0a398fb99747. See README for the public source.
5
+ """
6
+
7
+ import json
8
+ import platform
9
+ from pathlib import Path
10
+ import subprocess
11
+
12
+ import torch
13
+ from torch import nn
14
+
15
+
16
+ ROOT = Path(__file__).resolve().parent
17
+ CASES = [
18
+ {"batch": batch, "height": height, "width": width, "mode": mode}
19
+ for batch, height, width in [
20
+ (1, 16, 16), (2, 16, 16), (5, 16, 16),
21
+ (2, 16, 20), (3, 17, 19), (2, 8, 8), (1, 3, 3),
22
+ ]
23
+ for mode in ["broken", "fixed", "pooled"]
24
+ ]
25
+ NODE_SCRIPT = """
26
+ import { inspectShapes } from './shape-math.mjs';
27
+ let text = '';
28
+ for await (const chunk of process.stdin) text += chunk;
29
+ console.log(JSON.stringify(JSON.parse(text).map(inspectShapes)));
30
+ """
31
+
32
+
33
+ def main():
34
+ actual_js = json.loads(subprocess.check_output(
35
+ ["node", "--input-type=module", "-e", NODE_SCRIPT],
36
+ input=json.dumps(CASES), text=True, cwd=ROOT,
37
+ ))
38
+ torch.manual_seed(0)
39
+ checked = []
40
+ with torch.inference_mode():
41
+ for case, result in zip(CASES, actual_js, strict=True):
42
+ b, h, w = case["batch"], case["height"], case["width"]
43
+ x = torch.randn(b, 3, h, w, device="cpu")
44
+ features = nn.Sequential(nn.Conv2d(3, 8, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2)).eval()
45
+ conv = features[:2](x)
46
+ pooled = features[2](conv)
47
+ original_flat = nn.Flatten(start_dim=1)(pooled)
48
+ assert list(x.shape) == result["input"]
49
+ assert list(conv.shape) == result["conv"]
50
+ assert list(pooled.shape) == result["pool"]
51
+ assert original_flat.shape[0] == b, "Flatten must keep the sample count"
52
+ stage = nn.AdaptiveAvgPool2d((1, 1))(pooled) if case["mode"] == "pooled" else pooled
53
+ if case["mode"] == "pooled":
54
+ assert list(stage.shape) == result["adaptive"]
55
+ flat = nn.Flatten(start_dim=1)(stage)
56
+ assert list(flat.shape) == result["flattened"]
57
+ head = nn.Linear({"broken": 128, "fixed": 512, "pooled": 8}[case["mode"]], 5).eval()
58
+ assert list(head.weight.shape) == result["weight"]
59
+ try:
60
+ out = head(flat)
61
+ except RuntimeError as exc:
62
+ assert not result["matches"]
63
+ assert str(exc) == result["error"]
64
+ else:
65
+ assert result["matches"]
66
+ assert list(out.shape) == result["output"] == [b, 5]
67
+ # A batch-preserving path must give one independently computed row per image.
68
+ per_image = []
69
+ for image in x.split(1):
70
+ value = features(image)
71
+ if case["mode"] == "pooled":
72
+ value = nn.AdaptiveAvgPool2d((1, 1))(value)
73
+ per_image.append(head(value.flatten(1)))
74
+ torch.testing.assert_close(out, torch.cat(per_image), atol=1e-6, rtol=1e-5)
75
+ try:
76
+ unsafe = original_flat.reshape(-1, 128)
77
+ except RuntimeError:
78
+ assert not result["unsafeReshape"]["possible"]
79
+ else:
80
+ expected = result["unsafeReshape"]
81
+ assert list(unsafe.shape) == expected["shape"]
82
+ unsafe_out = nn.Linear(128, 5)(unsafe)
83
+ assert list(unsafe_out.shape) == expected["output"]
84
+ assert (unsafe.shape[0] == b) == expected["preservesBatch"]
85
+ checked.append({**case, "flattened": result["flattened"], "output": result["output"], "error": result["error"]})
86
+ print(json.dumps({"python": platform.python_version(), "torch": torch.__version__, "device": "cpu", "cases_passed": len(checked), "cases": checked}, indent=2))
87
+
88
+
89
+ if __name__ == "__main__":
90
+ main()