Spaces:
Running
Download text_trace_implementation_plan.md from simon-clmtd/autodiff-visualizer: direct link, hf CLI and curl.
- Browser
- Download file 20.3 kB
-
https://huggingface.co/spaces/simon-clmtd/autodiff-visualizer/resolve/main/text_trace_implementation_plan.md
- Command line
-
hf download hf://spaces/simon-clmtd/autodiff-visualizer/text_trace_implementation_plan.md
-
curl -L -o text_trace_implementation_plan.md https://huggingface.co/spaces/simon-clmtd/autodiff-visualizer/resolve/main/text_trace_implementation_plan.md
A newer version of the Gradio SDK is available: 6.30.0
Yes. I would implement the textual view as a second renderer of the same semantic computation model, not as an explanation independently reconstructed from the expression. That is the key architectural decision: graph and text must be two projections of exactly the same forward/backward events.
Implementation plan: Stepwise Text Trace
- Define the purpose precisely
The graph and trace answer different questions:
Graph: Where does information flow? Trace: What happens, step by step?
The textual trace should linearize the computation while preserving the visual grammarโs concepts:
FORWARD input values โ local operation โ resulting quantity BACKWARD incoming adjoint โ local derivative/rule โ numerical application โ contribution โ accumulation if necessary โ resulting adjoint
It must not introduce a second autodiff explanation system.
โธป
- Make DiagramModel the shared semantic source
Target architecture:
Tensor/autodiff engine
โ
โผ
DiagramModel
โ
โโโโโโโโโโโโโโดโโโโโโโโโโโโโ
โ โ
โผ โผ
Mermaid renderer Trace renderer
โ โ
โผ โผ
computation graph stepwise text trace
The trace renderer must consume the same:
- quantity IDs: x, y, n1, n2, โฆ, out;
- operation formulas;
- operand occurrences;
- primal values;
- adjoints;
- local derivative rules;
- evaluated local derivatives;
- gradient contributions;
- accumulation relationships;
- output seed.
Do not recompute derivatives inside the trace renderer.
โธป
- Add an explicit trace renderer
Create something such as:
trace_renderer.py
with a public entry point conceptually:
render_trace(model, notation, mode) -> str
where:
notation = mathbook | code mode = forward | forward_backward
This should parallel the existing diagram renderer.
โธป
- Preserve execution order explicitly
The graph uses spatial topology. The trace needs deterministic sequential order.
Forward
Operations appear in topological execution order:
n1 n2 n3 ... out
Backward
Operations appear in reverse topological order:
out ... n3 n2 n1
Within an operation, operand occurrences retain their original argument order.
For:
n1 = x * x
there are still two operand occurrences. Do not collapse them merely because they refer to the same variable.
โธป
- Forward-pass trace grammar
Every forward operation gets the same structure.
For example:
FORWARD PASS Inputs โโโโโโ x = 3 y = 4
- Multiply โโโโโโโโโโโ n1 = x ยท x Inputs: x = 3 x = 3 Apply: n1 = 3 ยท 3 Result: n1 = 9
- Multiply โโโโโโโโโโโ n2 = n1 ยท y Inputs: n1 = 9 y = 4 Apply: n2 = 9 ยท 4 Result: n2 = 36
- Add โโโโโโ out = 2 + y + n2 Inputs: 2 = 2 y = 4 n2 = 36 Apply: out = 2 + 4 + 36 Result: out = 42
The exact amount of repetition can later be tuned, but the schema should initially remain explicit.
That is consistent with the pedagogical goal of this application.
โธป
- Treat the Output boundary separately
As in the graph, do not conflate the final operation with the Output presentation quantity.
After the final forward operation:
OUTPUT โโโโโโ out = 42
In Forward+Backward mode this then becomes the starting point for reverse mode:
BACKWARD PASS Seed โโโโ outฬ = โout/โout = 1
This mirrors the established graph semantics exactly.
โธป
- Backward-pass trace grammar
Every local backward computation should follow one rigid rhetorical template:
Incoming adjoint โ Local derivative / backward rule โ Numerical application โ Contribution
For a scalar multiplication:
- Backpropagate through n2 = n1 ยท y โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ Incoming adjoint: nฬ2 = โout/โn2 = 1 With respect to n1 โโโโโโโโโโโโโโโโโโ Local derivative: โn2/โn1 = y Apply: nฬ2 ยท y = 1 ยท 4 = 4 Contribution to n1: 4 With respect to y โโโโโโโโโโโโโโโโโ Local derivative: โn2/โy = n1 Apply: nฬ2 ยท n1 = 1 ยท 9 = 9 Contribution to y: 9
This is the textual equivalent of the two derivative trapezoids.
โธป
- Make the adjoint interpretation explicit
Whenever a new accumulated adjoint becomes available, state it explicitly:
Adjoint: nฬ1 = โout/โn1 = 4
This is important because the adjoint is our notation for:
the chain rule accumulated so far from the output to this quantity.
The trace should therefore use exactly the same adjoint notation as the graph.
โธป
- Make accumulation an explicit textual step
Do not silently sum contributions.
For x*x, write:
Backpropagate through n1 = x ยท x โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ Incoming adjoint: nฬ1 = โout/โn1 = 4 Argument 1: x โโโโโโโโโโโโโ Local derivative: โn1/โx = x Apply: nฬ1 ยท x = 4 ยท 3 = 12 Contribution to x: 12 Argument 2: x โโโโโโโโโโโโโ Local derivative: โn1/โx = x Apply: nฬ1 ยท x = 4 ยท 3 = 12 Contribution to x: 12 Accumulate contributions to x โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ xฬ = 12 + 12 = 24 Therefore: xฬ = โout/โx = 24
This is the exact textual equivalent of:
12 โโโ ฮฃ โโโ xฬ = 24 12 โโโ
The trace therefore preserves the core invariant:
Along one backward dependency: apply/multiply. Across multiple contributions: add.
โธป
- Distinguish contributions from adjoints
This distinction must be just as strict in text as in the graph.
Before accumulation:
Contribution to x: 12
not:
xฬ = 12
if another contribution to x still exists.
Only after all contributions have been collected may the trace say:
xฬ = 24
This prevents exactly the conceptual ambiguity the explicit ฮฃ nodes solved in the graph.
โธป
- Tensor operations use the same structure
Do not create a separate tensor trace grammar.
For:
n1 = W @ x
write:
Backpropagate through n1 = W @ x โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ Incoming adjoint: nฬ1 = [-0.10, 0.24, -0.14] With respect to W โโโโโโโโโโโโโโโโโ Local backward rule: Wฬ contribution = nฬ1 xแต Apply: [-0.10, 0.24, -0.14] [1, 2]แต Result: [[-0.10, -0.20], [ 0.24, 0.48], [-0.14, -0.28]] Contribution to W: [[-0.10, -0.20], [ 0.24, 0.48], [-0.14, -0.28]] With respect to x โโโโโโโโโโโโโโโโโ Local backward rule: xฬ contribution = Wแต nฬ1 Apply: ... Result: [-0.12, 0.20] Contribution to x: [-0.12, 0.20]
If each target receives only one contribution, those contributions immediately establish the respective adjoints.
โธป
- Handle VJPs honestly
For tensor operations, forcing everything into scalar notation such as
\bar v,\frac{\partial v}{\partial x}
can obscure what is actually happening.
The trace should use the operation-specific local backward rule already represented by the model.
For softmax:
Backpropagate through n3 = softmax(n2) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ Incoming adjoint: nฬ3 = [0, 1, 0] Local backward rule: nฬ2 = n3 โ (nฬ3 - sum(nฬ3 โ n3)) Values: n3 = [0.18, 0.59, 0.24] nฬ3 = [0, 1, 0] Apply: ... Result: nฬ2 = [-0.10, 0.24, -0.14]
This keeps the same conceptual ruleโincoming adjoint transformed by the local backward operationโwithout pretending every tensor derivative should be displayed as a huge Jacobian.
โธป
- Reuse the canonical tensor formatter
The trace must use the tensor-display policy we just defined.
Initially:
element budget = 16
or, if you deliberately want more detail in text:
graph budget = 16 trace budget = 32
But both must use the same formatter and semantics.
Thus a 3\times2 gradient must appear as its six actual values:
[[-0.10, -0.20], [ 0.24, 0.48], [-0.14, -0.28]]
Never:
shape (3, 2)
For a large tensor:
[[0.12, -0.31, โฆ, 0.08], [ โฎ โฎ โฑ โฎ ], [-0.42, 0.06, โฆ, 0.29]] shape (128, 64)
โธป
- Respect Forward-only mode strictly
If the user selects Forward only, the textual trace must end after the forward result.
For example:
FORWARD PASS Inputs โโโโโโ x = 3 y = 4 ... OUTPUT โโโโโโ out = 42
There must be no:
BACKWARD PASS outฬ gradient derivative contribution adjoint โ
This is the textual counterpart of the Forward-only purity invariant.
โธป
- Support Mathbook and Code notation
The trace should obey the existing notation toggle.
Mathbook
Local derivative: โn2/โy = n1 Apply: nฬ2 ยท n1 = 1 ยท 9 = 9
Code
Local derivative: diff(n1 _ y, y) = n1 Apply: n2_bar _ n1 = 1 * 9 = 9
Do not implement separate mathematical semantics. Only notation changes.
Use the existing centralized formatting helpers wherever possible.
โธป
- Introduce structured trace events if DiagramModel is insufficient
This is the one architectural extension I would seriously consider.
Rendering directly by reverse-engineering edges can become fragile. If DiagramModel does not already make execution steps explicit enough, add semantic objects such as:
@dataclass class ForwardStep: result: QuantityState operation: OperationState operands: list[QuantityState] @dataclass class BackwardOperandStep: operation: OperationState operand_occurrence: int target: QuantityState incoming_adjoint: Any local_rule: ... local_value: Any contribution: Any @dataclass class AccumulationStep: target: QuantityState contributions: list[Any] resulting_adjoint: Any
Then both renderers can consume these.
The important architecture is:
autodiff computation โ โผ semantic steps โ โโโโโโโดโโโโโโ โผ โผ graph text
not:
autodiff โ graph โ โผ parse graph back into explanation
โธป
- Keep operation identity separate from pedagogical quantity names
The trace should use the established:
x, y, W, b n1, n2, n3, ... out
Do not reintroduce O1, O2, etc.
If an internal operation ID is required, keep it internal.
The learner should see:
- n2 = n1 + b
not:
Operation O3
unless there is eventually a specific debugging mode that exposes internal IDs.
โธป
- Do not overuse operation names
A heading like:
- Multiply
can be useful, but the actual formula should remain authoritative:
n2 = n1 ยท y
For less familiar operations:
- Softmax โโโโโโโโโโโ n3 = softmax(n2)
This is clearer than generating verbose prose such as:
โThe third operation applies the softmax function to the second intermediate node.โ
The trace should remain computational rather than narrative.
โธป
- Suggested complete scalar fixture
Use the existing canonical expression:
out=y(x x)+(y+2),\quad x=3,;y=4.
A shortened target trace is:
FORWARD PASS
Inputs x = 3 y = 4
- n1 = x ยท x = 3 ยท 3 = 9
- n2 = n1 ยท y = 9 ยท 4 = 36
out = n2 + y + 2 = 36 + 4 + 2 = 42 OUTPUT out = 42 BACKWARD PASS
Seed outฬ = โout/โout = 1- Backpropagate through out = n2 + y + 2 To n2: โout/โn2 = 1 outฬ ยท 1 = 1 ยท 1 = 1 contribution = 1 To y: โout/โy = 1 outฬ ยท 1 = 1 ยท 1 = 1 contribution = 1 Therefore: nฬ2 = โout/โn2 = 1
- Backpropagate through n2 = n1 ยท y Incoming: nฬ2 = 1 To n1: โn2/โn1 = y nฬ2 ยท y = 1 ยท 4 = 4 contribution = 4 To y: โn2/โy = n1 nฬ2 ยท n1 = 1 ยท 9 = 9 contribution = 9 Therefore: nฬ1 = โout/โn1 = 4
Backpropagate through n1 = x ยท x Incoming: nฬ1 = 4 Argument 1: โn1/โx = x nฬ1 ยท x = 4 ยท 3 = 12 contribution = 12 Argument 2: โn1/โx = x nฬ1 ยท x = 4 ยท 3 = 12 contribution = 12 ACCUMULATION
For x: xฬ = 12 + 12 = 24 For y: yฬ = 1 + 9 = 10 FINAL GRADIENTS
โout/โx = xฬ = 24 โout/โy = yฬ = 10
There is one refinement I would make in the real implementation: perform each accumulation at the point where the resulting adjoint becomes available, rather than collecting all accumulation into a final section. That better reflects reverse-mode execution:
contributions to y: 1 + 9 โ yฬ = 10
and then continue.
โธป
- Make accumulation timing semantically correct
This is important for shared intermediates.
The trace must not backpropagate through quantity v until all downstream contributions to v have been accumulated.
For example:
contribution to n1 from path A = ... contribution to n1 from path B = ... Accumulate: nฬ1 = contribution A + contribution B
Only then:
Backpropagate through n1 = ... Incoming adjoint: nฬ1 = ...
That reproduces the semantics of the graphโs ฮฃ node.
โธป
- Add dedicated tests at the semantic level
Tests should verify more than strings.
For x*x:
two operand backward steps two contributions to x one accumulation step final xฬ = 2x
For x**2 + x:
two independent contributions to x one accumulation
For (x+y)*(x+y):
multiple contributions to shared intermediate accumulation occurs before backpropagating through that intermediate
For tensor cases:
actual tensor values present no shape substituted for value VJP dimensions correct
โธป
- Add rendering snapshot tests
Then test representative complete trace strings for:
- x*y
- x*x
- x**2 + x
- (x+y)*(x+y)
- (x*x)*y + (y+2)
- W @ x
- softmax(W @ x + b)[1]
- the larger loss example.
Test both Mathbook and Code modes where notation differs.
โธป
- Add mode-purity tests
For forward:
assert no_adjoint_symbols assert no_local_derivatives assert no_contributions assert no_accumulations assert no_backward_heading
For forward_backward:
assert seed_present assert incoming_adjoints_present assert local_rules_present assert numerical_applications_present assert accumulations_present_when_required
This should parallel the diagram mode-purity tests.
โธป
- UI integration
I would expose the trace as a view choice, not as another mathematical mode.
For example:
View: โ Graph โ Step-by-step
while keeping:
Pass: โ Forward โ Forward + Backward Notation: โ Mathbook โ Code
These dimensions should remain orthogonal:
Graph Step-by-step
Forward โ โ Forward+Back โ โ Mathbook โ โ Code โ โ
A future third option could show both side by side, but I would not start there.
โธป
- Help text should explicitly connect the two views
A short explanation can say:
Graph view shows where values and gradients flow through the computation graph. Step-by-step view shows the same computation in execution order. During the backward pass, each step starts with the current adjoint, applies one local backward rule, and produces a contribution. Contributions to the same quantity are added to form its adjoint.
That explicitly teaches that these are two representations of one process.
โธป
- Final implementation sequence
I would implement in this order:
- Confirm DiagramModel exposes all required semantic data.
- Add structured ForwardStep, BackwardOperandStep, and AccumulationStep objects only where needed.
- Generate these steps deterministically from the computation model.
- Implement trace_renderer.py.
- Implement Forward-only trace first.
- Add output boundary and backward seed.
- Add scalar backward steps.
- Add explicit accumulation.
- Add tensor/VJP backward steps.
- Route all numerical values through the canonical tensor formatter.
- Add Mathbook/Code formatting.
- Add semantic tests.
- Add trace snapshot tests.
- Add mode-purity tests.
- Integrate the view selector into the app.
- Update VISUAL_LANGUAGE.md.
- Visually compare graph and trace for all canonical fixtures.
The final acceptance invariant should be:
For every piece of information visible in the stepwise trace, there is a corresponding semantic object in the computation model; and for every forward/backward computation represented in the graph, the trace can present the same computation sequentially without recomputing or reinterpreting it.
That will keep the textual view from becoming a parallel explanation system that gradually diverges from the graphโthe main implementation risk I would avoid.
Yes. I think two explicit action buttons are better than introducing another โViewโ radio selector. The current UI already has two conceptual settingsโcomputation mode and notationโso output type can naturally be the action the user takes.
I would change the bottom row to:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ Generate Diagram โ Generate Trace โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Both buttons consume exactly the same settings above them:
Expression Input Values Graph/Computation Mode Derivative Notation
I would probably rename โGraph Modeโ โ โComputation Modeโ at the same time. Once the application produces text traces, โGraph Modeโ is misleading because Forward Only versus Forward & Backward controls the semantics of both representations.
So the conceptual UI becomes:
Expression Input Values Computation Mode Derivative Notation โ Forward & Backward โ Mathbook โ Forward Only โ Code โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ Generate Diagram โ โ Generate Trace โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
And the behavior is particularly clean:
Computation mode Generate Diagram Generate Trace Forward Only pure forward graph pure forward stepwise trace Forward & Backward complete autodiff graph forward + backward stepwise trace
The notation selection applies identically to both.
I would also keep the buttons visually equal. โGenerate Diagramโ should not look primary and โGenerate Traceโ secondary, because these are two equivalent representations of the same computation, not a main action plus an auxiliary feature.
One small naming point: I prefer Generate Trace over โGenerate Textโ or โText View.โ โTraceโ accurately communicates that this is the computation presented in execution order. The output itself can then be headed Step-by-step trace to make the term immediately understandable.
Architecturally, this fits the plan nicely:
current settings
โ
expression + inputs
computation mode
notation
โ
build semantic model
โ
โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโ
โผ โผ
Generate Diagram Generate Trace
โ โ
โผ โผ
Mermaid renderer Trace renderer
Most importantly, the buttons select the representation; they must not select different computation logic. That keeps the UI aligned with the architecture we have been defining.