yum-mirror/slang

Making it easier to work with shaders

git clone https://git.yummers.dev/yum-mirror/slang

Bruce Mitchenerdocs: Reduce typo count (#5671)c3557978c

master
8.8 KiB333 linesraw

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 inst is a RETURN(A), add pair (A, d_out) to accMap
  • If an instruction is MUL(P, D) where D is the differential, add pair (D, MUL(P, accMap[this_inst])) to accMap
  • 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
  • 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
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{
36    if (a > 0)
37    {
38        return a + b + 2.0 * a * b;
39    }
40    else
41    {
42        return 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{
52    bool _b1 = a > 0;
53    if (_b1)
54    {
55        float _t1 = a + b;
56        float _t2 = 2.0 * a;
57        float _t3 = _t2 * b;
58        float _t4 = _t1 + _t3;
59
60        return _t4;
61    }
62    else
63    {
64        float _t1 = sqrt(a);
65        return _t1;
66    }
67}
68
69DP<float> f_SSA(DP<float> dpa, DP<float> dpb)
70{
71
72    bool _b1 = dpa.p > 0;
73    if (_b1)
74    {
75        float _t1 = dpa.p + dpb.p;
76        float _t1_d = dpa.d + dpb.d;
77
78        float _t2 = 2.0 * dpa.p;
79        float _t2_d = 0.0 * dpa.p + 2.0 * dpa.d;
80
81        float _t3 = _t2 * dpb.p;
82        float _t3_d = _t2_d * dpb.p + _t2 * dpb.d;
83
84        float _t4 = _t1 + _t3;
85        float _t4_d = _t1_d + _t3_d;
86
87        return DP<float>(_t4, _t4_d);
88    }
89    else
90    {
91        DP<float> _t1_dp = sqrt_fwd(dpa);
92        return 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{
115    bool _b1 = dpa.p > 0;
116    if (_b1)
117    {
118        float _t1 = dpa.p + dpb.p;
119        
120        float.D _q1_d = dpa.d;
121        float.D _q2_d = dpb.d;
122
123        float.D _t1_d = _q1_d + _q2_d;
124
125        float _t2 = 2.0 * dpa.p;
126        
127        float.D _q2_d = dpa.d;
128        float.D _q3_d = 2.0 * dpa.d;
129
130        float _q4 = dpa.p;
131        float.D _q4_d = 0.0 * dpa.p;
132
133        float.D _t2_d = _q4_d + _q3_d;
134
135        float _t3 = _t2 * dpb.p;
136
137        float _q5 = dpb.p;
138        float.D _q6_d = _q5 * _t2_d;
139
140        float.D _q7_d = dpb.d;
141        float.D _q8_d = _t2 * _q7_d
142
143        float _t3_d = _q6_d + _q8_d;
144
145        float _t4 = _t1 + _t3;
146
147        float.D _t4_d = _t1_d + _t3_d;
148
149        return DP<float>(_t4, _t4_d);
150    }
151    else
152    {
153        DP<float> _t1_dp = sqrt_fwd(dpa);
154
155        float _q1 = _t1_dp.p;
156        float.D _q1_d = _t1_dp.d;
157
158        return 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{
178    bool _b1 = dpa.p > 0;
179
180    float _t1, _t2, _q4, _t3, _q5, _t3_d, _t4, _q1;
181
182    if (_b1)
183    {
184        _t1 = dpa.p + dpb.p;
185        
186        _t2 = 2.0 * dpa.p;
187        
188        _q4 = dpa.p;
189        
190        _t3 = _t2 * dpb.p;
191
192        _q5 = dpb.p;
193
194        _t4 = _t1 + _t3;
195
196    }
197    else
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
207    if (_b1)
208    {
209        float.D _q1_d = dpa.d;
210        float.D _q2_d = dpb.d;
211
212        float.D _t1_d = _q1_d + _q2_d;
213
214        float.D _q2_d = dpa.d;
215        float.D _q3_d = 2.0 * dpa.d;
216
217        float.D _q4_d = 0.0 * dpa.p;
218
219        float.D _t2_d = _q4_d + _q3_d;
220
221        float.D _q6_d = _q5 * _t2_d;
222
223        float.D _q7_d = dpb.d;
224        float.D _q8_d = _t2 * _q7_d
225
226        float.D _t3_d = _q6_d + _q8_d;
227
228        float.D _t4_d = _t1_d + _t3_d;
229
230        return DP<float>(_t4, _t4_d);
231    }
232    else
233    {
234        DP<float> _t1_dp = sqrt_fwd(dpa);
235
236        float.D _q1_d = _t1_dp.d;
237
238        return 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{
265    bool _b1 = dpa.p > 0;
266
267    float _t1, _t2, _q4, _t3, _q5, _t3_d, _t4, _q1;
268
269    if (_b1)
270    {
271        _t1 = dpa.p + dpb.p;
272        
273        _t2 = 2.0 * dpa.p;
274        
275        _q4 = dpa.p;
276        
277        _t3 = _t2 * dpb.p;
278
279        _q5 = dpb.p;
280
281        _t4 = _t1 + _t3;
282
283    }
284    else
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
294    if (_b1)
295    {
296
297        float.D _t4_rev = d_out;
298
299        float.D _t1_rev = _t4_rev;
300        float.D _t3_rev = _t4_rev;
301
302        float.D _q8_rev = _t3_rev;
303        float.D _q6_rev = _t3_rev;
304
305        float.D _q7_rev = _t2 * _q8_rev;
306
307        dpb.d += _q7_rev;
308
309        float.D _t2_rev = _q5 * _q6_rev;
310
311        float.D _q4_rev = _t2_rev;
312        float.D _q3_rev = _t2_rev;
313
314        dpa.d += 2.0 * _q3_rev;
315
316        float.D _q1_rev = _t1_rev;
317        float.D _q2_rev = _t1_rev;
318
319        dpb.d += _q2_rev;
320        dpa.d += _q1_rev;
321    }
322    else
323    {
324        _q1_rev = d_out;
325
326        DP<float> dpa_copy;
327        sqrt_rev(dpa_copy, _q1_rev);
328
329        dpa.d += dpa_copy.d;
330    }
331}
332
333```