yum-mirror/slang
Making it easier to work with shaders
git clone https://git.yummers.dev/yum-mirror/slang
c3557978c
master
Reverse Mode Autodiff (Out of Date)
This document serves as a design reference for reverse-mode auto-diff in the Slang compiler.
Reverse-Mode Passes
Rather than implementing reverse-mode as a separate pass, Slang implements this as a series of independent passes:
If a function needs a reverse-mode version generated:
- Linearize the function, and all dependencies.
- Propagate differential types through the linearized code.
- Unzip by moving primal insts to before differential insts.
- Transpose the differential insts.
Linearization (Forward-mode)
Overview
(This is a incomplete section. More details coming soon)
Consider an arbitrary function float f(float a, float b, float c, ..., z) which takes in N inputs and generates one output y. Linearization aims to generate the first-order Taylor expansion of f about all of it's inputs.
Mathematically, the forward derivative fwd_f represents df/da * (a_0 - a) + df/db * (b_0 - b) + ..., where a_0 is the value at which the Taylor expansion was produced. The quantity a_0 - a is known as the 'differential' (for brevity we'll denote them da, db, dc, etc..), and there is at-most one differential per input.
Thus, the new function's signature should be fwd_f(float a, float da, float b, float db, float c, float dc, ...). For simplicity, we'll use pairs instead of interleaving the original and differential parameters. We use the intrinsic DifferentialPair<T> (or for short: DP<T>) to denote this.
The signature we use is then fwd_f(DP<float> a, DP<float> b, DP<float> c)
An example of linearization:
float f (float a ,float b ) {if (a > 0 ) {return a + b + 2.0 * a * b ; }else {return sqrt (a ); } }
We'll write out the SSA form of this function.
float f_SSA (float a ,float b ) {bool _b1 = a > 0 ;if (_b1 ) {float _t1 = a + b ;float _t2 = 2.0 * a ;float _t3 = _t2 * b ;float _t4 = _t1 + _t3 ;return _t4 ; }else {float _t1 = sqrt (a );return _t1 ; } }DP < float > f_SSA (DP < float > dpa ,DP < float > dpb ) {bool _b1 = dpa .p > 0 ;if (_b1 ) {float _t1 = dpa .p + dpb .p ;float _t1_d = dpa .d + dpb .d ;float _t2 = 2.0 * dpa .p ;float _t2_d = 0.0 * dpa .p + 2.0 * dpa .d ;float _t3 = _t2 * dpb .p ;float _t3_d = _t2_d * dpb .p + _t2 * dpb .d ;float _t4 = _t1 + _t3 ;float _t4_d = _t1_d + _t3_d ;return DP < float > (_t4 ,_t4_d ); }else {DP < float > _t1_dp = sqrt_fwd (dpa );return DP < float > (_t1_dp .p ,_t1_dp .d ); } }
In the result, the primal part of the pair holds the original computation, while the differential part computes the dot product of the differentials with the derivatives of the function's output w.r.t each input.
Propagation
This step takes a linearized function and propagates information about which instructions are computing a differential and which ones are part of the primal (original) computation.
Assuming first-order differentiation only: The approach will be to mark any instructions that extract the differential from the differential pair as a differential. Then any instruction that uses the differential is itself marked as a differential and so on. The only exception is the call instruction which is either non-differentiable (do nothing) or differentiable and returns a pair (follow the same process)
Here's the above example with propagated type information (we use float.D to denote intermediaries that have been marked as differential, and also expand everything so that each line has a single operation)
DP < float > f_SSA_Proped (DP < float > dpa ,DP < float > dpb ) {bool _b1 = dpa .p > 0 ;if (_b1 ) {float _t1 = dpa .p + dpb .p ;float .D _q1_d = dpa .d ;float .D _q2_d = dpb .d ;float .D _t1_d = _q1_d + _q2_d ;float _t2 = 2.0 * dpa .p ;float .D _q2_d = dpa .d ;float .D _q3_d = 2.0 * dpa .d ;float _q4 = dpa .p ;float .D _q4_d = 0.0 * dpa .p ;float .D _t2_d = _q4_d + _q3_d ;float _t3 = _t2 * dpb .p ;float _q5 = dpb .p ;float .D _q6_d = _q5 * _t2_d ;float .D _q7_d = dpb .d ;float .D _q8_d = _t2 * _q7_d float _t3_d = _q6_d + _q8_d ;float _t4 = _t1 + _t3 ;float .D _t4_d = _t1_d + _t3_d ;return DP < float > (_t4 ,_t4_d ); }else {DP < float > _t1_dp = sqrt_fwd (dpa );float _q1 = _t1_dp .p ;float .D _q1_d = _t1_dp .d ;return DP < float > (_q1 ,_q1_d ); } }
Unzipping
This is a fairly simple process when there is no control flow. We simply move all non-differential instructions to before the first differential instruction.
When there is control flow, we need to be a bit more careful: the key is to replicate the control flow graph once for primal and once for the differential.
Here's the previous example unzipped:
DP < float > f_SSA_Proped (DP < float > dpa ,DP < float > dpb ) {bool _b1 = dpa .p > 0 ;float _t1 ,_t2 ,_q4 ,_t3 ,_q5 ,_t3_d ,_t4 ,_q1 ;if (_b1 ) {_t1 = dpa .p + dpb .p ;_t2 = 2.0 * dpa .p ;_q4 = dpa .p ;_t3 = _t2 * dpb .p ;_q5 = dpb .p ;_t4 = _t1 + _t3 ; }else {_q1 = sqrt_fwd (DP < float > (dpa .p ,0.0 )); }// Note here that we have to 'store' all the intermediaries // _t1, _t2, _q4, _t3, _q5, _t3_d, _t4 and _q1. This is fundamentally // the tradeoff between fwd_mode and rev_mode if (_b1 ) {float .D _q1_d = dpa .d ;float .D _q2_d = dpb .d ;float .D _t1_d = _q1_d + _q2_d ;float .D _q2_d = dpa .d ;float .D _q3_d = 2.0 * dpa .d ;float .D _q4_d = 0.0 * dpa .p ;float .D _t2_d = _q4_d + _q3_d ;float .D _q6_d = _q5 * _t2_d ;float .D _q7_d = dpb .d ;float .D _q8_d = _t2 * _q7_d float .D _t3_d = _q6_d + _q8_d ;float .D _t4_d = _t1_d + _t3_d ;return DP < float > (_t4 ,_t4_d ); }else {DP < float > _t1_dp = sqrt_fwd (dpa );float .D _q1_d = _t1_dp .d ;return DP < float > (_q1 ,_q1_d ); } }
Transposition
Overview
This transposition pass assumes that provided function is linear in it's differentials. It is out of scope of this project to attempt to enforce that constraint for user-defined differential code.
For transposition we walk all differential instructions in reverse starting from the return statement, and apply the following rules:
We'll have an accumulator dictionary Dictionary<IRInst, IRInst> accMap holding assignments for
intermediaries which don't have concrete variables. When we add a pair (A, C) and (A, B) already exists, this will form the pair (A, ADD(C, B)) in the dictionary. (ADD will be replaced with a call to T.dadd for a generic type T)
- If
instis aRETURN(A), add pair(A, d_out)toaccMap - If an instruction is
MUL(P, D)where D is the differential, add pair(D, MUL(P, accMap[this_inst]))toaccMap - If an instruction is
ADD(D1, D2), where both D1 and D2 are differentials (this is the only config that should occur), then add pair(D1, accMap[this_inst])toaccMap - If an instruction is
CALL(f_fwd, (P1, D1), (P2, D2), ...), create variables D1v, D2v, ... for D1, D2, ..., then replace withCALL(f_rev, (P1, D1v), (P2, D2v), ..., accMap[this_inst]), and finally add pairs(D1, LOAD[D1v]), (D2, LOAD[D2v]), ...toaccMap
void f_SSA_Rev (inout DP < float > dpa ,inout DP < float > dpb ,float dout ) {bool _b1 = dpa .p > 0 ;float _t1 ,_t2 ,_q4 ,_t3 ,_q5 ,_t3_d ,_t4 ,_q1 ;if (_b1 ) {_t1 = dpa .p + dpb .p ;_t2 = 2.0 * dpa .p ;_q4 = dpa .p ;_t3 = _t2 * dpb .p ;_q5 = dpb .p ;_t4 = _t1 + _t3 ; }else {_q1 = sqrt_fwd (DP < float > (dpa .p ,0.0 )); }// Note here that we have to 'store' all the intermediaries // _t1, _t2, _q4, _t3, _q5, _t3_d, _t4 and _q1. This is fundamentally // the tradeoff between fwd_mode and rev_mode if (_b1 ) {float .D _t4_rev = d_out ;float .D _t1_rev = _t4_rev ;float .D _t3_rev = _t4_rev ;float .D _q8_rev = _t3_rev ;float .D _q6_rev = _t3_rev ;float .D _q7_rev = _t2 * _q8_rev ;dpb .d += _q7_rev ;float .D _t2_rev = _q5 * _q6_rev ;float .D _q4_rev = _t2_rev ;float .D _q3_rev = _t2_rev ;dpa .d += 2.0 * _q3_rev ;float .D _q1_rev = _t1_rev ;float .D _q2_rev = _t1_rev ;dpb .d += _q2_rev ;dpa .d += _q1_rev ; }else {_q1_rev = d_out ;DP < float > dpa_copy ;sqrt_rev (dpa_copy ,_q1_rev );dpa .d += dpa_copy .d ; } }
1Reverse Mode Autodiff (Out of Date) 2================================== 3 4 5This document serves as a design reference for reverse-mode auto-diff in the Slang compiler. 6 7## Reverse-Mode Passes 8 9Rather than implementing reverse-mode as a separate pass, Slang implements this as a series of independent passes: 10 11If a function needs a reverse-mode version generated: 12- *Linearize* the function, and all dependencies. 13- *Propagate* differential types through the linearized code. 14- *Unzip* by moving primal insts to before differential insts. 15- *Transpose* the differential insts. 16 17 18## Linearization (Forward-mode) 19 20### Overview 21(This is a incomplete section. More details coming soon) 22 23Consider an arbitrary function `float f(float a, float b, float c, ..., z)` which takes in N inputs and generates one output `y`. Linearization aims to generate the first-order Taylor expansion of f about _all_ of it's inputs. 24 25Mathematically, the forward derivative `fwd_f` represents `df/da * (a_0 - a) + df/db * (b_0 - b) + ...`, where `a_0` is the value at which the Taylor expansion was produced. The quantity `a_0 - a` is known as the 'differential' (for brevity we'll denote them da, db, dc, etc..), and there is at-most one differential per input. 26 27Thus, the new function's signature should be `fwd_f(float a, float da, float b, float db, float c, float dc, ...)`. For simplicity, we'll use *pairs* instead of interleaving the original and differential parameters. We use the intrinsic `DifferentialPair<T>` (or for short: `DP<T>`) to denote this. 28 29The signature we use is then `fwd_f(DP<float> a, DP<float> b, DP<float> c)` 30 31An example of linearization: 32``` C 33 34float f(float a, float b) 35{ 36if (a > 0) 37{ 38return a + b + 2.0 * a * b; 39} 40else 41{ 42return sqrt(a); 43} 44} 45``` 46 47We'll write out the SSA form of this function. 48 49``` C 50float f_SSA(float a, float b) 51{ 52bool _b1 = a > 0; 53if (_b1) 54{ 55float _t1 = a + b; 56float _t2 = 2.0 * a; 57float _t3 = _t2 * b; 58float _t4 = _t1 + _t3; 59 60return _t4; 61} 62else 63{ 64float _t1 = sqrt(a); 65return _t1; 66} 67} 68 69DP<float> f_SSA(DP<float> dpa, DP<float> dpb) 70{ 71 72bool _b1 = dpa.p > 0; 73if (_b1) 74{ 75float _t1 = dpa.p + dpb.p; 76float _t1_d = dpa.d + dpb.d; 77 78float _t2 = 2.0 * dpa.p; 79float _t2_d = 0.0 * dpa.p + 2.0 * dpa.d; 80 81float _t3 = _t2 * dpb.p; 82float _t3_d = _t2_d * dpb.p + _t2 * dpb.d; 83 84float _t4 = _t1 + _t3; 85float _t4_d = _t1_d + _t3_d; 86 87return DP<float>(_t4, _t4_d); 88} 89else 90{ 91DP<float> _t1_dp = sqrt_fwd(dpa); 92return DP<float>(_t1_dp.p, _t1_dp.d); 93} 94} 95 96``` 97 98In the result, the primal part of the pair holds the original computation, while the differential part computes the dot product of the differentials with the derivatives of the function's output w.r.t each input. 99 100 101## Propagation 102 103This step takes a linearized function and propagates information about which instructions are computing a differential and which ones are part of the primal (original) computation. 104 105Assuming first-order differentiation only: 106The approach will be to mark any instructions that extract the differential from the differential pair as a differential. Then any instruction that uses the differential is itself marked as a differential and so on. The only exception is the call instruction which is either non-differentiable (do nothing) or differentiable and returns a pair (follow the same process) 107 108 109Here's the above example with propagated type information (we use float.D to denote intermediaries that have been marked as differential, and also expand everything so that each line has a single operation) 110 111``` C 112 113DP<float> f_SSA_Proped(DP<float> dpa, DP<float> dpb) 114{ 115bool _b1 = dpa.p > 0; 116if (_b1) 117{ 118float _t1 = dpa.p + dpb.p; 119120 float.D _q1_d = dpa.d; 121float.D _q2_d = dpb.d; 122 123float.D _t1_d = _q1_d + _q2_d; 124 125float _t2 = 2.0 * dpa.p; 126127 float.D _q2_d = dpa.d; 128float.D _q3_d = 2.0 * dpa.d; 129 130float _q4 = dpa.p; 131float.D _q4_d = 0.0 * dpa.p; 132 133float.D _t2_d = _q4_d + _q3_d; 134 135float _t3 = _t2 * dpb.p; 136 137float _q5 = dpb.p; 138float.D _q6_d = _q5 * _t2_d; 139 140float.D _q7_d = dpb.d; 141float.D _q8_d = _t2 * _q7_d 142 143float _t3_d = _q6_d + _q8_d; 144 145float _t4 = _t1 + _t3; 146 147float.D _t4_d = _t1_d + _t3_d; 148 149return DP<float>(_t4, _t4_d); 150} 151else 152{ 153DP<float> _t1_dp = sqrt_fwd(dpa); 154 155float _q1 = _t1_dp.p; 156float.D _q1_d = _t1_dp.d; 157 158return DP<float>(_q1, _q1_d); 159} 160} 161 162``` 163 164## Unzipping 165 166 167This is a fairly simple process when there is no control flow. We simply move all non-differential instructions to before the first differential instruction. 168 169When there is control flow, we need to be a bit more careful: the key is to *replicate* the control flow graph once for primal and once for the differential. 170 171Here's the previous example unzipped: 172 173 174``` C 175 176DP<float> f_SSA_Proped(DP<float> dpa, DP<float> dpb) 177{ 178bool _b1 = dpa.p > 0; 179 180float _t1, _t2, _q4, _t3, _q5, _t3_d, _t4, _q1; 181 182if (_b1) 183{ 184_t1 = dpa.p + dpb.p; 185186 _t2 = 2.0 * dpa.p; 187188 _q4 = dpa.p; 189190 _t3 = _t2 * dpb.p; 191 192_q5 = dpb.p; 193 194_t4 = _t1 + _t3; 195 196} 197else 198{ 199 200_q1 = sqrt_fwd(DP<float>(dpa.p, 0.0)); 201} 202 203// Note here that we have to 'store' all the intermediaries 204// _t1, _t2, _q4, _t3, _q5, _t3_d, _t4 and _q1. This is fundamentally 205// the tradeoff between fwd_mode and rev_mode 206 207if (_b1) 208{ 209float.D _q1_d = dpa.d; 210float.D _q2_d = dpb.d; 211 212float.D _t1_d = _q1_d + _q2_d; 213 214float.D _q2_d = dpa.d; 215float.D _q3_d = 2.0 * dpa.d; 216 217float.D _q4_d = 0.0 * dpa.p; 218 219float.D _t2_d = _q4_d + _q3_d; 220 221float.D _q6_d = _q5 * _t2_d; 222 223float.D _q7_d = dpb.d; 224float.D _q8_d = _t2 * _q7_d 225 226float.D _t3_d = _q6_d + _q8_d; 227 228float.D _t4_d = _t1_d + _t3_d; 229 230return DP<float>(_t4, _t4_d); 231} 232else 233{ 234DP<float> _t1_dp = sqrt_fwd(dpa); 235 236float.D _q1_d = _t1_dp.d; 237 238return DP<float>(_q1, _q1_d); 239} 240} 241 242``` 243 244## Transposition 245 246### Overview 247 248This transposition pass _assumes_ that provided function is linear in it's differentials. 249It is out of scope of this project to attempt to enforce that constraint for user-defined differential code. 250 251For transposition we walk all differential instructions in reverse starting from the return statement, and apply the following rules: 252 253We'll have an accumulator dictionary `Dictionary<IRInst, IRInst> accMap` holding assignments for 254intermediaries which don't have concrete variables. When we add a pair (A, C) and (A, B) already exists, this will form the pair (A, ADD(C, B)) in the dictionary. (ADD will be replaced with a call to `T.dadd` for a generic type T) 255 256- If `inst` is a `RETURN(A)`, add pair `(A, d_out)` to `accMap` 257- If an instruction is `MUL(P, D)` where D is the differential, add pair `(D, MUL(P, accMap[this_inst]))` to `accMap` 258- If an instruction is `ADD(D1, D2)`, where both D1 and D2 are differentials (this is the only config that should occur), then add pair `(D1, accMap[this_inst])` to `accMap` 259- If an instruction is `CALL(f_fwd, (P1, D1), (P2, D2), ...)`, create variables D1v, D2v, ... for D1, D2, ..., then replace with `CALL(f_rev, (P1, D1v), (P2, D2v), ..., accMap[this_inst])`, and finally add pairs `(D1, LOAD[D1v]), (D2, LOAD[D2v]), ...` to `accMap` 260 261``` C 262 263void f_SSA_Rev(inout DP<float> dpa, inout DP<float> dpb, float dout) 264{ 265bool _b1 = dpa.p > 0; 266 267float _t1, _t2, _q4, _t3, _q5, _t3_d, _t4, _q1; 268 269if (_b1) 270{ 271_t1 = dpa.p + dpb.p; 272273 _t2 = 2.0 * dpa.p; 274275 _q4 = dpa.p; 276277 _t3 = _t2 * dpb.p; 278 279_q5 = dpb.p; 280 281_t4 = _t1 + _t3; 282 283} 284else 285{ 286 287_q1 = sqrt_fwd(DP<float>(dpa.p, 0.0)); 288} 289 290// Note here that we have to 'store' all the intermediaries 291// _t1, _t2, _q4, _t3, _q5, _t3_d, _t4 and _q1. This is fundamentally 292// the tradeoff between fwd_mode and rev_mode 293 294if (_b1) 295{ 296 297float.D _t4_rev = d_out; 298 299float.D _t1_rev = _t4_rev; 300float.D _t3_rev = _t4_rev; 301 302float.D _q8_rev = _t3_rev; 303float.D _q6_rev = _t3_rev; 304 305float.D _q7_rev = _t2 * _q8_rev; 306 307dpb.d += _q7_rev; 308 309float.D _t2_rev = _q5 * _q6_rev; 310 311float.D _q4_rev = _t2_rev; 312float.D _q3_rev = _t2_rev; 313 314dpa.d += 2.0 * _q3_rev; 315 316float.D _q1_rev = _t1_rev; 317float.D _q2_rev = _t1_rev; 318 319dpb.d += _q2_rev; 320dpa.d += _q1_rev; 321} 322else 323{ 324_q1_rev = d_out; 325 326DP<float> dpa_copy; 327sqrt_rev(dpa_copy, _q1_rev); 328 329dpa.d += dpa_copy.d; 330} 331} 332 333```