carray-jit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +10 -0
  3. data/CHANGELOG.md +84 -0
  4. data/LICENSE +21 -0
  5. data/README.md +88 -0
  6. data/bin/carray-jit +194 -0
  7. data/carray-jit.gemspec +41 -0
  8. data/docs/00_Introduction.md +40 -0
  9. data/docs/01_GettingStarted.md +80 -0
  10. data/docs/02_KernelShapes.md +397 -0
  11. data/docs/03_SupportedFeatures.md +595 -0
  12. data/docs/04_Compiling.md +234 -0
  13. data/docs/05_DesignNotes.md +136 -0
  14. data/docs/06_Cheatsheet.md +177 -0
  15. data/examples/README.md +56 -0
  16. data/examples/applications/game_of_life.rb +161 -0
  17. data/examples/applications/heat_equation.rb +117 -0
  18. data/examples/applications/kepler.rb +178 -0
  19. data/examples/applications/mandelbrot.rb +151 -0
  20. data/examples/applications/moving_average.rb +124 -0
  21. data/examples/applications/partial_sums.rb +141 -0
  22. data/examples/applications/point_cloud.rb +110 -0
  23. data/examples/applications/quicksort.rb +118 -0
  24. data/examples/applications/recursion.rb +121 -0
  25. data/examples/applications/relaxation.rb +115 -0
  26. data/examples/applications/sensor_gaps.rb +118 -0
  27. data/examples/applications/sieve.rb +95 -0
  28. data/examples/applications/sobel_edges.rb +80 -0
  29. data/examples/features/01_element_wise.rb +69 -0
  30. data/examples/features/02_stencil.rb +40 -0
  31. data/examples/features/03_recurrence.rb +50 -0
  32. data/examples/features/04_thomas.rb +81 -0
  33. data/examples/features/05_reduction.rb +90 -0
  34. data/examples/features/06_jit_contract.rb +58 -0
  35. data/examples/features/07_masks.rb +55 -0
  36. data/examples/features/08_views.rb +46 -0
  37. data/examples/features/09_inspecting.rb +55 -0
  38. data/examples/features/10_complex.rb +107 -0
  39. data/examples/features/11_c_functions.rb +260 -0
  40. data/examples/features/12_sweep.rb +139 -0
  41. data/examples/features/13_cscalar.rb +80 -0
  42. data/examples/features/14_stencil_window.rb +106 -0
  43. data/examples/features/15_loops.rb +148 -0
  44. data/examples/features/16_raising.rb +69 -0
  45. data/ext/carray_jit_access/carray_jit_access.c +460 -0
  46. data/ext/carray_jit_access/extconf.rb +8 -0
  47. data/lib/carray/jit/analyzer.rb +1847 -0
  48. data/lib/carray/jit/block_reader.rb +139 -0
  49. data/lib/carray/jit/c_function.rb +777 -0
  50. data/lib/carray/jit/c_generator.rb +2305 -0
  51. data/lib/carray/jit/compiler.rb +468 -0
  52. data/lib/carray/jit/errors.rb +37 -0
  53. data/lib/carray/jit/expression.rb +202 -0
  54. data/lib/carray/jit/kernel.rb +509 -0
  55. data/lib/carray/jit/node.rb +573 -0
  56. data/lib/carray/jit/sweep.rb +97 -0
  57. data/lib/carray/jit/type_assignment.rb +811 -0
  58. data/lib/carray/jit/version.rb +5 -0
  59. data/lib/carray/jit.rb +1210 -0
  60. metadata +139 -0
@@ -0,0 +1,397 @@
1
+ # The shapes a kernel takes
2
+
3
+ ## `jit_each` and `jit_map`, when nothing reaches a neighbour
4
+
5
+ Naming an index is what lets a kernel reach a neighbouring cell, and reaching a neighbouring cell is what makes the range and the direction matter. A computation that reaches none needs none of that, so it names no index, takes no extent, and has a method of its own:
6
+
7
+ ```ruby
8
+ CArray.jit_each { out = a + b * c }
9
+ ```
10
+
11
+ That right-hand side is what the expression already means in CArray, written exactly as it would be written anywhere else. What changes is not the expression but how it runs: at the cell, in one pass, instead of three passes with two arrays in between.
12
+
13
+ The assignment is Ruby's own. Every name in the block is a cell -- the loop is this compiler's and is not written here -- so `out = ...` writes the cell of the array `out` names outside, and a name that is not an array there stays a local of the block's. `out[] = ...` was the spelling from when the block had to run as Ruby, `[]=` being the only way Ruby has to say "the whole array"; the block is read rather than run, so it is refused now and says this. `a[]` is still accepted on the right, and says what the bare name says.
14
+
15
+ #### A `CScalar` is a value with a home
16
+
17
+ `CScalar` is the one-cell `CArray` it subclasses, minus the index: `s[]` is the value, and `s[] = ...` puts one back. CArray's own operators read that one cell for every cell of everything else, and so does this:
18
+
19
+ ```ruby
20
+ gain = CScalar.double() { 2.0 }
21
+ CArray.jit_each { out = signal * gain } # gain read at every cell
22
+ CArray.jit_each { gain = gain + 0.5 } # and written like any cell
23
+ ```
24
+
25
+ An indexed kernel reaches it too, and there the missing index is the whole point: `s[]` and a bare `s` both mean the value, because there is no axis to walk and so no index to write.
26
+
27
+ ```ruby
28
+ CArray.jit_for(n) { |i| out[i] = signal[i] * gain[] }
29
+ ```
30
+
31
+ The two routes differ in how they get there. `jit_each` stretches it as it stretches a one-cell `CArray` -- a stride of zero -- while `jit_for` reads it where it lies. They compute the same thing, and `s[0]` keeps working in both, since it is still the one-cell array it is. Writing a wider expression into one is refused, with the shapes named, exactly as CArray refuses it.
32
+
33
+ The other method is the same block with its value asked for:
34
+
35
+ ```ruby
36
+ sums = CArray.jit_map { a + b }
37
+ ```
38
+
39
+ The last statement is what every cell of the result gets, so there is no output array to name and the result comes back typed from that value rather than from the arrays. The name is the whole of the difference: the block is read the same way, and what it may do is the same.
40
+
41
+ An assignment is a statement with a value -- Ruby's rule, not a special case here -- so `CArray.jit_map { out = a + b }` writes `out` and hands the same value back. Which is why the two methods are split by what returns rather than by what is written: writing is the block's business either way.
42
+
43
+ It is the same shape of block `CArray.jit_function` takes, and not the same thing. There, the loop belongs to whoever calls the function, so the body is reached through a pointer and may close over nothing. Here the loop is this compiler's and the body is inlined into it, so it costs no call and may reach a captured value, a `Math` function or a bound C function like any other kernel body.
44
+
45
+ #### Who drives the loop
46
+
47
+ An element-wise pass is the one shape that is a *sweep* -- nothing reaches a neighbour, nothing chooses an order -- so CArray can drive it instead. Where this CArray has `ca_call_cslab` (3.0.1 and later), `jit_each` hands it the compiled body and lets it acquire the operands.
48
+
49
+ The body is the same either way. The sweep entry point is a wrapper that calls the same kernel with the chunk as its bounds, so what changes is who opens the arrays, not what they compute.
50
+
51
+ What that is worth is memory. An operand CArray cannot walk in place -- a gather, a lazy array -- is re-gathered 32KB at a time by the sweep, where the tiers here move the whole box the kernel touches, and for an element-wise pass that box is the whole array. Over two million doubles with a gather view as an operand, both take about 1.3 ms and one of them holds sixteen megabytes of scratch while the other holds thirty-two kilobytes. With entity operands the two are indistinguishable, so there is nothing to weigh.
52
+
53
+ A **strided view** is the case where there is. A column, a transpose, every other cell: the tiers address those in place, with no gather and no scatter, so the sweep's re-gather has no whole-array copy to save you from and costs what it costs. Over two million doubles, `0.42 ns` a cell walked in place against `4.7 ns` swept, and no scratch either way. So the driver is chosen on the operands as well as the shape -- the sweep runs where some operand has to be moved whatever happens, and where none of them does.
54
+
55
+ The rank does not stand in the way, and the arrays are not reshaped to get it out of the way. CArray's acquire reads an operand's element count and element size and never looks at its shape, so a chunk is already a flat run of cells; what has to be flat is the *kernel*, and it is compiled at rank one over the same cells rather than as a nest over the axes. One such kernel then serves whatever rank it is given, because the rank has left the loop.
56
+
57
+ Four things keep the driver here. An operand that is a **strided view**, for the reason just measured: it is walked in place here, and re-gathered there for nothing. A **stretched operand**, which is the one thing that cannot be flattened: broadcasting arrives as a stride of zero on an axis, and a flat run has no axes, so cell k of the output would stop lining up with cell k of the operand. A body that **asks about a mask**, because a chunk carries no per-cell mask to ask it of -- CArray ORs and propagates the masks, but `a[i] == UNDEF` is a question about one cell. And a CArray **without the family** at all, which is asked for by symbol rather than assumed, so an older one falls back without the caller hearing about it.
58
+
59
+ `jit_for` never goes this way, and that is not a limitation: a kernel that names an index reaches neighbours, chooses an order and runs inner loops, none of which a chunked walk can offer. The two names mark the same line.
60
+
61
+ The two methods are one machinery and two names, and the names are the point: "cell" is a position, which is what a block names when it can reach the positions around it, and "element" is what CArray calls the same work done cell by cell with no neighbour in it. Each method's block is turned away by the other rather than quietly reinterpreted.
62
+
63
+ Shapes are broadcast the way CArray broadcasts them, which costs nothing here: a stretched axis comes back as a stride of zero, and the kernel addresses by stride anyway. The rank comes from the arrays rather than from the block, so one expression serves whatever shape it is given.
64
+
65
+ CArray has its own answer to the intermediate arrays: `CArray.fuse { a + b * c }` builds the expression as a structure rather than as arrays, reading the names from where the block was written. Walked by CArray that holds the intermediates to one buffer per level of the expression's right spine rather than removing them -- see the table in [Four ways](01_GettingStarted.md#four-ways-to-compute-an-expression-and-what-separates-them) -- and it leaves the passes alone: each operation is still its own walk over the data.
66
+
67
+ Where this gem is installed, though, `fuse` does not walk: it is registered as CArray's expression evaluator, and a fused expression is compiled into one kernel like any other. So there are two fuse rows, and the difference between them is this gem:
68
+
69
+ ```
70
+ n = 4,000,000
71
+
72
+ out = a + b * c
73
+ a + b * c 5.2 ms 1.30 ns/element
74
+ CArray.fuse, walked 3.9 ms 0.99 ns/element 1.32x
75
+ CArray.fuse, compiled here 2.9 ms 0.74 ns/element 1.76x
76
+ jit_each { out = ... } 1.3 ms 0.32 ns/element 4.11x
77
+
78
+ out = (a + b) * (c - a) + b * c - a
79
+ plain 17.7 ms 4.42 ns/element
80
+ CArray.fuse, walked 8.5 ms 2.13 ns/element 2.08x
81
+ CArray.fuse, compiled here 2.6 ms 0.66 ns/element 6.74x
82
+ jit_each { out = ... } 1.2 ms 0.30 ns/element 14.67x
83
+ ```
84
+
85
+ Walked, `fuse` earns more as the expression grows -- there are more intermediates it is not allocating -- but it still grows, because the passes are still there. Compiled, neither row grows: the expression is read once whatever it says.
86
+
87
+ What is left between the last two is not speed. `jit_each` is handed the array to write into, where `fuse` returns the expression and allocates when it is asked for an array; and a block can say what an expression cannot -- a cell reaching its neighbours, a loop written out, a reduction. Where an expression over whole arrays is the whole of it, `fuse` is what to write, and this gem makes it faster without being named.
88
+
89
+ ## Extents, steps and subscripts
90
+
91
+ ### The extent says which cells, and which way
92
+
93
+ Which way an axis runs is the extent's to say, and the kernel runs the order it is given. Where a kernel reads a cell it will later write, that order is the answer: reading behind the cell being written propagates forwards, reading ahead of it propagates backwards, and both together mean cells already passed hold new values while cells not yet reached hold old ones. None of these is ambiguous, and each is what the same Ruby loop leaves behind.
94
+
95
+ So the direction is **written at the call site**, because that is the only place a reader sees it:
96
+
97
+ ```ruby
98
+ CArray.jit_for(1...n) { |i| # upward: reads cc[i-1]
99
+ denominator = b[i] - a[i] * cc[i-1]
100
+ cc[i] = c[i] / denominator
101
+ dd[i] = (d[i] - a[i] * dd[i-1]) / denominator
102
+ }
103
+
104
+ CArray.jit_for((n-2).step(0, -1)) { |i| # downward: reads x[i+1]
105
+ x[i] = dd[i] - cc[i] * x[i+1]
106
+ }
107
+ ```
108
+
109
+ A plain `Range` means upward, and `step` is how the other direction is said. Neither is checked against the body: a kernel is not asked to justify the order it was handed, any more than a `for` loop in C or a `do` loop in Fortran is. What *is* checked is that every cell the loop would touch exists -- see below.
110
+
111
+ `(n-2)..0` would read better than `(n-2).step(0, -1)`, and is not accepted: Ruby gives that Range no elements at all, so the same expression written as a Ruby loop would silently do nothing. `step` is the spelling Ruby actually iterates backwards.
112
+
113
+ The forward sweep above also shows why a kernel writes as many arrays as it likes: `cc` and `dd` share a denominator, and splitting them into two loops would compute it twice.
114
+
115
+ ### Skipping cells
116
+
117
+ An extent may step by more than one, and an offset still means what it means in Ruby -- `a[i-1]` is the cell at index i-1, whether or not the loop is one that writes it:
118
+
119
+ ```ruby
120
+ CArray.jit_for((2...n).step(2)) { |i| a[i] = a[i-1] + b[i] }
121
+ CArray.jit_for((2...n).step(2)) { |i| a[i] = a[i-2] * 3.0 }
122
+ ```
123
+
124
+ With a step of two the loop writes only every other cell, so the first reads cells this loop never touches and the second reads its own previous iterate. Both are what the same Ruby loop over the same sequence would do.
125
+
126
+ ### Ranges are checked, not guessed
127
+
128
+ Because the range is given rather than inferred, the extents, the range and the offsets are all known before anything runs, and a kernel that would reach outside its array says so rather than reaching:
129
+
130
+ ```ruby
131
+ CArray.jit_for(0...8) { |i| values[i] = values[i-1] * 2.0 }
132
+ #=> CArray::JIT::Unsupported: `values` is indexed at `values[i - 1]`,
133
+ # so the range on `i` cannot start at 0
134
+ ```
135
+
136
+ This is the check that has to be here. Ruby raises on an index past the end and CArray does too; C reads whatever is there, or writes it. Everything else about the order is the caller's to say.
137
+
138
+ An inferred range would have quietly started at 1 instead, and a kernel that meant to touch cell 0 would never say so.
139
+
140
+ ### Subscripts the kernel works out
141
+
142
+ A subscript is usually an index and a constant, and then every cell the kernel touches is known before it runs. But it may also be a value the kernel works out -- a cell of another array, or a local -- and then it cannot be, so the check moves to the access:
143
+
144
+ ```ruby
145
+ CArray.jit_for(n) { |i| result[i] = table[index[i]] } # a gather
146
+
147
+ CArray.jit_for(n) { |i| # a histogram
148
+ bin = value[i].floor
149
+ histogram[bin] = histogram[bin] + 1
150
+ }
151
+ ```
152
+
153
+ Out of range raises `IndexError`, at the cell the Ruby loop would have raised at: the loop stops there rather than running on and reporting at the end. A read outside the array reads cell zero and reports, which changes nothing that is kept; a write outside it is not made at all, since a report that arrived after the damage would be no use.
154
+
155
+ This is the one thing about a kernel that is not settled in advance, and it costs what that implies: a compare per access, and no vectorising the expression it is in. Kernels without such a subscript are untouched -- they keep both loops and the check that costs nothing.
156
+
157
+ A view that has to be reached a box at a time still takes one, but the box becomes the whole view: a computed index could reach any cell of it. That costs what copying the view would have cost, which is what the caller would otherwise have been told to write by hand.
158
+
159
+ One restriction remains. A write is either the cell the loop is on or a computed one -- never the cell one along, which is the cell another iteration writes.
160
+
161
+ ## Stencils
162
+
163
+ A stencil is every cell from the ones around it, and `jit_stencil` is the spelling where the loop is implied:
164
+
165
+ ```ruby
166
+ smoothed = CArray.jit_stencil(image) { |a|
167
+ 0.25 * (a[-1, 0] + a[1, 0] + a[0, -1] + a[0, 1])
168
+ }
169
+ ```
170
+
171
+ The arrays are given rather than closed over, and the block's parameters are windows onto them, in that order. `a[0, 0]` is the cell the loop is on and `a[-1, 1]` its neighbour, so the offsets are the stencil as it is drawn. The block's value is what the cell gets, as `jit_map`'s is, and what comes back is an array of the same shape.
172
+
173
+ The offsets are written out — a literal, or arithmetic over literals — because the radius has to be known before the loop runs; see [what is not a stencil](#what-is-not-a-stencil). The weights need not be: `w[-1] * coef[0] + w[0] * coef[1]` reads those from an array like any other captured value.
174
+
175
+ Written with the indices named, the same thing is [`jit_for`](#extents-steps-and-subscripts)'s:
176
+
177
+ ```ruby
178
+ rows, columns = image.dim
179
+ smoothed = CArray.double(rows, columns)
180
+ CArray.jit_for(1...(rows-1), 1...(columns-1)) { |i, j|
181
+ smoothed[i, j] = 0.25 * (image[i-1, j] + image[i+1, j] +
182
+ image[i, j-1] + image[i, j+1])
183
+ }
184
+ ```
185
+
186
+ What the window is for is not the four lines. It is the border.
187
+
188
+ ### The border is an argument
189
+
190
+ With the indices named, what `image[i-1, j]` means at `i = 0` has nowhere to be said. So the extents say it by not going there, and the border keeps whatever the output array held — zeros, usually, which cannot be told from zeros that were computed. A window has nowhere to write an index and so has somewhere to put the question:
191
+
192
+ ```ruby
193
+ CArray.jit_stencil(image, border: :mask) # the cell is UNDEF (default)
194
+ CArray.jit_stencil(image, border: :skip) # the cell is left as found
195
+ CArray.jit_stencil(image, border: :zero) # a read outside gives 0
196
+ CArray.jit_stencil(image, border: :clamp) # a read outside gives the nearest cell
197
+ CArray.jit_stencil(image, border: :wrap) # a read outside comes back the other side
198
+ ```
199
+
200
+ The default is `:mask` because CArray can say "not computed", and that is what those cells are. `:skip` is the older spelling's behaviour, for when the border is yours to fill.
201
+
202
+ The other three are answers about the read rather than about the cell, so the border cells are computed after all — `:wrap` is what makes a Game of Life board a torus, and `:clamp` what an image filter usually wants at the edge.
203
+
204
+ ### What it costs
205
+
206
+ Two loops, not one with a question in it. The interior is the loop the written-out spelling compiles to, with nothing about the border in it; the frame is walked afterwards, by the same statements with the rule woven into their reads. Over a 2000x2000 five-point stencil:
207
+
208
+ ```
209
+ jit_for over the interior, by hand 1.13 ms 0.28 ns/cell
210
+ jit_stencil, border: :skip 1.13 ms 0.28 ns/cell
211
+ jit_stencil, border: :clamp 1.19 ms 0.30 ns/cell
212
+ jit_stencil, border: :wrap 1.20 ms 0.30 ns/cell
213
+ the same clamp written inside one loop 3.20 ms 0.80 ns/cell
214
+ ```
215
+
216
+ The window costs nothing to run: the interior is the same loop, and the same answer bit for bit. The border costs about five per cent, because the frame is 0.2% of the cells — and writing the same rule into the one loop costs 2.7x, because then every cell pays for what only the frame needed.
217
+
218
+ ### Several arrays, and everything else
219
+
220
+ Each array given gets a window, in the order the block names them; the names shadow whatever they hold outside, as `CArray.fuse`'s do.
221
+
222
+ ```ruby
223
+ CArray.jit_stencil(u, k) { |u, k| u[0,0] + k[0,0] * (u[-1,0] + u[1,0] - 2.0*u[0,0]) }
224
+ ```
225
+
226
+ An array the block closed over rather than was given has no window, and is read at the cell — what a bare name means wherever the loop is this compiler's. A captured scalar is a scalar.
227
+
228
+ A missing cell reaches as far as the window does: `a[-1, 0]` over a cell whose neighbour is UNDEF gives UNDEF, which is the propagation the rest of this compiler already does.
229
+
230
+ ### The array that comes back
231
+
232
+ Typed from the block's value, as `jit_map`'s result is, unless you say otherwise:
233
+
234
+ ```ruby
235
+ CArray.jit_stencil(image, type: :float32) # collect into float32
236
+ CArray.jit_stencil(image, into: edges) # write into an array of yours
237
+ ```
238
+
239
+ `into:` takes an array of the stencil's own shape and returns it; the type is then that array's. Passing both is refused — the array already says what type it is.
240
+
241
+ ### What is not a stencil
242
+
243
+ - **A computed offset.** `a[k, 0]` where `k` is a value is refused: the offsets are what the radius is read from, and the radius is what lets the interior be walked without asking, at every cell, whether it is still inside. A subscript the kernel works out is [`jit_for`](#extents-steps-and-subscripts)'s, and so is a window whose width is decided when the program runs.
244
+
245
+ Arithmetic over literals is not a computed offset — `a[-1-1, 0]` is `a[-2, 0]`, folded where the block is read, and a stencil drawn from a formula is usually written that way. What may not appear is anything that has to be *read* to be known, a captured integer included: those arrive with the call, and one compiled kernel serves every value of them, so a window built from one would have a radius the loop does not know.
246
+ - **A recurrence.** `smoothed[i] = alpha * price[i] + (1-alpha) * smoothed[i-1]` reads a cell this loop wrote. A window reads the array as it was, so that is not a stencil however much it looks like one — it is `jit_for`'s, and the direction of its extent is what records the dependency.
247
+ - **Writing.** A stencil produces a value. A block that writes several arrays is [`jit_each`](#jit_each-and-jit_map-when-nothing-reaches-a-neighbour)'s.
248
+ - **Arrays of different shapes.** They must agree; a stretched axis has no neighbour to reach.
249
+
250
+ ## Reductions
251
+
252
+ A reduction is a per-cell computation like any other: the caller says where the answer goes, and the kernel fills that cell.
253
+
254
+ ```ruby
255
+ CArray.jit_for(rows) { |i|
256
+ accumulator = 0.0
257
+ (0...columns).each { |j| accumulator = accumulator + source[i, j] }
258
+ total[i] = accumulator
259
+ }
260
+ ```
261
+
262
+ The accumulator is split into partial sums, which is faster than the serial chain and usually the more accurate answer, and is not the order the same loop would take in Ruby. `reassociate: false` asks for that order; see "The order a reduction takes its terms in" below.
263
+
264
+ What makes it expressible is `(from...to).each { |j| ... }` -- or `n.times { |j| ... }`, which is the same loop from zero: an inner loop whose index addresses arrays but writes nothing. The accumulator is then an ordinary block-local, which is why sum, maximum, product, count and a dot product all fall out without a primitive each -- and why a matrix multiply does too:
265
+
266
+ ```ruby
267
+ CArray.jit_for(rows, columns) { |i, j|
268
+ accumulator = 0.0
269
+ (0...inner).each { |t| accumulator = accumulator + left[i, t] * right[t, j] }
270
+ result[i, j] = accumulator
271
+ }
272
+ ```
273
+
274
+ `each` rather than `for`, because `for` does not open a scope: it would assign an enclosing variable of the same name and leave the index bound afterwards, neither of which the generated loop does. The range must be a literal `Range`; `Enumerator::ArithmeticSequence` -- `(0...n).step(2)` -- is not handled yet.
275
+
276
+ Inner loops nest, and an index binds to an axis by name rather than by position. One index may walk two axes of the same array (a trace), two indices may walk one axis of it (`c[p,a] * c[p,b]`, which is a covariance), the outer indices need not address axes in their own order (a transposing read), and a full contraction is the same thing written into a one-cell box.
277
+
278
+ That holds for an array the kernel writes as well. In `v[a,b] = v[a,a]` the cell read is one this loop also writes, so cells reached before it read the old value and cells reached after it read the new one, and the answer depends on the order -- which is what the extent states. So the kernel runs the order it was given and means what the same Ruby loop means.
279
+
280
+ Written out, a contraction is loops; but the loops are the *definition* rewritten, so `jit_contract` writes the definition instead -- see [Contraction](#contraction).
281
+
282
+ An offset may be an integer the block closed over -- `a[i - window]` for a window the caller chooses -- and it reaches the kernel as an argument, so one compiled kernel serves every value of it. How far the kernel reaches is then known when it is called rather than when it compiled, which is where the bounds check already lives. Nothing is executed before it passes.
283
+
284
+ An index may address any axis of any array that is read. A subscript that is not an index **pins** the axis: `a[i, 0]` is the first column, and so is `a[i, offset]` where `offset` is an integer the block closed over. A pinned position is an argument to the kernel rather than part of it, so one compiled kernel serves every value -- which is what lets a contraction be taken row by row from an ordinary Ruby loop:
285
+
286
+ ```ruby
287
+ n.times { |i| out[i] = CArray.jit_contract { |k| a[i, k] * b[i, k] }[0] }
288
+ ```
289
+
290
+ Whether that is a good idea depends on how much work each call does. A call costs 20-40 us before the kernel starts, and against that:
291
+
292
+ ```
293
+ 64 dot products, inner length Ruby loop loop of jit_contract one jit_for
294
+ 4 0.04 ms 1.22 ms 0.03 ms
295
+ 64 0.56 ms 1.41 ms 0.02 ms
296
+ 1,000 8.38 ms 1.51 ms 0.07 ms
297
+ 10,000 83.17 ms 2.00 ms 0.60 ms
298
+ 100,000 839.73 ms 7.17 ms 5.92 ms
299
+ ```
300
+
301
+ Below a few hundred elements the loop of `jit_contract` is **slower than the Ruby loop it replaces** -- at an inner length of four, thirty times slower -- because the per-call cost dwarfs an inner loop the interpreter gets through quickly. Past a thousand it wins, and by a hundred thousand the overhead has disappeared and the loop form is simply the more readable one.
302
+
303
+ How many times the outer loop runs does not enter into it: both sides scale with it, so the ratio holds. At an inner length of 64 the loop of `jit_contract` is 0.12x, 0.33x and 0.40x the Ruby loop for 4, 64 and 1024 outer iterations; at 10,000 it is 48x, 41x and 41x. The only question is whether one call's worth of work is worth its 20-30 us.
304
+
305
+ Writing the whole thing as one kernel has no such threshold: it beats the Ruby loop at every size on this table, and by two orders of magnitude once there is real work.
306
+
307
+ A constant subscript pins an axis: `a[i, 0]` and `a[i, j]` on the same array is ordinary. An array the kernel *writes* cannot be read through an inner index, because that would reach cells another outer iteration owns and no evaluation order settles it.
308
+
309
+ ```
310
+ row sums over 2000 x 500
311
+ jit_for 0.3 ms 0.25 ns/cell
312
+ jit_for, reassociate: false 0.9 ms 0.92 ns/cell 3.61x
313
+ Ruby loop 50.7 ms 50.75 ns/cell 200x
314
+ sum(axis: 1) 0.1 ms 0.10 ns/cell 0.40x
315
+
316
+ matrix multiply 300 x 300 x 300
317
+ jit_for 6.5 ms 0.24 ns per multiply-add 8.37 GFLOP/s
318
+ jit_for, reassociate: false 15.3 ms 0.57 ns per multiply-add 3.53 GFLOP/s
319
+ ```
320
+
321
+ #### The order a reduction takes its terms in
322
+
323
+ Floating-point addition is not associative, so a serial accumulator is one dependent chain and each addition waits for the one before it. Splitting it into partial sums is what fills that latency, and it is the whole of the difference between the two rows above -- and between `jit_for` and `sum(axis: 1)`, whose kernels take a `reduction_kind:` licence that emits `#pragma omp simd reduction(...)`.
324
+
325
+ A kernel splits the accumulator by default, into eight chains combined pairwise at the end, with the terms that do not fill a round left to a serial tail. So the three answers are three numbers:
326
+
327
+ ```
328
+ Ruby's serial sum 204800000000.0 4247d78400000000
329
+ jit_for, reassociate: false 204800000000.0 4247d78400000000
330
+ jit_for 204800000000.0042 4247d7840000008a
331
+ sum(axis: 1) 204800000000.0074 4247d784000000f2
332
+ ```
333
+
334
+ That is not a trade of accuracy for speed. Splitting the accumulation is what limits the cancellation, so the partial sums are usually the *more* accurate answer -- on this row, which cancels, the licensed kernel is nearer the true sum than the serial one is. What it is not is the order Ruby's loop would have taken, and that is what `reassociate: false` asks for:
335
+
336
+ ```ruby
337
+ CArray.jit_for(rows, reassociate: false) { |i|
338
+ accumulator = 0.0
339
+ (0...columns).each { |j| accumulator = accumulator + source[i, j] }
340
+ total[i] = accumulator
341
+ }
342
+ ```
343
+
344
+ Two reasons to ask for it. A **compensated summation** -- Kahan's, or any error-free transformation -- *is* its order: reassociating it does not make it less accurate, it deletes the algorithm. And **checking a kernel against the loop it replaces** needs the two to be comparable to the last bit, which is how this gem's own tests are written; `CARRAY_JIT_REASSOCIATE=0` sets it for a whole process.
345
+
346
+ What is licensed is the order the *iterations* are grouped in, and nothing else. Each term is computed exactly as it was written, in the operand order it was written in, and everything outside a reduction is untouched: a recurrence is serial by definition, and a stencil's cells are independent, so neither has an order to give up.
347
+
348
+ The licence is part of the kernel rather than of the call -- it decides what C is emitted -- so the two spellings compile to two kernels and the cache keeps them apart. `#c_source` shows which one ran; the chains are named `<accumulator>__p0` and up.
349
+
350
+ **A fold and nothing else.** The accumulator has to enter the loop already live and leave it folded whole: one statement, one associative operator, the accumulator on one side and a term that does not mention it on the other. An exponential average -- `accumulator = accumulator * 0.5 + source[i, j]` -- is not that, and keeps the serial loop, which is right: there the order is the algorithm. So does a masked accumulator (a partial sum would need a mask each), an integer one (reassociating it computes the same number, so there is nothing to license), and a written-out extent shorter than one round.
351
+
352
+ The matrix multiply carries a different subtlety -- it is a plain triple loop and not a blocked GEMM, so BLAS is still an order of magnitude away.
353
+
354
+ So this is not a faster `sum`. It is a way to write the reduction that has no `sum`.
355
+
356
+ ## Contraction
357
+
358
+ `CArray.jit_contract` contracts over a repeated index: **an index that appears twice in the term is summed**. The repetition is the notation -- it is what stands in for the sigma.
359
+
360
+ ```ruby
361
+ c = CArray.jit_contract { |i, j, k| a[i,k] * b[k,j] } # a matrix product
362
+ m = CArray.jit_contract { |i, k| a[i,k] * v[k] } # a matrix times a vector
363
+ s = CArray.jit_contract { |i, k| a[i,k] * b[i,k] } # both summed, one cell
364
+ t = CArray.jit_contract { |i| q[i,i] } # a trace
365
+ o = CArray.jit_contract { |i, j| p[i] * r[j] } # an outer product, nothing summed
366
+ ```
367
+
368
+ The result is allocated and returned, its axes being the free indices in the order the block named them -- so the parameter list is where the axis order is stated, and `{ |j, i, k| ... }` gives the transpose. Assigning into an array of your own says where to put it instead:
369
+
370
+ ```ruby
371
+ CArray.jit_contract { |i, j, k| c[i,j] = a[i,k] * b[k,j] }
372
+ ```
373
+
374
+ which must name exactly the free indices, and still does not decide what is summed.
375
+
376
+ No extent is given, because each index's extent is fixed by the axes it addresses. An index whose axes disagree is refused, which is the shape check a contraction exists to do:
377
+
378
+ ```
379
+ `k` addresses axes of different extents: `a` axis 1 is 4, `b` axis 0 is 5
380
+ ```
381
+
382
+ Neither form decides what is summed. So a sum along an axis is not a contraction, and is refused:
383
+
384
+ ```ruby
385
+ CArray.jit_contract { |i, k| total[i] = a[i,k] }
386
+ #=> `k` appears once, so it is free and must be on the left. A contraction
387
+ # sums the indices that appear twice; to sum one that does not, write the
388
+ # loop with jit_for, or use sum(axis:)
389
+ ```
390
+
391
+ There is nothing in `a[i,k]` standing in for a sigma, and summing anyway would be the `=` quietly meaning something it does not say. `sum(axis: 1)` is that operation, and it is faster than anything written here.
392
+
393
+ An array that is both written and read is a recurrence rather than a contraction, and is refused with a pointer at `jit_for` too.
394
+
395
+ There is no BLAS for an arbitrary contraction, which is rather the point: this compiles to a plain nest of loops and is slower than a tuned GEMM, but it is one line and it exists.
396
+
397
+ Its sum is serial. `jit_for`'s reduction takes partial sums by default and `jit_contract`'s does not, which is the wrong way round -- a contraction is a sum with no loop written anywhere for it to agree with -- and stays that way only until `jit_contract` has a meaning in the core to be licensed against.