|
Uni20
Asynchronous tensor and runtime library.
|
This document explains the Var<T> model used by Uni20. It focuses on behavior and usage patterns, not internal queue implementation.
Uni20 AD is dataflow-based, not tape-replay-based.
Var operation schedules forward and reverse coroutine work immediately.This keeps AD on the same runtime model as normal async computation.
| Type | Role |
|---|---|
Var<T> | user-facing AD variable (value + grad) |
Async<T> | forward value channel |
ReverseValue<T> | gradient accumulation channel |
Var<T> is:
For custom ops, the common signature is Var<T> f(Var<T> x) (pass by value, not const&).
Var<T>.1 for scalar loss): loss.grad = 1.0;.backprop() and wait only where needed.x.grad.wait() waits for completion without materializing the value. x.grad.get_wait() and x.grad.final_wait() are convenience forms for waiting and reading the value. The explicit async forms are x.backprop().get_wait() and x.grad.backprop().get_wait().
ScopedScheduler is mainly for tests or temporary scheduler overrides.run_all() is usually redundant; waiting on outputs/gradients drives execution.run_all() is still useful with DebugScheduler for deterministic debugging.Uni20 propagates gradients in the dL/dz* direction (Wirtinger conjugate gradient), assuming a real-valued scalar loss L.
For f(z, z*), the implemented reverse update shape is:
out_grad += in_grad * conj(df_dz) + conj(in_grad) * df_dz_conj
where:
in_grad is the upstream dL/df*df_dz is df/dzdf_dz_conj is df/dz*For holomorphic functions (df_dz_conj = 0), this reduces to:
out_grad += in_grad * conj(df_dz)
That is the pattern used by the sin(Var<T>) implementation.
Recommended structure:
value path).grad path).Var.Important points:
static and capturelessresult.grad.input() for upstream gradientx.grad.output() for accumulation into inputsor_cancel() the first awaited reverse dependencyAn output gradient that is never seeded is an absent value, not a zero tensor and not a failure. Its or_cancel() reader prunes that reverse coroutine, whose unwritten output gradient is then absent in the preceding accumulation stage. This permits unused differentiable branches to leave scope harmlessly. A genuine exception from a forward or reverse dependency is never converted into this cancellation path; it propagates if the failed result is consumed and is discarded only when the result and all of its dependents are unobserved.
The gradient channel of a temporary Var is finalized when that descriptor is destroyed. A named intermediate retained while the graph is executed must currently be finalized explicitly after graph construction:
This tells ReverseValue that no further gradient contributions will be scheduled for the intermediate. A future graph- or tape-level finalization API may centralize this bookkeeping, but reverse operations must still use the upstream-first pruning rule above.
async_ad_stress_example exercises this behavior with deterministic discarded branches, deliberately failing unobserved branches, and an observed failure whose original exception must survive:
The ReverseValue surface currently has overlap:
get_wait() and final_wait() are equivalentbackprop() exposes the underlying Async<T> channelinput()/read() and output() expose buffer-level accessFor most user code, the minimal set is:
loss.grad = seedx.grad.get_wait()src/uni20/async/var.hppsrc/uni20/async/var_toys.hppsrc/uni20/async/reverse_value.hpptests/async/test_var.cpptests/async/test_reverse_value.cpp