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.
- checksums.yaml +7 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +84 -0
- data/LICENSE +21 -0
- data/README.md +88 -0
- data/bin/carray-jit +194 -0
- data/carray-jit.gemspec +41 -0
- data/docs/00_Introduction.md +40 -0
- data/docs/01_GettingStarted.md +80 -0
- data/docs/02_KernelShapes.md +397 -0
- data/docs/03_SupportedFeatures.md +595 -0
- data/docs/04_Compiling.md +234 -0
- data/docs/05_DesignNotes.md +136 -0
- data/docs/06_Cheatsheet.md +177 -0
- data/examples/README.md +56 -0
- data/examples/applications/game_of_life.rb +161 -0
- data/examples/applications/heat_equation.rb +117 -0
- data/examples/applications/kepler.rb +178 -0
- data/examples/applications/mandelbrot.rb +151 -0
- data/examples/applications/moving_average.rb +124 -0
- data/examples/applications/partial_sums.rb +141 -0
- data/examples/applications/point_cloud.rb +110 -0
- data/examples/applications/quicksort.rb +118 -0
- data/examples/applications/recursion.rb +121 -0
- data/examples/applications/relaxation.rb +115 -0
- data/examples/applications/sensor_gaps.rb +118 -0
- data/examples/applications/sieve.rb +95 -0
- data/examples/applications/sobel_edges.rb +80 -0
- data/examples/features/01_element_wise.rb +69 -0
- data/examples/features/02_stencil.rb +40 -0
- data/examples/features/03_recurrence.rb +50 -0
- data/examples/features/04_thomas.rb +81 -0
- data/examples/features/05_reduction.rb +90 -0
- data/examples/features/06_jit_contract.rb +58 -0
- data/examples/features/07_masks.rb +55 -0
- data/examples/features/08_views.rb +46 -0
- data/examples/features/09_inspecting.rb +55 -0
- data/examples/features/10_complex.rb +107 -0
- data/examples/features/11_c_functions.rb +260 -0
- data/examples/features/12_sweep.rb +139 -0
- data/examples/features/13_cscalar.rb +80 -0
- data/examples/features/14_stencil_window.rb +106 -0
- data/examples/features/15_loops.rb +148 -0
- data/examples/features/16_raising.rb +69 -0
- data/ext/carray_jit_access/carray_jit_access.c +460 -0
- data/ext/carray_jit_access/extconf.rb +8 -0
- data/lib/carray/jit/analyzer.rb +1847 -0
- data/lib/carray/jit/block_reader.rb +139 -0
- data/lib/carray/jit/c_function.rb +777 -0
- data/lib/carray/jit/c_generator.rb +2305 -0
- data/lib/carray/jit/compiler.rb +468 -0
- data/lib/carray/jit/errors.rb +37 -0
- data/lib/carray/jit/expression.rb +202 -0
- data/lib/carray/jit/kernel.rb +509 -0
- data/lib/carray/jit/node.rb +573 -0
- data/lib/carray/jit/sweep.rb +97 -0
- data/lib/carray/jit/type_assignment.rb +811 -0
- data/lib/carray/jit/version.rb +5 -0
- data/lib/carray/jit.rb +1210 -0
- metadata +139 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Letting CArray drive the loop.
|
|
2
|
+
#
|
|
3
|
+
# An element-wise pass is the one shape that is a *sweep*: nothing reaches a
|
|
4
|
+
# neighbour, nothing chooses an order. So where this CArray has
|
|
5
|
+
# `ca_call_cslab` (3.0.1 and later), jit_each hands it the compiled body
|
|
6
|
+
# and lets it acquire the operands -- broadcasting them, ORing and propagating
|
|
7
|
+
# the masks, and calling back a chunk at a time.
|
|
8
|
+
#
|
|
9
|
+
# Nothing about the expression changes, and nothing here is written
|
|
10
|
+
# differently. What changes is who opens the arrays, and what that costs.
|
|
11
|
+
|
|
12
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
13
|
+
require "carray/jit"
|
|
14
|
+
require "benchmark"
|
|
15
|
+
|
|
16
|
+
Sweep = CArray::JIT::Sweep
|
|
17
|
+
Access = CArray::JIT::Access
|
|
18
|
+
|
|
19
|
+
puts "this CArray has ca_call_cslab: #{Sweep.available?}"
|
|
20
|
+
puts
|
|
21
|
+
|
|
22
|
+
# The rank leaves the loop
|
|
23
|
+
# ------------------------
|
|
24
|
+
# A chunk is one flat run of cells -- and CArray's acquire reads an operand's
|
|
25
|
+
# element count and element size and never looks at its shape, so a chunk is
|
|
26
|
+
# already flat. Nothing is reshaped. What is compiled flat is the *kernel*.
|
|
27
|
+
|
|
28
|
+
def doubled (source, out)
|
|
29
|
+
CArray.jit_each { out = source * 2.0 }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
flat_in = CArray.double(6).seq!
|
|
33
|
+
flat_out = CArray.double(6)
|
|
34
|
+
cube_in = CArray.double(2, 3, 4).seq!
|
|
35
|
+
cube_out = CArray.double(2, 3, 4)
|
|
36
|
+
|
|
37
|
+
flat = doubled(flat_in, flat_out)
|
|
38
|
+
cube = doubled(cube_in, cube_out)
|
|
39
|
+
|
|
40
|
+
puts "one block, a run of cells and a cube of them"
|
|
41
|
+
puts " loop axes #{flat.rank} and #{cube.rank}"
|
|
42
|
+
puts " arrays keep shape #{flat_out.dim.inspect} and #{cube_out.dim.inspect}"
|
|
43
|
+
puts " one compiled kernel #{flat.equal?(cube)}"
|
|
44
|
+
puts " answers #{flat_out.to_a == (flat_in * 2.0).to_a} and " \
|
|
45
|
+
"#{cube_out.to_a == (cube_in * 2.0).to_a}"
|
|
46
|
+
puts
|
|
47
|
+
|
|
48
|
+
# The wrapper is a call, not a second code generator: the kernel's first three
|
|
49
|
+
# arguments are already base, stride and bounds, which is what a chunk arrives
|
|
50
|
+
# as.
|
|
51
|
+
puts "the sweep entry point"
|
|
52
|
+
puts flat.slab_source.lines
|
|
53
|
+
.grep(/^carray_jit_slab|bounds\[1\] = n|carray_jit_kernel\(base/)
|
|
54
|
+
.map { |line| " " + line.strip }
|
|
55
|
+
puts
|
|
56
|
+
|
|
57
|
+
# What it is worth
|
|
58
|
+
# ----------------
|
|
59
|
+
# An operand CArray cannot walk in place -- a gather, a lazy array -- is
|
|
60
|
+
# re-gathered 32KB at a time by the sweep. The tiers here move the whole box
|
|
61
|
+
# the kernel touches instead, and for an element-wise pass that box is the
|
|
62
|
+
# whole array.
|
|
63
|
+
|
|
64
|
+
n = 2_000_000
|
|
65
|
+
src = CArray.double(n).seq!
|
|
66
|
+
b = CArray.double(n).seq!(0.5, 0.5)
|
|
67
|
+
out = CArray.double(n)
|
|
68
|
+
gather = src[CArray.int32(n).seq.reverse]
|
|
69
|
+
|
|
70
|
+
def timed
|
|
71
|
+
2.times { yield }
|
|
72
|
+
Benchmark.realtime { 3.times { yield } } / 3
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
puts "n = #{n}, out[] = x + b * 2.0"
|
|
76
|
+
[["entity", src], ["gather view", gather]].each do |label, x|
|
|
77
|
+
elapsed = timed { CArray.jit_each { out = x + b * 2.0 } }
|
|
78
|
+
scratch = Access.classify(x)[:tier] == 3 ? "%.0f MB" % (n * 8.0 / 1024 / 1024) : "none"
|
|
79
|
+
puts " %-12s tier %d %6.2f ms the driver here would hold %s" %
|
|
80
|
+
[label, Access.classify(x)[:tier], elapsed * 1e3, scratch]
|
|
81
|
+
end
|
|
82
|
+
puts " -- the sweep holds 32 KB per gathered operand, whatever n is."
|
|
83
|
+
|
|
84
|
+
# A strided view is the case where the re-gather buys nothing: the tiers
|
|
85
|
+
# address a column, a transpose or every other cell in place, so there is no
|
|
86
|
+
# whole-array copy to be saved from. That is why the driver is chosen on the
|
|
87
|
+
# operands and not only on the shape.
|
|
88
|
+
half = (0...n).step(2)
|
|
89
|
+
strided_x, strided_b, strided_out = src[half], b[half], out[half]
|
|
90
|
+
elapsed = timed { CArray.jit_each { strided_out = strided_x + strided_b * 2.0 } }
|
|
91
|
+
puts " %-12s tier %d %6.2f ms %.2f ns/cell, and no scratch either way" %
|
|
92
|
+
["strided view", Access.classify(strided_x)[:tier], elapsed * 1e3,
|
|
93
|
+
elapsed * 1e9 / strided_x.elements]
|
|
94
|
+
puts " -- swept, the same pass was 4.7 ns/cell: re-gathered for nothing."
|
|
95
|
+
puts
|
|
96
|
+
|
|
97
|
+
# What keeps the driver here
|
|
98
|
+
# --------------------------
|
|
99
|
+
# Four things, and only the last is about this CArray being old.
|
|
100
|
+
|
|
101
|
+
columns = CArray.double(64, 64).seq!
|
|
102
|
+
into = CArray.double(64, 64)
|
|
103
|
+
every_other = columns[nil, (0...64).step(2)]
|
|
104
|
+
every_other_out = into[nil, (0...64).step(2)]
|
|
105
|
+
kernel = CArray.jit_each { every_other_out = every_other * 2.0 }
|
|
106
|
+
puts "a strided view operand loop axes #{kernel.rank}"
|
|
107
|
+
puts " -- walked in place here, re-gathered there for nothing."
|
|
108
|
+
|
|
109
|
+
matrix = CArray.double(3, 4).seq!
|
|
110
|
+
row = CArray.double(1, 4).seq!(10.0)
|
|
111
|
+
stretched = CArray.double(3, 4)
|
|
112
|
+
kernel = CArray.jit_each { stretched = matrix * row }
|
|
113
|
+
puts "a stretched operand loop axes #{kernel.rank}"
|
|
114
|
+
puts " -- broadcasting arrives as a stride of zero on an axis, and a flat"
|
|
115
|
+
puts " run has no axes, so cell k would stop lining up."
|
|
116
|
+
|
|
117
|
+
masked = CArray.double(5).seq!(1.0)
|
|
118
|
+
masked[2] = UNDEF
|
|
119
|
+
masked_out = CArray.double(5)
|
|
120
|
+
kernel = CArray.jit_each { masked_out = masked * 2.0 }
|
|
121
|
+
puts "a mask anywhere in the pass sweepable? #{kernel.sweepable?}"
|
|
122
|
+
puts " -- an operand that carries one, or a body that asks about one."
|
|
123
|
+
puts " CArray ORs and propagates the masks, but `a[i] == UNDEF` is a"
|
|
124
|
+
puts " question about a single cell, and a chunk carries no per-cell"
|
|
125
|
+
puts " mask to ask it of. The answer is the same either way:"
|
|
126
|
+
puts " #{masked_out.is_masked.to_a.inspect}"
|
|
127
|
+
puts
|
|
128
|
+
puts "a CArray without the family asked for by symbol, not assumed;"
|
|
129
|
+
puts " -- an older one falls back without the caller hearing about it."
|
|
130
|
+
puts
|
|
131
|
+
|
|
132
|
+
# jit_for never goes this way, and that is not a limitation: a kernel that
|
|
133
|
+
# names an index reaches neighbours, chooses an order and runs inner loops,
|
|
134
|
+
# none of which a chunked walk can offer. Splitting the two methods by what
|
|
135
|
+
# the block names drew the same line as what a sweep can drive.
|
|
136
|
+
recurrence = CArray.double(8).seq!(1.0)
|
|
137
|
+
CArray.jit_for(1...8) { |i| recurrence[i] = recurrence[i-1] * 1.5 }
|
|
138
|
+
puts "jit_for stays here by its nature"
|
|
139
|
+
puts " #{recurrence.to_a.map { |v| v.round(4) }.inspect}"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# CScalar: a value with a home.
|
|
2
|
+
#
|
|
3
|
+
# A CScalar is the one-cell CArray it subclasses, minus the index. `s[]` is
|
|
4
|
+
# the value and `s[] = ...` puts one back, and that missing index is the whole
|
|
5
|
+
# of the difference -- which is why it needed saying to a compiler whose
|
|
6
|
+
# kernel language is written in subscripts.
|
|
7
|
+
|
|
8
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
9
|
+
require "carray/jit"
|
|
10
|
+
|
|
11
|
+
gain = CScalar.double() { 2.0 }
|
|
12
|
+
offset = CScalar.double() { 10.0 }
|
|
13
|
+
signal = CArray.double(6).seq!(1.0)
|
|
14
|
+
out = CArray.double(6)
|
|
15
|
+
|
|
16
|
+
# In an expression it is read at every cell, which is what CArray's own
|
|
17
|
+
# operators do with it. Nothing here says it is a scalar; it says so itself.
|
|
18
|
+
CArray.jit_each { out = signal * gain + offset }
|
|
19
|
+
puts "signal * gain + offset"
|
|
20
|
+
puts " #{out.to_a.inspect}"
|
|
21
|
+
puts " same as CArray's own operators #{out.to_a == (signal * gain + offset).to_a}"
|
|
22
|
+
puts
|
|
23
|
+
|
|
24
|
+
# And written like any other cell.
|
|
25
|
+
CArray.jit_each { gain = gain + 0.5 }
|
|
26
|
+
puts "gain after gain[] = gain + 0.5 #{gain[0]}"
|
|
27
|
+
puts
|
|
28
|
+
|
|
29
|
+
# An indexed kernel reaches it too, and there the missing index is the point:
|
|
30
|
+
# there is no axis to walk, so there is no index to write. A bare name says
|
|
31
|
+
# the same thing.
|
|
32
|
+
indexed = CArray.double(6)
|
|
33
|
+
CArray.jit_for(6) { |i| indexed[i] = signal[i] * gain[] }
|
|
34
|
+
puts "the same, with the loop written out"
|
|
35
|
+
puts " #{indexed.to_a.inspect}"
|
|
36
|
+
|
|
37
|
+
bare = CArray.double(6)
|
|
38
|
+
CArray.jit_for(6) { |i| bare[i] = signal[i] * gain }
|
|
39
|
+
puts " a bare `gain` agrees #{bare.to_a == indexed.to_a}"
|
|
40
|
+
|
|
41
|
+
# It is still the one-cell array it is, so the index spelling keeps working
|
|
42
|
+
# and means the same cell.
|
|
43
|
+
spelled = CArray.double(6)
|
|
44
|
+
CArray.jit_for(6) { |i| spelled[i] = signal[i] * gain[0] }
|
|
45
|
+
puts " and so does gain[0] #{spelled.to_a == indexed.to_a}"
|
|
46
|
+
puts
|
|
47
|
+
|
|
48
|
+
# Every iteration writes the one cell it has, and what is left is what the
|
|
49
|
+
# same Ruby loop leaves -- which makes it an accumulator without asking for
|
|
50
|
+
# one. The inner loop of a reduction rests on exactly this.
|
|
51
|
+
total = CScalar.int() { 0 }
|
|
52
|
+
counts = CArray.int(4).seq!(1)
|
|
53
|
+
CArray.jit_for(4) { |i| total[] = total[] + counts[i] }
|
|
54
|
+
puts "total after one pass over #{counts.to_a.inspect} #{total[0]}"
|
|
55
|
+
|
|
56
|
+
in_ruby = 0
|
|
57
|
+
(0...4).each { |i| in_ruby = in_ruby + counts[i] }
|
|
58
|
+
puts " what the same Ruby loop leaves #{in_ruby}"
|
|
59
|
+
puts
|
|
60
|
+
|
|
61
|
+
# A contraction takes one as it takes any other operand it does not index.
|
|
62
|
+
left = CArray.double(3).seq!(1.0)
|
|
63
|
+
right = CArray.double(3).seq!(1.0)
|
|
64
|
+
weight = CScalar.double() { 2.0 }
|
|
65
|
+
puts "weighted dot product #{CArray.jit_contract { |k| left[k] * right[k] * weight[] }[0]}"
|
|
66
|
+
puts
|
|
67
|
+
|
|
68
|
+
# What it will not do is take an expression wider than itself, because there
|
|
69
|
+
# is one cell and no answer to which of the values lands. CArray refuses the
|
|
70
|
+
# same assignment, in the same terms.
|
|
71
|
+
begin
|
|
72
|
+
CArray.jit_each { weight = signal + 1.0 }
|
|
73
|
+
rescue CArray::JIT::Unsupported => error
|
|
74
|
+
puts "refused: #{error.message}"
|
|
75
|
+
end
|
|
76
|
+
begin
|
|
77
|
+
weight[] = signal + 1.0
|
|
78
|
+
rescue => error
|
|
79
|
+
puts "CArray: #{error.message.lines.first.strip[0, 78]}"
|
|
80
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# The window spelling of a stencil, and the border as an argument.
|
|
2
|
+
#
|
|
3
|
+
# 02_stencil.rb writes the same five-point average with the indices named.
|
|
4
|
+
# What that spelling has nowhere to put is the edge: `src[i-1, j]` at i = 0
|
|
5
|
+
# has no meaning, so the extents avoid it and the border keeps whatever the
|
|
6
|
+
# output array held. A window has no index to write and so has somewhere to
|
|
7
|
+
# put the question -- `border:` at the call.
|
|
8
|
+
|
|
9
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
10
|
+
require "carray/jit"
|
|
11
|
+
|
|
12
|
+
rows, columns = 5, 6
|
|
13
|
+
src = CArray.double(rows, columns).seq!(1.0)
|
|
14
|
+
|
|
15
|
+
# The block's parameters are windows onto the arrays given, in that order:
|
|
16
|
+
# a[0, 0] is the cell the loop is on, a[-1, 0] its neighbour. The block's
|
|
17
|
+
# value is what the cell gets, as jit_map's is, and an array of the same shape
|
|
18
|
+
# comes back.
|
|
19
|
+
smoothed = CArray.jit_stencil(src) { |a|
|
|
20
|
+
0.25 * (a[-1, 0] + a[1, 0] + a[0, -1] + a[0, 1])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# The same stencil with the indices named, over the interior alone.
|
|
24
|
+
written = CArray.double(rows, columns)
|
|
25
|
+
CArray.jit_for(1...(rows-1), 1...(columns-1)) { |i, j|
|
|
26
|
+
written[i, j] = 0.25 * (src[i-1, j] + src[i+1, j] + src[i, j-1] + src[i, j+1])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
puts "the window"
|
|
30
|
+
puts " interior matches jit_for #{smoothed[1..-2, 1..-2].to_a == written[1..-2, 1..-2].to_a}"
|
|
31
|
+
# The default is :mask, because CArray can say "not computed", and a border of
|
|
32
|
+
# zeros that means the same cannot be told from zeros that were computed.
|
|
33
|
+
puts " border missing by default #{smoothed.count_masked} cells"
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------- the border
|
|
36
|
+
|
|
37
|
+
# Five answers, on one row of a small array so they can be read side by side.
|
|
38
|
+
line = CArray.double(1, 5).seq!(1.0) # 1 2 3 4 5
|
|
39
|
+
|
|
40
|
+
def row (result)
|
|
41
|
+
result.to_a[0].map { |e| e == UNDEF ? " --" : format("%4.1f", e) }.join(" ")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
puts
|
|
45
|
+
puts "border:, on [1 2 3 4 5] averaging each cell with its two neighbours"
|
|
46
|
+
[:mask, :skip, :zero, :clamp, :wrap].each do |border|
|
|
47
|
+
# :skip leaves the border as it was found, so what it was found as is worth
|
|
48
|
+
# seeing: this one is filled with -1 first.
|
|
49
|
+
into = CArray.double(1, 5).seq!(-1.0, 0.0)
|
|
50
|
+
result = CArray.jit_stencil(line, border: border, into: into) { |a|
|
|
51
|
+
(a[0, -1] + a[0, 0] + a[0, 1]) / 3.0
|
|
52
|
+
}
|
|
53
|
+
puts format(" %-6s %s", border, row(result))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# :mask and :skip are answers about the cell -- it is not computed. The other
|
|
57
|
+
# three answer for the read instead, so those cells are computed after all:
|
|
58
|
+
# :zero reads 0 outside, :clamp the nearest cell, :wrap the far side. A Game
|
|
59
|
+
# of Life board is a torus because of :wrap; an image filter usually wants
|
|
60
|
+
# :clamp.
|
|
61
|
+
|
|
62
|
+
# --------------------------------------------------- several arrays and rest
|
|
63
|
+
|
|
64
|
+
# Each array given gets a window, in the order the block names them; the names
|
|
65
|
+
# shadow whatever they hold outside, as CArray.fuse's do. A diffusion step
|
|
66
|
+
# with a conductivity that varies from cell to cell:
|
|
67
|
+
u = CArray.double(6, 6) { |i, j| (i - 2.5).abs + (j - 2.5).abs }
|
|
68
|
+
k = CArray.double(6, 6).fill(0.2)
|
|
69
|
+
|
|
70
|
+
stepped = CArray.jit_stencil(u, k, border: :clamp) { |u, k|
|
|
71
|
+
u[0,0] + k[0,0] * (u[-1,0] + u[1,0] + u[0,-1] + u[0,1] - 4.0 * u[0,0])
|
|
72
|
+
}
|
|
73
|
+
puts
|
|
74
|
+
puts "several arrays"
|
|
75
|
+
puts " every cell computed #{stepped.count_masked.zero?}"
|
|
76
|
+
|
|
77
|
+
# The offsets have to be known before the loop runs -- the radius is what lets
|
|
78
|
+
# the interior be walked without asking, at each cell, whether it is inside.
|
|
79
|
+
# Arithmetic over literals is not a computed offset: it is folded where the
|
|
80
|
+
# block is read, so a stencil drawn from a formula may be written as one.
|
|
81
|
+
wide = CArray.jit_stencil(src, border: :clamp) { |a| a[0, -1-1] + a[0, 1+1] }
|
|
82
|
+
puts " a folded offset #{wide[2, 2] == src[2, 0] + src[2, 4]}"
|
|
83
|
+
|
|
84
|
+
# What may not appear is anything that has to be read to be known -- a
|
|
85
|
+
# captured integer included, since one compiled kernel serves every value of
|
|
86
|
+
# it and the loop would not know the radius.
|
|
87
|
+
begin
|
|
88
|
+
offset = 1
|
|
89
|
+
CArray.jit_stencil(src) { |a| a[0, offset] }
|
|
90
|
+
rescue CArray::JIT::Unsupported => error
|
|
91
|
+
puts " a computed offset refused: #{error.message.sub(/ \(at line.*/m, "")}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# ------------------------------------------------------ the array comes back
|
|
95
|
+
|
|
96
|
+
# Typed from the block's value unless you say otherwise, as jit_map's is.
|
|
97
|
+
counts = CArray.jit_stencil(src, border: :zero, type: :int32) { |a|
|
|
98
|
+
a[-1, 0] + a[1, 0] > a[0, 0] ? 1 : 0
|
|
99
|
+
}
|
|
100
|
+
puts
|
|
101
|
+
puts "the result"
|
|
102
|
+
puts " type: #{counts.data_type_name}"
|
|
103
|
+
# into: writes an array of yours and returns it; the type is then that array's.
|
|
104
|
+
mine = CArray.float32(rows, columns)
|
|
105
|
+
same = CArray.jit_stencil(src, border: :clamp, into: mine) { |a| a[0, 0] * 2.0 }
|
|
106
|
+
puts " into: #{same.equal?(mine)}, and typed #{mine.data_type_name} by it"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Loops inside a kernel: an inner `each` with a `break`, and `while` where the
|
|
2
|
+
# bound is not knowable.
|
|
3
|
+
#
|
|
4
|
+
# The kernel's own loop is written by the extents. Inside the body a cell may
|
|
5
|
+
# still need a loop of its own -- a search, an iteration to a tolerance -- and
|
|
6
|
+
# there are two spellings, which differ in whether the number of passes can be
|
|
7
|
+
# bounded before the loop runs.
|
|
8
|
+
|
|
9
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
10
|
+
require "carray/jit"
|
|
11
|
+
|
|
12
|
+
rows, columns = 5, 8
|
|
13
|
+
source = CArray.double(rows, columns) { |i, j| (j - i) * 1.0 }
|
|
14
|
+
threshold = 1.5
|
|
15
|
+
|
|
16
|
+
# A search: the first column past the threshold, or -1. `break` in an inner
|
|
17
|
+
# loop means what it means in Ruby, and so does `next`.
|
|
18
|
+
first = CArray.int32(rows)
|
|
19
|
+
CArray.jit_for(rows) { |i|
|
|
20
|
+
found = -1
|
|
21
|
+
(0...columns).each { |j|
|
|
22
|
+
if source[i, j] > threshold
|
|
23
|
+
found = j
|
|
24
|
+
break
|
|
25
|
+
end
|
|
26
|
+
}
|
|
27
|
+
first[i] = found
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
reference = (0...rows).map { |i|
|
|
31
|
+
(0...columns).find { |j| source[i, j] > threshold } || -1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
puts "an inner loop with a break"
|
|
35
|
+
puts " first past #{threshold} #{first.to_a.inspect}"
|
|
36
|
+
puts " matches Ruby #{first.to_a == reference}"
|
|
37
|
+
|
|
38
|
+
# ------------------------------------------------------- a bounded iteration
|
|
39
|
+
|
|
40
|
+
# Newton's method for a square root, with the bound in the extent. This is
|
|
41
|
+
# the better spelling wherever a bound exists: the kernel cannot fail to stop,
|
|
42
|
+
# and a cell that ran out of passes can be told from one that converged.
|
|
43
|
+
n = 2000
|
|
44
|
+
a = CArray.double(n).seq!(1.0)
|
|
45
|
+
tolerance = 1e-12
|
|
46
|
+
cap = 40
|
|
47
|
+
|
|
48
|
+
root = CArray.double(n)
|
|
49
|
+
passes = CArray.int32(n)
|
|
50
|
+
CArray.jit_for(n) { |i|
|
|
51
|
+
x = a[i]
|
|
52
|
+
taken = 0
|
|
53
|
+
(0...cap).each { |k|
|
|
54
|
+
break if (x * x - a[i]).abs <= tolerance * a[i]
|
|
55
|
+
x = 0.5 * (x + a[i] / x)
|
|
56
|
+
taken = taken + 1
|
|
57
|
+
}
|
|
58
|
+
root[i] = x
|
|
59
|
+
passes[i] = taken
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
puts
|
|
63
|
+
puts "Newton, bounded by the extent"
|
|
64
|
+
puts format(" worst relative error %.1e", ((root ** 2 - a).abs / a).max)
|
|
65
|
+
puts " passes taken #{passes.min}..#{passes.max}"
|
|
66
|
+
puts " none ran out #{passes.max < cap}"
|
|
67
|
+
|
|
68
|
+
# ----------------------------------------------------------------- and while
|
|
69
|
+
|
|
70
|
+
# Where the bound is not knowable, `while` says so. The condition is read at
|
|
71
|
+
# the top of every pass, as Ruby's is; a local the condition reads has to be a
|
|
72
|
+
# local before the loop, since the condition is read before the body is.
|
|
73
|
+
by_while = CArray.double(n)
|
|
74
|
+
CArray.jit_for(n) { |i|
|
|
75
|
+
guess = a[i]
|
|
76
|
+
while (guess * guess - a[i]).abs > tolerance * a[i]
|
|
77
|
+
guess = 0.5 * (guess + a[i] / guess)
|
|
78
|
+
end
|
|
79
|
+
by_while[i] = guess
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
puts
|
|
83
|
+
puts "while"
|
|
84
|
+
puts " same answers #{by_while.to_a == root.to_a}"
|
|
85
|
+
|
|
86
|
+
# What it gives up is the guarantee that the loop ends -- nothing here can
|
|
87
|
+
# decide that in general, and a kernel that does not return cannot be
|
|
88
|
+
# interrupted, because the generated loop has no place to notice a signal.
|
|
89
|
+
# The one runaway that can be read off the page is refused rather than
|
|
90
|
+
# compiled:
|
|
91
|
+
begin
|
|
92
|
+
CArray.jit_for(4) { |i| while true do root[i] = root[i] + 1.0 end }
|
|
93
|
+
rescue CArray::JIT::Unsupported => error
|
|
94
|
+
puts " while true, no break refused: #{error.message.sub(/ \(at line.*/m, "")}"
|
|
95
|
+
end
|
|
96
|
+
# `while true` with a `break` in it is an ordinary thing to write, and is left
|
|
97
|
+
# alone. `until` is not in the subset: `while` with the condition negated is
|
|
98
|
+
# the same loop, and one spelling is enough to keep.
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------- and next
|
|
101
|
+
|
|
102
|
+
# `next` in the kernel block skips the cell, the way it would end a block Ruby
|
|
103
|
+
# was running: the cell keeps its value and its mask, and the loop moves on.
|
|
104
|
+
kept = CArray.double(n).fill(-1.0)
|
|
105
|
+
CArray.jit_for(n) { |i|
|
|
106
|
+
next if a[i] % 2.0 == 0.0
|
|
107
|
+
kept[i] = a[i]
|
|
108
|
+
}
|
|
109
|
+
puts
|
|
110
|
+
puts "next in the kernel block"
|
|
111
|
+
puts " even cells untouched #{(0...8).map { |i| kept[i] }.inspect}"
|
|
112
|
+
|
|
113
|
+
# What it costs: an inner loop carrying a break is not a fold, so it is not
|
|
114
|
+
# split into partial sums -- it stays the serial chain it was. The kernel is
|
|
115
|
+
# compiled on its first call and cached, so it is run once before the clock
|
|
116
|
+
# starts.
|
|
117
|
+
newton = lambda do
|
|
118
|
+
CArray.jit_for(n) { |i|
|
|
119
|
+
x = a[i]
|
|
120
|
+
(0...cap).each { |k|
|
|
121
|
+
break if (x * x - a[i]).abs <= tolerance * a[i]
|
|
122
|
+
x = 0.5 * (x + a[i] / x)
|
|
123
|
+
}
|
|
124
|
+
root[i] = x
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
newton.call
|
|
128
|
+
|
|
129
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
130
|
+
20.times { newton.call }
|
|
131
|
+
compiled = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 20
|
|
132
|
+
|
|
133
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
134
|
+
20.times do
|
|
135
|
+
(0...n).each do |i|
|
|
136
|
+
x = a[i]
|
|
137
|
+
cap.times do
|
|
138
|
+
break if (x * x - a[i]).abs <= tolerance * a[i]
|
|
139
|
+
x = 0.5 * (x + a[i] / x)
|
|
140
|
+
end
|
|
141
|
+
root[i] = x
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
interpreted = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 20
|
|
145
|
+
|
|
146
|
+
puts
|
|
147
|
+
puts format("Newton over %d cells: %.3f ms compiled, %.3f ms in Ruby (%.0fx)",
|
|
148
|
+
n, compiled * 1e3, interpreted * 1e3, interpreted / compiled)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Raising from a kernel.
|
|
2
|
+
#
|
|
3
|
+
# A kernel can stop and say why. What comes back is a RuntimeError with the
|
|
4
|
+
# message written in the block -- what `raise "..."` gives in Ruby -- and the
|
|
5
|
+
# loop stops where it raised: the cell that raised is not written, and neither
|
|
6
|
+
# are the ones after it.
|
|
7
|
+
|
|
8
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
9
|
+
require "carray/jit"
|
|
10
|
+
|
|
11
|
+
n = 8
|
|
12
|
+
depth = CArray.double(n).seq!(3.0, -1.0) # 3 2 1 0 -1 -2 ...
|
|
13
|
+
out = CArray.double(n).fill(-99.0)
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
CArray.jit_for(n) { |i|
|
|
17
|
+
raise "depth went negative" if depth[i] < 0.0
|
|
18
|
+
out[i] = Math.sqrt(depth[i])
|
|
19
|
+
}
|
|
20
|
+
rescue RuntimeError => error
|
|
21
|
+
puts "raising"
|
|
22
|
+
puts " class #{error.class}"
|
|
23
|
+
puts " message #{error.message}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The cells before the raise keep what the kernel wrote, as they do when a
|
|
27
|
+
# division with no divisor stops one.
|
|
28
|
+
puts " written before it #{out.to_a[0, 4].map { |e| e.round(3) }.inspect}"
|
|
29
|
+
puts " untouched after it #{out.to_a[4..].all?(-99.0)}"
|
|
30
|
+
|
|
31
|
+
# The message is written out and the class is not named, and both follow from
|
|
32
|
+
# where the message goes: C has nothing to carry a string out of a cell in, so
|
|
33
|
+
# the message is registered as the kernel is compiled and the cell writes a
|
|
34
|
+
# code for it into the error slot the kernel already watches. The raise
|
|
35
|
+
# itself happens once the loop has stopped and there is a Ruby stack to raise
|
|
36
|
+
# on.
|
|
37
|
+
|
|
38
|
+
# A cell whose value is missing does not raise: the comparison was decided by
|
|
39
|
+
# bytes that mean nothing, and this is the rule the division helper already
|
|
40
|
+
# keeps and that `if` keeps for what it writes.
|
|
41
|
+
holed = CArray.double(4).seq!(1.0)
|
|
42
|
+
holed[2] = 999.0 # a value that would raise below
|
|
43
|
+
holed[2] = UNDEF # marked missing; the bytes are left alone
|
|
44
|
+
squares = CArray.double(4)
|
|
45
|
+
CArray.jit_for(4) { |i|
|
|
46
|
+
raise "out of range" if holed[i] > 100.0
|
|
47
|
+
squares[i] = holed[i] * holed[i]
|
|
48
|
+
}
|
|
49
|
+
puts " a masked cell did not raise on its 999.0; #{squares.count_masked} cell came back missing"
|
|
50
|
+
|
|
51
|
+
# A jit_function body raises the same way, and the message travels with the
|
|
52
|
+
# function: f.call(-1.0) and the same body reached from a kernel raise the
|
|
53
|
+
# same thing.
|
|
54
|
+
safe_sqrt = CArray.jit_function("double (*)(double)") { |x|
|
|
55
|
+
raise "negative argument" if x < 0.0
|
|
56
|
+
Math.sqrt(x)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
safe_sqrt.call(-1.0)
|
|
61
|
+
rescue RuntimeError => error
|
|
62
|
+
puts " from jit_function #{error.message} (called from Ruby)"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
begin
|
|
66
|
+
CArray.jit_for(n) { |i| out[i] = safe_sqrt.call(depth[i]) }
|
|
67
|
+
rescue RuntimeError => error
|
|
68
|
+
puts " from a kernel #{error.message} (the same message, pasted in)"
|
|
69
|
+
end
|