carray-jit 0.1.3 → 0.1.5
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 +4 -4
- data/CHANGELOG.md +165 -0
- data/README.md +11 -10
- data/carray-jit.gemspec +1 -1
- data/docs/00_Introduction.md +2 -1
- data/docs/02_KernelShapes.md +1 -1
- data/docs/03_SupportedFeatures.md +88 -11
- data/docs/04_Compiling.md +2 -2
- data/docs/05_DesignNotes.md +1 -1
- data/docs/06_Cheatsheet.md +3 -3
- data/docs/07_StepByStep.ja.md +14 -15
- data/docs/07_StepByStep.md +14 -15
- data/docs/08_LanguageReference.md +428 -0
- data/lib/carray/jit/analyzer.rb +144 -7
- data/lib/carray/jit/c_function.rb +359 -21
- data/lib/carray/jit/c_generator.rb +398 -58
- data/lib/carray/jit/compiler.rb +27 -3
- data/lib/carray/jit/expression.rb +271 -33
- data/lib/carray/jit/kernel.rb +48 -9
- data/lib/carray/jit/node.rb +15 -0
- data/lib/carray/jit/type_assignment.rb +53 -1
- data/lib/carray/jit/version.rb +1 -1
- data/lib/carray/jit.rb +223 -87
- metadata +4 -3
data/lib/carray/jit/compiler.rb
CHANGED
|
@@ -56,11 +56,32 @@ class CArray
|
|
|
56
56
|
# only where it means something -- and Apple's linker rejects it
|
|
57
57
|
# outright, which would trade a Linux crash for a macOS build that never
|
|
58
58
|
# compiles at all.
|
|
59
|
+
#
|
|
60
|
+
# A compiled `jit_function` is the exception and exports no fixed name:
|
|
61
|
+
# its body, its helpers and the four words a window reads are static,
|
|
62
|
+
# and what leaves the object is the declared symbol, the shim beside it
|
|
63
|
+
# and the accessor -- all three carrying the body's own digest. That is
|
|
64
|
+
# what lets carray-jit-aot link several of them into one library.
|
|
59
65
|
SYMBOLIC =
|
|
60
66
|
(RbConfig::CONFIG["host_os"] =~ /darwin|mswin|mingw|cygwin/ ?
|
|
61
67
|
[] : ["-Wl,-Bsymbolic"]).freeze
|
|
62
68
|
|
|
63
|
-
|
|
69
|
+
# -fwrapv makes signed overflow wrap, as CArray's own operators do and
|
|
70
|
+
# as the documentation promises. Without it the compiler is free to
|
|
71
|
+
# assume a sum cannot overflow, so one kernel contradicts itself:
|
|
72
|
+
# `y = -a; y > 0 ? y : 0` stored a negative value for a = INT64_MIN.
|
|
73
|
+
FLAGS = ["-O3", "-fPIC", "-shared", "-ffp-contract=off", "-fwrapv", *SYMBOLIC].freeze
|
|
74
|
+
|
|
75
|
+
# A compiled body calls a function borrowed with `jit_extern` by name,
|
|
76
|
+
# and the library it came from is one the process opened -- which is
|
|
77
|
+
# where the name is resolved, when the object is loaded. ELF leaves a
|
|
78
|
+
# shared object's undefined names to the loader as it is; Apple's
|
|
79
|
+
# linker refuses them unless told to, so a borrowed function from
|
|
80
|
+
# anywhere but libm (which `-lm` names) failed to link on macOS.
|
|
81
|
+
# `jit_extern` has opened the library before any body that calls it
|
|
82
|
+
# is compiled, so by the time the object loads the name is there.
|
|
83
|
+
LINK = (RbConfig::CONFIG["host_os"] =~ /darwin/ ?
|
|
84
|
+
["-undefined", "dynamic_lookup"] : []).freeze
|
|
64
85
|
|
|
65
86
|
# Kernels retained on disk. Each costs about 17 KB, so the default is
|
|
66
87
|
# roughly 9 MB -- far more than any real program compiles, but a bound
|
|
@@ -142,7 +163,9 @@ class CArray
|
|
|
142
163
|
if defined?(Gem::Specification)
|
|
143
164
|
begin
|
|
144
165
|
Gem::Specification.find_by_name("carray").version.to_s
|
|
145
|
-
rescue StandardError
|
|
166
|
+
rescue StandardError, LoadError
|
|
167
|
+
# A gem that is not installed raises Gem::MissingSpecError,
|
|
168
|
+
# which is a LoadError rather than a StandardError.
|
|
146
169
|
"none"
|
|
147
170
|
end
|
|
148
171
|
else
|
|
@@ -537,7 +560,8 @@ class CArray
|
|
|
537
560
|
"Set CARRAY_JIT_CC to a C compiler, or leave it unset for " \
|
|
538
561
|
"the one Ruby was built with."
|
|
539
562
|
end
|
|
540
|
-
command = [compiler_command, *flags, source_path, "-o", object_path,
|
|
563
|
+
command = [compiler_command, *flags, source_path, "-o", object_path,
|
|
564
|
+
"-lm", *LINK]
|
|
541
565
|
output = IO.popen(command, err: [:child, :out]) { |io| io.read }
|
|
542
566
|
unless $?.success?
|
|
543
567
|
raise CompilationError, "#{command.join(' ')}\n#{output}"
|
|
@@ -11,8 +11,15 @@ class CArray
|
|
|
11
11
|
# nothing here restates any of that: it substitutes operands into bodies
|
|
12
12
|
# it was given and wraps the result in a loop.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
14
|
+
# An operand need not be laid out end to end. A slice, a transpose or a
|
|
15
|
+
# broadcast column is read where it lies, by the byte strides CArray
|
|
16
|
+
# lends for it -- a broadcast axis is a stride of 0 -- and a view with no
|
|
17
|
+
# strides of its own is lent as a packed copy of it. The strides are
|
|
18
|
+
# handed over at the call, so one kernel serves every view of the same
|
|
19
|
+
# kind; which operands are read that way is part of what it is cached by.
|
|
20
|
+
#
|
|
21
|
+
# Declining is ordinary. An expression this cannot address -- a data
|
|
22
|
+
# type with no C to write it in, a destination whose cells overlap --
|
|
16
23
|
# goes back to CArray, which walks it and arrives at the same answer.
|
|
17
24
|
class Expression
|
|
18
25
|
|
|
@@ -54,14 +61,22 @@ class CArray
|
|
|
54
61
|
def call (plan, out)
|
|
55
62
|
return false unless C_TYPES.key?(plan.data_type)
|
|
56
63
|
aliased = plan.leaves.any? { |array| array.equal?(out) }
|
|
57
|
-
|
|
64
|
+
# A shifted read reaches cells other than the one being written, so
|
|
65
|
+
# over its own output it would read what it has just written.
|
|
66
|
+
return false if aliased && shifted_leaves(plan).any? { |i| plan.leaves[i].equal?(out) }
|
|
67
|
+
return false unless out.dim == plan.dim
|
|
58
68
|
arrays = [out, *plan.leaves]
|
|
59
69
|
writable = [true, *Array.new(plan.leaves.size, false)]
|
|
60
70
|
Access.open(arrays, writable) do |bases|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
strided = strided_slots(arrays, bases) or return false
|
|
72
|
+
kernel = kernel_for(plan, aliased, strided) or return false
|
|
73
|
+
# Held here so that the buffers outlive the call.
|
|
74
|
+
dim = out.dim.pack("q*")
|
|
75
|
+
steps = strided.flat_map { |slot| slot_strides(bases, slot) }.pack("q*")
|
|
76
|
+
args = [out.elements]
|
|
77
|
+
args << Fiddle::Pointer[dim] if walks_axes?(plan, strided)
|
|
78
|
+
args << Fiddle::Pointer[steps] unless strided.empty?
|
|
79
|
+
kernel.call(*args, *pointers(plan, bases))
|
|
65
80
|
end
|
|
66
81
|
true
|
|
67
82
|
rescue ZeroDivisionError
|
|
@@ -88,27 +103,46 @@ class CArray
|
|
|
88
103
|
private
|
|
89
104
|
|
|
90
105
|
# An expression of the same shape compiles to the same kernel whatever
|
|
91
|
-
# arrays it is over, which is what the plan's signature says
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
# arrays it is over, which is what the plan's signature says -- given
|
|
107
|
+
# the same operands read by strides. The strides themselves are
|
|
108
|
+
# arguments, not part of the kernel.
|
|
109
|
+
def kernel_for (plan, aliased, strided)
|
|
110
|
+
key = [plan.signature, aliased, strided]
|
|
111
|
+
@kernels.fetch(key) do
|
|
112
|
+
@kernels[key] = compile(plan, aliased, strided)
|
|
95
113
|
end
|
|
96
114
|
end
|
|
97
115
|
|
|
98
|
-
def compile (plan, aliased)
|
|
116
|
+
def compile (plan, aliased, strided)
|
|
117
|
+
@strided = strided
|
|
99
118
|
source = source_for(plan, aliased) or return nil
|
|
100
119
|
handle, = Compiler.build(source, "carray_jit_expression", flags: FLAGS)
|
|
101
120
|
Fiddle::Function.new(handle["carray_jit_expression"],
|
|
102
121
|
[Fiddle::TYPE_LONG_LONG] +
|
|
103
|
-
[Fiddle::TYPE_VOIDP] * (1 + arity(plan)),
|
|
122
|
+
[Fiddle::TYPE_VOIDP] * (1 + arity(plan, strided)),
|
|
104
123
|
Fiddle::TYPE_VOID)
|
|
105
124
|
rescue CompilationError
|
|
106
125
|
nil
|
|
107
126
|
end
|
|
108
127
|
|
|
109
|
-
def arity (plan)
|
|
128
|
+
def arity (plan, strided)
|
|
110
129
|
plan.leaves.size + plan.leaves.count { |array| array.has_mask? } +
|
|
111
|
-
(plan.masked ? 1 : 0)
|
|
130
|
+
(plan.masked ? 1 : 0) + (walks_axes?(plan, strided) ? 1 : 0) +
|
|
131
|
+
(strided.empty? ? 0 : 1)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# A plan that reads an operand shifted, or by strides, needs to know
|
|
135
|
+
# where along each axis the cell is; any other is one flat loop.
|
|
136
|
+
def walks_axes? (plan, strided)
|
|
137
|
+
shifted?(plan) || ! strided.empty?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def shifted? (plan)
|
|
141
|
+
plan.nodes.any? { |node| node.is_a?(CArray::Fusion::Shifted) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def shifted_leaves (plan)
|
|
145
|
+
plan.nodes.grep(CArray::Fusion::Shifted).map(&:index)
|
|
112
146
|
end
|
|
113
147
|
|
|
114
148
|
def pointers (plan, bases)
|
|
@@ -121,23 +155,80 @@ class CArray
|
|
|
121
155
|
args
|
|
122
156
|
end
|
|
123
157
|
|
|
124
|
-
def end_to_end (
|
|
125
|
-
steps = Array.new(
|
|
126
|
-
step =
|
|
127
|
-
(
|
|
158
|
+
def end_to_end (dim, bytes)
|
|
159
|
+
steps = Array.new(dim.size)
|
|
160
|
+
step = bytes
|
|
161
|
+
(dim.size - 1).downto(0) do |axis|
|
|
128
162
|
steps[axis] = step
|
|
129
|
-
step *=
|
|
163
|
+
step *= dim[axis]
|
|
130
164
|
end
|
|
131
165
|
steps
|
|
132
166
|
end
|
|
133
167
|
|
|
168
|
+
# The C names of the pointers that are not laid out end to end, in the
|
|
169
|
+
# order their strides are handed over: the destination, its mask, then
|
|
170
|
+
# each operand and its mask. Nil where the destination has two cells
|
|
171
|
+
# at one address -- writing them one at a time is not what the walk
|
|
172
|
+
# does -- or where an operand is not the shape of the destination.
|
|
173
|
+
def strided_slots (arrays, bases)
|
|
174
|
+
slots = []
|
|
175
|
+
bases.each_with_index do |basis, i|
|
|
176
|
+
return nil unless basis[:dim] == arrays[0].dim
|
|
177
|
+
data, mask = i == 0 ? ["out", "out_mask"] : ["a#{i - 1}", "k#{i - 1}"]
|
|
178
|
+
if i == 0
|
|
179
|
+
return nil unless one_to_one?(basis[:strides], basis[:dim], basis[:bytes])
|
|
180
|
+
return nil if basis[:mask_strides] && ! one_to_one?(basis[:mask_strides], basis[:dim], 1)
|
|
181
|
+
end
|
|
182
|
+
slots << data unless basis[:strides] == end_to_end(basis[:dim], basis[:bytes])
|
|
183
|
+
if basis[:mask_strides] && basis[:mask_strides] != end_to_end(basis[:dim], 1)
|
|
184
|
+
slots << mask
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
slots.freeze
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def slot_strides (bases, slot)
|
|
191
|
+
index = slot.start_with?("out") ? 0 : slot[1..].to_i + 1
|
|
192
|
+
slot.start_with?("k", "out_mask") ? bases[index][:mask_strides] : bases[index][:strides]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Whether every cell has an address of its own. Taken axis by axis
|
|
196
|
+
# from the shortest stride up, each has to step past everything the
|
|
197
|
+
# ones below it reach. A layout that interleaves axes without sharing
|
|
198
|
+
# an address fails this too, and is declined though it need not be.
|
|
199
|
+
def one_to_one? (strides, dim, bytes)
|
|
200
|
+
reach = bytes
|
|
201
|
+
strides.zip(dim).reject { |_, d| d == 1 }.sort_by { |s, _| s.abs }.all? do |s, d|
|
|
202
|
+
fits = s.abs >= reach
|
|
203
|
+
reach += s.abs * (d - 1)
|
|
204
|
+
fits
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# A cell of the array `slot` names: at the flat index `flat` where it
|
|
209
|
+
# is laid out end to end, and at the sum of `axes` times its strides
|
|
210
|
+
# where it is not.
|
|
211
|
+
def cell (slot, type, flat, axes)
|
|
212
|
+
return "#{slot}[#{flat}]" unless @strided.include?(slot)
|
|
213
|
+
offset = axes.each_with_index.map { |a, k| "(#{a}) * s_#{slot}_#{k}" }.join(" + ")
|
|
214
|
+
qualifier = slot.start_with?("out") ? "" : "const "
|
|
215
|
+
"*(#{qualifier}#{type} *) ((#{qualifier}char *) #{slot} + #{offset})"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def axes (plan)
|
|
219
|
+
(0...plan.dim.size).map { |k| "i#{k}" }
|
|
220
|
+
end
|
|
221
|
+
|
|
134
222
|
# -- the C ------------------------------------------------------------
|
|
135
223
|
|
|
136
224
|
def source_for (plan, aliased)
|
|
137
225
|
out_type = C_TYPES.fetch(plan.data_type)
|
|
138
226
|
restrict = aliased ? "" : "restrict "
|
|
139
|
-
body = plan
|
|
140
|
-
|
|
227
|
+
body = statements(plan, :edge) or return nil
|
|
228
|
+
last = plan.nodes.size - 1
|
|
229
|
+
result = ["#{cell("out", out_type, "n", axes(plan))} = v#{last};",
|
|
230
|
+
*(plan.masked ? ["#{cell("out_mask", "uint8_t", "n", axes(plan))} = m#{last};"] : [])]
|
|
231
|
+
walking = walks_axes?(plan, @strided)
|
|
141
232
|
<<~C
|
|
142
233
|
#include <math.h>
|
|
143
234
|
#include <stdlib.h>
|
|
@@ -153,17 +244,92 @@ class CArray
|
|
|
153
244
|
typedef float float32_t;
|
|
154
245
|
typedef double float64_t;
|
|
155
246
|
|
|
247
|
+
#{helpers}
|
|
156
248
|
void
|
|
157
|
-
carray_jit_expression (int64_t elements, #{out_type} *#{restrict}out#{mask_parameter(plan, restrict)}#{parameters(plan, restrict)})
|
|
249
|
+
carray_jit_expression (int64_t elements#{walking ? ", const int64_t *dim" : ""}#{@strided.empty? ? "" : ", const int64_t *steps"}, #{out_type} *#{restrict}out#{mask_parameter(plan, restrict)}#{parameters(plan, restrict)})
|
|
158
250
|
{
|
|
159
|
-
|
|
160
|
-
#{body
|
|
161
|
-
out[n] = v#{plan.nodes.size - 1};#{plan.masked ? "\n out_mask[n] = m#{plan.nodes.size - 1};" : ""}
|
|
162
|
-
}
|
|
251
|
+
#{stride_lines(plan).join("\n")}
|
|
252
|
+
#{loop(plan, body + result, walking && (statements(plan, :inside) + result)).join("\n")}
|
|
163
253
|
}
|
|
164
254
|
C
|
|
165
255
|
end
|
|
166
256
|
|
|
257
|
+
# The C for every node, in order. `where` matters only to a shifted
|
|
258
|
+
# read: at the :edge it may fall outside the array, :inside it cannot.
|
|
259
|
+
def statements (plan, where)
|
|
260
|
+
@where = where
|
|
261
|
+
body = plan.nodes.each_with_index.map { |node, i| line(plan, node, i) }
|
|
262
|
+
body.any?(&:nil?) ? nil : body.flatten
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# The strides handed over, one per axis for each slot, named.
|
|
266
|
+
def stride_lines (plan)
|
|
267
|
+
ndim = plan.dim.size
|
|
268
|
+
@strided.each_with_index.flat_map { |slot, j|
|
|
269
|
+
(0...ndim).map { |k| " const int64_t s_#{slot}_#{k} = steps[#{j * ndim + k}];" }
|
|
270
|
+
}
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Cell by cell in storage order. A plan that reads an array shifted
|
|
274
|
+
# or by strides needs to know where along each axis the cell is, so it
|
|
275
|
+
# walks the outer axes and then the last. A shifted read splits the
|
|
276
|
+
# last one in three: the stretch in the middle, where no shifted read
|
|
277
|
+
# can leave the array, is a plain loop the compiler can vectorise, and
|
|
278
|
+
# only the ends check.
|
|
279
|
+
def loop (plan, edge, inside = nil)
|
|
280
|
+
unless inside
|
|
281
|
+
return [" for ( int64_t n = 0; n < elements; n++ ) {",
|
|
282
|
+
*edge.map { |l| " " + l }, " }"]
|
|
283
|
+
end
|
|
284
|
+
ndim = plan.dim.size
|
|
285
|
+
last = ndim - 1
|
|
286
|
+
offsets = plan.nodes.grep(CArray::Fusion::Shifted).map(&:offset)
|
|
287
|
+
lines = (0...ndim).map { |k| " const int64_t d#{k} = dim[#{k}];" }
|
|
288
|
+
plan.nodes.each_with_index do |node, i|
|
|
289
|
+
next unless node.is_a?(CArray::Fusion::Shifted)
|
|
290
|
+
flat = (1...ndim).reduce("(int64_t)(#{node.offset[0]})") { |acc, k|
|
|
291
|
+
"(#{acc}) * d#{k} + (#{node.offset[k]})"
|
|
292
|
+
}
|
|
293
|
+
lines << " const int64_t o#{i} = #{flat};"
|
|
294
|
+
end
|
|
295
|
+
(0...ndim).each do |k|
|
|
296
|
+
break if offsets.empty?
|
|
297
|
+
below = [0, *offsets.map { |o| -o[k] }].max
|
|
298
|
+
above = [0, *offsets.map { |o| o[k] }].max
|
|
299
|
+
lines << " const int64_t lo#{k} = d#{k} < #{below} ? d#{k} : #{below};"
|
|
300
|
+
lines << " const int64_t hi#{k} = d#{k} - #{above} > lo#{k} ? d#{k} - #{above} : lo#{k};"
|
|
301
|
+
end
|
|
302
|
+
indent = " "
|
|
303
|
+
(0...last).each do |k|
|
|
304
|
+
lines << "#{indent}for ( int64_t i#{k} = 0; i#{k} < d#{k}; i#{k}++ ) {"
|
|
305
|
+
indent += " "
|
|
306
|
+
end
|
|
307
|
+
row = (0...last).reduce("(int64_t) 0") { |acc, k| "(#{acc}) * d#{k} + i#{k}" }
|
|
308
|
+
row = "(#{row}) * d#{last}"
|
|
309
|
+
outer = (0...last).map { |k| "i#{k} >= lo#{k} && i#{k} < hi#{k}" }
|
|
310
|
+
lines << "#{indent}const int64_t row = #{row};"
|
|
311
|
+
unless offsets.empty?
|
|
312
|
+
lines << "#{indent}const int64_t a = #{outer.empty? ? "lo#{last}" : "(#{outer.join(" && ")}) ? lo#{last} : d#{last}"};"
|
|
313
|
+
lines << "#{indent}const int64_t b = a < hi#{last} ? hi#{last} : a;"
|
|
314
|
+
end
|
|
315
|
+
stretches = if offsets.empty?
|
|
316
|
+
[["0", "d#{last}", edge]]
|
|
317
|
+
else
|
|
318
|
+
[["0", "a", edge], ["a", "b", inside], ["b", "d#{last}", edge]]
|
|
319
|
+
end
|
|
320
|
+
stretches.each do |from, to, body|
|
|
321
|
+
lines << "#{indent}for ( int64_t i#{last} = #{from}; i#{last} < #{to}; i#{last}++ ) {"
|
|
322
|
+
lines << "#{indent} const int64_t n = row + i#{last};"
|
|
323
|
+
lines.concat(body.map { |l| "#{indent} " + l })
|
|
324
|
+
lines << "#{indent}}"
|
|
325
|
+
end
|
|
326
|
+
(last - 1).downto(0) do |k|
|
|
327
|
+
indent = indent[2..]
|
|
328
|
+
lines << "#{indent}}"
|
|
329
|
+
end
|
|
330
|
+
lines
|
|
331
|
+
end
|
|
332
|
+
|
|
167
333
|
def mask_parameter (plan, restrict)
|
|
168
334
|
plan.masked ? ", uint8_t *#{restrict}out_mask" : ""
|
|
169
335
|
end
|
|
@@ -180,31 +346,100 @@ class CArray
|
|
|
180
346
|
type = C_TYPES[node.data_type] or return nil
|
|
181
347
|
case node
|
|
182
348
|
when CArray::Fusion::Leaf
|
|
183
|
-
["#{type} v#{i} = a#{node.index}
|
|
184
|
-
*(plan.masked ? ["uint8_t m#{i} = #{node.masked ? "k#{node.index}
|
|
349
|
+
["#{type} v#{i} = #{cell("a#{node.index}", type, "n", axes(plan))};",
|
|
350
|
+
*(plan.masked ? ["uint8_t m#{i} = #{node.masked ? cell("k#{node.index}", "uint8_t", "n", axes(plan)) : "0"};"] : [])]
|
|
351
|
+
when CArray::Fusion::Shifted
|
|
352
|
+
shifted_line(plan, node, i, type)
|
|
185
353
|
when CArray::Fusion::Const
|
|
186
354
|
["#{type} v#{i} = #{literal(node)};",
|
|
187
355
|
*(plan.masked ? ["uint8_t m#{i} = 0;"] : [])]
|
|
188
356
|
when CArray::Fusion::Op
|
|
189
357
|
statement = substitute(node, i, type) or return nil
|
|
190
|
-
|
|
358
|
+
# A mask rule this gem does not know is a plan it declines.
|
|
359
|
+
mask = plan.masked ? (mask_line(node, i) or return nil) : nil
|
|
360
|
+
[*mask,
|
|
191
361
|
"#{type} v#{i};",
|
|
192
362
|
*guarded(node, i, statement, plan.masked)]
|
|
193
363
|
end
|
|
194
364
|
end
|
|
195
365
|
|
|
366
|
+
# The cell `offset` away, read from an index clamped into the array so
|
|
367
|
+
# that the load is always in bounds, then replaced where it fell out.
|
|
368
|
+
def shifted_line (plan, node, i, type)
|
|
369
|
+
bounds = node.bounds.uniq
|
|
370
|
+
return nil unless bounds.size == 1
|
|
371
|
+
ndim = node.offset.size
|
|
372
|
+
if @where == :inside
|
|
373
|
+
moved = (0...ndim).map { |k| "i#{k} + (#{node.offset[k]})" }
|
|
374
|
+
return ["#{type} v#{i} = #{cell("a#{node.index}", type, "n + o#{i}", moved)};",
|
|
375
|
+
*(plan.masked ? ["uint8_t m#{i} = #{node.masked ? cell("k#{node.index}", "uint8_t", "n + o#{i}", moved) : "0"};"] : [])]
|
|
376
|
+
end
|
|
377
|
+
index = (0...ndim).map { |k| "s#{i}_#{k}" }
|
|
378
|
+
lines = node.offset.each_with_index.map { |o, k|
|
|
379
|
+
"int64_t #{index[k]} = i#{k} + (#{o});"
|
|
380
|
+
}
|
|
381
|
+
inside = (0...ndim).map { |k| "#{index[k]} >= 0 && #{index[k]} < d#{k}" }.join(" && ")
|
|
382
|
+
lines << "int s#{i}_in = #{inside};"
|
|
383
|
+
(0...ndim).each { |k|
|
|
384
|
+
lines << "int64_t c#{i}_#{k} = #{index[k]} < 0 ? 0 : (#{index[k]} >= d#{k} ? d#{k} - 1 : #{index[k]});"
|
|
385
|
+
}
|
|
386
|
+
flat = (1...ndim).reduce("c#{i}_0") { |acc, k| "(#{acc}) * d#{k} + c#{i}_#{k}" }
|
|
387
|
+
clamped = (0...ndim).map { |k| "c#{i}_#{k}" }
|
|
388
|
+
lines << "#{type} v#{i} = #{cell("a#{node.index}", type, flat, clamped)};"
|
|
389
|
+
if bounds.first == :fill
|
|
390
|
+
fill = CArray::Fusion::Const.new(node.fill, node.data_type)
|
|
391
|
+
lines << "if ( ! s#{i}_in ) { v#{i} = #{literal(fill)}; }"
|
|
392
|
+
end
|
|
393
|
+
if plan.masked
|
|
394
|
+
inner = node.masked ? cell("k#{node.index}", "uint8_t", flat, clamped) : "0"
|
|
395
|
+
outer = bounds.first == :mask ? "1" : "0"
|
|
396
|
+
lines << "uint8_t m#{i} = s#{i}_in ? #{inner} : #{outer};"
|
|
397
|
+
end
|
|
398
|
+
lines
|
|
399
|
+
end
|
|
400
|
+
|
|
196
401
|
def literal (node)
|
|
197
402
|
case node.data_type
|
|
198
|
-
when :float64, :float32 then
|
|
199
|
-
when :boolean then node.value ? 1 : 0
|
|
403
|
+
when :float64, :float32 then float_literal(node.value.to_f)
|
|
404
|
+
when :boolean then truth(node.value) ? 1 : 0
|
|
200
405
|
else node.value.to_s
|
|
201
406
|
end
|
|
202
407
|
end
|
|
203
408
|
|
|
409
|
+
# `%.17g` writes -0.0 as "-0", which C reads as the integer 0 negated,
|
|
410
|
+
# and writes NaN and the infinities as words C does not know.
|
|
411
|
+
def float_literal (value)
|
|
412
|
+
return "NAN" if value.nan?
|
|
413
|
+
return (value > 0 ? "INFINITY" : "(-INFINITY)") if value.infinite?
|
|
414
|
+
text = "%.17g" % value
|
|
415
|
+
text.match?(/[.e]/) ? text : "#{text}.0"
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# A boolean cell as CArray stores it: 0 is false, as is false.
|
|
419
|
+
def truth (value)
|
|
420
|
+
value.is_a?(Numeric) ? value != 0 : !!value
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# `<type>` names a helper by CArray's spelling of the type -- the
|
|
424
|
+
# float64_t of op_powi_float64_t -- not by its C spelling.
|
|
204
425
|
def substitute (node, i, type)
|
|
205
426
|
text = node.body.dup
|
|
206
427
|
node.args.each_with_index { |arg, k| text = text.gsub("##{k + 1}", "v#{arg}") }
|
|
207
|
-
text.gsub("##{node.args.size + 1}", "v#{i}")
|
|
428
|
+
text.gsub("##{node.args.size + 1}", "v#{i}")
|
|
429
|
+
.gsub("<type>", carray_type(node.data_type, type)).lines.map(&:strip)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def carray_type (data_type, c_type)
|
|
433
|
+
case data_type
|
|
434
|
+
when :float32, :float64 then "#{data_type}_t"
|
|
435
|
+
else c_type
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# The C a body may call beyond the standard library, as CArray hands it
|
|
440
|
+
# over.
|
|
441
|
+
def helpers
|
|
442
|
+
CArray.__kernel_helpers__
|
|
208
443
|
end
|
|
209
444
|
|
|
210
445
|
# A masked cell is not computed where computing it would raise: the
|
|
@@ -220,6 +455,9 @@ class CArray
|
|
|
220
455
|
case node.mask
|
|
221
456
|
when :pass then "uint8_t m#{i} = m#{args[0]};"
|
|
222
457
|
when :union then "uint8_t m#{i} = #{args.map { |a| "m#{a}" }.join(" | ")};"
|
|
458
|
+
# The condition's mask, else the mask of the operand it chooses.
|
|
459
|
+
when :select
|
|
460
|
+
"uint8_t m#{i} = m#{args[0]} | (v#{args[0]} ? m#{args[1]} : m#{args[2]});"
|
|
223
461
|
when :kleene_or, :kleene_and
|
|
224
462
|
known = if node.mask == :kleene_or
|
|
225
463
|
"((!m#{args[0]} && v#{args[0]}) || (!m#{args[1]} && v#{args[1]}))"
|
data/lib/carray/jit/kernel.rb
CHANGED
|
@@ -45,6 +45,7 @@ class CArray
|
|
|
45
45
|
@integers = generator.integers
|
|
46
46
|
@complexes = generator.complexes
|
|
47
47
|
@unsigned_integers = generator.unsigned_integers
|
|
48
|
+
@unsigned_captures = generator.unsigned_captures
|
|
48
49
|
# Per array, the axes read at an index only the running kernel knows,
|
|
49
50
|
# and the extents it checks them against.
|
|
50
51
|
@extent_slots = generator.extent_slots
|
|
@@ -55,6 +56,7 @@ class CArray
|
|
|
55
56
|
@written_arrays = analyzer.written_arrays
|
|
56
57
|
@array_ranks = analyzer.array_ranks
|
|
57
58
|
@inner_ranges = analyzer.inner_ranges
|
|
59
|
+
@unbounded_indices = analyzer.unbounded_indices
|
|
58
60
|
# An index the block wrote twice has two identifiers here; the
|
|
59
61
|
# messages speak the name it was written with.
|
|
60
62
|
@index_sources = analyzer.index_sources
|
|
@@ -233,9 +235,10 @@ class CArray
|
|
|
233
235
|
|
|
234
236
|
held = [buffer(reals), buffer(integers), buffer(functions),
|
|
235
237
|
buffer(data), masks, masks, error]
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
238
|
+
error = in_one_block(held) { |context, slots|
|
|
239
|
+
Sweep.call(slab_pointer, fsync, arrays, context)
|
|
240
|
+
slots.last[0, error.bytesize]
|
|
241
|
+
}
|
|
239
242
|
|
|
240
243
|
packed.each_with_index do |(array, item), index|
|
|
241
244
|
next if array.equal?(item)
|
|
@@ -497,11 +500,19 @@ class CArray
|
|
|
497
500
|
# counter, and the arrays' half, which is the shape and travels in the
|
|
498
501
|
# plan. Two methods because `#call` packs the first and takes the
|
|
499
502
|
# second from what it prepared earlier; `#sweep` still wants both.
|
|
503
|
+
# A captured Integer that meets a uint64 takes that width, and a
|
|
504
|
+
# negative one has none to take: it would compute as 2**64-1. CArray
|
|
505
|
+
# refuses the same operand on a uint64 array, and so does this.
|
|
500
506
|
def packed_scalar_integers (scalar_values)
|
|
501
|
-
@integers.map { |name|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
507
|
+
@integers.map { |name|
|
|
508
|
+
value = Integer(scalar_values.fetch(name))
|
|
509
|
+
if value.negative? && @unsigned_captures.include?(name)
|
|
510
|
+
raise RangeError, "#{value} is out of range for uint64 " \
|
|
511
|
+
"(`#{name}` meets a uint64)"
|
|
512
|
+
end
|
|
513
|
+
value
|
|
514
|
+
}.pack("q*") +
|
|
515
|
+
@unsigned_integers.map { |name| Integer(scalar_values.fetch(name)) }.pack("Q*")
|
|
505
516
|
end
|
|
506
517
|
|
|
507
518
|
def packed_extents (array_values)
|
|
@@ -548,6 +559,31 @@ class CArray
|
|
|
548
559
|
packed.empty? ? "\0" * 8 : packed
|
|
549
560
|
end
|
|
550
561
|
|
|
562
|
+
# Copies `parts` into one block of C memory, each part 8-byte aligned,
|
|
563
|
+
# after a table of their addresses, and yields a pointer to the table
|
|
564
|
+
# and a pointer to each part; the block is freed when this returns.
|
|
565
|
+
#
|
|
566
|
+
# For a call that is handed addresses inside other memory rather than
|
|
567
|
+
# arguments: a Ruby String's bytes may be moved by GC compaction, and
|
|
568
|
+
# an address taken from one and packed elsewhere is not one Ruby knows
|
|
569
|
+
# to keep in place. Memory from malloc is never moved.
|
|
570
|
+
def in_one_block (parts)
|
|
571
|
+
table = parts.size * Fiddle::SIZEOF_VOIDP
|
|
572
|
+
offsets = parts.each_with_object([]) { |part, list|
|
|
573
|
+
last = list.empty? ? table : list.last + parts[list.size - 1].bytesize
|
|
574
|
+
list << ((last + 7) & ~7)
|
|
575
|
+
}
|
|
576
|
+
size = offsets.last + parts.last.bytesize
|
|
577
|
+
Fiddle::Pointer.malloc(size, Fiddle::RUBY_FREE) do |block|
|
|
578
|
+
slots = parts.zip(offsets).map { |part, offset|
|
|
579
|
+
block[offset, part.bytesize] = part
|
|
580
|
+
block + offset
|
|
581
|
+
}
|
|
582
|
+
block[0, table] = slots.map(&:to_i).pack("Q*")
|
|
583
|
+
yield block, slots
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
|
|
551
587
|
# The xfer tier transfers a box rather than the whole view. The box
|
|
552
588
|
# is the loop range grown by how far the kernel reaches from the cell it
|
|
553
589
|
# is on -- per array, because two arrays in one kernel need not be read
|
|
@@ -609,6 +645,9 @@ class CArray
|
|
|
609
645
|
# In source order, outermost first, so an index a range is written
|
|
610
646
|
# over has its own range settled before it is read.
|
|
611
647
|
@inner_ranges.each do |name, (from, to, step)|
|
|
648
|
+
# A range read from a local or a cell has no span before the loop
|
|
649
|
+
# is reached; the subscripts it walks are checked at each access.
|
|
650
|
+
next if @unbounded_indices.include?(name)
|
|
612
651
|
step ||= 1
|
|
613
652
|
first = span_of(from, scalar_values, flat, ranges, name)
|
|
614
653
|
last = span_of(to, scalar_values, flat, ranges, name)
|
|
@@ -765,9 +804,9 @@ class CArray
|
|
|
765
804
|
unless array.is_a?(CArray)
|
|
766
805
|
raise Unsupported, "`#{name}` is not a CArray"
|
|
767
806
|
end
|
|
768
|
-
unless array.
|
|
807
|
+
unless array.ndim == array_rank(name)
|
|
769
808
|
raise Unsupported,
|
|
770
|
-
"`#{name}` has rank #{array.
|
|
809
|
+
"`#{name}` has rank #{array.ndim}, but is indexed with " \
|
|
771
810
|
"#{array_rank(name)} #{array_rank(name) == 1 ? 'index' : 'indices'}"
|
|
772
811
|
end
|
|
773
812
|
unless array.data_type_name == @storage_types.fetch(name)
|
data/lib/carray/jit/node.rb
CHANGED
|
@@ -153,6 +153,21 @@ class CArray
|
|
|
153
153
|
end
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
+
# Whether a pointer parameter was handed an address at all: `grad.nil?`,
|
|
157
|
+
# `grad == nil`, and `if grad` where a condition is read. A callback's
|
|
158
|
+
# caller decides that per call -- NLopt hands a derivative-free method's
|
|
159
|
+
# objective no gradient to fill -- so it is a question the body asks,
|
|
160
|
+
# not one the declaration can settle. `null` is true for the spelling
|
|
161
|
+
# that asks whether it is missing.
|
|
162
|
+
class PointerTest < Node
|
|
163
|
+
attr_reader :name, :null
|
|
164
|
+
def initialize (name, null, location = nil)
|
|
165
|
+
super(location)
|
|
166
|
+
@name = name
|
|
167
|
+
@null = null
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
156
171
|
# Zero of whatever type another expression has.
|
|
157
172
|
#
|
|
158
173
|
# A sum has to start from a zero of the summand's type: start it from an
|