carray-jit 0.1.3 → 0.1.4
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 +146 -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 +427 -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 +17 -4
- 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 +7 -33
- 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
|
|
@@ -497,11 +499,19 @@ class CArray
|
|
|
497
499
|
# counter, and the arrays' half, which is the shape and travels in the
|
|
498
500
|
# plan. Two methods because `#call` packs the first and takes the
|
|
499
501
|
# second from what it prepared earlier; `#sweep` still wants both.
|
|
502
|
+
# A captured Integer that meets a uint64 takes that width, and a
|
|
503
|
+
# negative one has none to take: it would compute as 2**64-1. CArray
|
|
504
|
+
# refuses the same operand on a uint64 array, and so does this.
|
|
500
505
|
def packed_scalar_integers (scalar_values)
|
|
501
|
-
@integers.map { |name|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
506
|
+
@integers.map { |name|
|
|
507
|
+
value = Integer(scalar_values.fetch(name))
|
|
508
|
+
if value.negative? && @unsigned_captures.include?(name)
|
|
509
|
+
raise RangeError, "#{value} is out of range for uint64 " \
|
|
510
|
+
"(`#{name}` meets a uint64)"
|
|
511
|
+
end
|
|
512
|
+
value
|
|
513
|
+
}.pack("q*") +
|
|
514
|
+
@unsigned_integers.map { |name| Integer(scalar_values.fetch(name)) }.pack("Q*")
|
|
505
515
|
end
|
|
506
516
|
|
|
507
517
|
def packed_extents (array_values)
|
|
@@ -609,6 +619,9 @@ class CArray
|
|
|
609
619
|
# In source order, outermost first, so an index a range is written
|
|
610
620
|
# over has its own range settled before it is read.
|
|
611
621
|
@inner_ranges.each do |name, (from, to, step)|
|
|
622
|
+
# A range read from a local or a cell has no span before the loop
|
|
623
|
+
# is reached; the subscripts it walks are checked at each access.
|
|
624
|
+
next if @unbounded_indices.include?(name)
|
|
612
625
|
step ||= 1
|
|
613
626
|
first = span_of(from, scalar_values, flat, ranges, name)
|
|
614
627
|
last = span_of(to, scalar_values, flat, ranges, 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
|
|
@@ -361,6 +361,8 @@ class CArray
|
|
|
361
361
|
when MaskTest
|
|
362
362
|
walk_subscripts(node.subscripts)
|
|
363
363
|
node.type = :boolean
|
|
364
|
+
when PointerTest
|
|
365
|
+
node.type = :boolean
|
|
364
366
|
when NumericPredicate
|
|
365
367
|
walk(node.operand)
|
|
366
368
|
node.type = :boolean
|
|
@@ -430,7 +432,9 @@ class CArray
|
|
|
430
432
|
node.alternative.each { |statement| walk(statement) }
|
|
431
433
|
@bindings = merge_bindings(from_consequent, @bindings)
|
|
432
434
|
when IntegerLiteral
|
|
433
|
-
|
|
435
|
+
# Above 2**63-1 a literal is a uint64, as a captured Integer is;
|
|
436
|
+
# where it then meets an int64 it is refused (#refuse_wide_literal).
|
|
437
|
+
node.type = TypeAssignment.integer_type(node.value)
|
|
434
438
|
when FloatLiteral
|
|
435
439
|
node.type = :double
|
|
436
440
|
when ImaginaryLiteral
|
|
@@ -565,6 +569,14 @@ class CArray
|
|
|
565
569
|
walk(node.right)
|
|
566
570
|
node.type =
|
|
567
571
|
if Analyzer::COMPARISON_OPERATORS.include?(node.operator)
|
|
572
|
+
# Compared in the wider of the two, which for a uint64 against a
|
|
573
|
+
# negative literal is a uint64 the literal does not fit.
|
|
574
|
+
if [node.left.type, node.right.type].include?(:uint64)
|
|
575
|
+
refuse_negative_literal(node.left, node.right)
|
|
576
|
+
refuse_negative_literal(node.right, node.left)
|
|
577
|
+
refuse_wide_literal(node.left, node.right)
|
|
578
|
+
refuse_wide_literal(node.right, node.left)
|
|
579
|
+
end
|
|
568
580
|
:boolean
|
|
569
581
|
elsif [:<<, :>>].include?(node.operator)
|
|
570
582
|
# A shift takes its count as a number and keeps the type of the
|
|
@@ -813,6 +825,11 @@ class CArray
|
|
|
813
825
|
"too. Store `.real`, `.imag` or `.abs`",
|
|
814
826
|
location)
|
|
815
827
|
end
|
|
828
|
+
if wanted == :int64 && expression.is_a?(IntegerLiteral) &&
|
|
829
|
+
expression.value > INT64_MAX
|
|
830
|
+
raise Unsupported.new(wide_literal_message(expression.value),
|
|
831
|
+
location)
|
|
832
|
+
end
|
|
816
833
|
return if !boolean?(wanted) && numeric?(given)
|
|
817
834
|
if boolean?(wanted) && expression.is_a?(IntegerLiteral) &&
|
|
818
835
|
[0, 1].include?(expression.value)
|
|
@@ -873,6 +890,8 @@ class CArray
|
|
|
873
890
|
return scalar.type if KIND_RANK[scalar_kind] > KIND_RANK[typed_kind]
|
|
874
891
|
if scalar_kind == typed_kind
|
|
875
892
|
refuse_wide_capture(scalar, typed)
|
|
893
|
+
refuse_negative_literal(scalar, typed)
|
|
894
|
+
refuse_wide_literal(scalar, typed)
|
|
876
895
|
# The scalar becomes that type rather than being widened to meet it,
|
|
877
896
|
# and says so: a literal emitted as a double would take the C
|
|
878
897
|
# expression back to double however this node is typed.
|
|
@@ -881,6 +900,37 @@ class CArray
|
|
|
881
900
|
typed.type
|
|
882
901
|
end
|
|
883
902
|
|
|
903
|
+
# A negative literal meeting a uint64 would take that width and compute
|
|
904
|
+
# as 2**64 minus its magnitude. CArray refuses the same operand on a
|
|
905
|
+
# uint64 array (`RangeError: -1 is out of range for uint64`); a captured
|
|
906
|
+
# one is refused when the kernel is called, since its value is not part
|
|
907
|
+
# of the kernel.
|
|
908
|
+
def refuse_negative_literal (scalar, typed)
|
|
909
|
+
return unless scalar.is_a?(IntegerLiteral) && scalar.value.negative? &&
|
|
910
|
+
typed.type == :uint64
|
|
911
|
+
raise Unsupported.new(
|
|
912
|
+
"#{scalar.value} is out of range for uint64: a negative Integer " \
|
|
913
|
+
"meeting a uint64 would compute as 2**64#{scalar.value}. CArray " \
|
|
914
|
+
"refuses the same operand. Convert the uint64 side first, " \
|
|
915
|
+
"`x.to_f` or a signed capture", scalar.location)
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
# An integer literal above 2**63-1 computed as an int64, where it would
|
|
919
|
+
# wrap to a negative number. In Ruby it is an Integer, which has no
|
|
920
|
+
# width; meeting a uint64 or a Float it keeps its value and is accepted.
|
|
921
|
+
def refuse_wide_literal (scalar, typed)
|
|
922
|
+
return unless scalar.is_a?(IntegerLiteral) &&
|
|
923
|
+
scalar.value > INT64_MAX && typed.type == :int64
|
|
924
|
+
raise Unsupported.new(wide_literal_message(scalar.value),
|
|
925
|
+
scalar.location)
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
def wide_literal_message (value)
|
|
929
|
+
"#{value} does not fit an int64, which is what it is computed in " \
|
|
930
|
+
"here, and would wrap to a negative number. Meet it with a uint64 " \
|
|
931
|
+
"or a Float, or store it into a uint64 array"
|
|
932
|
+
end
|
|
933
|
+
|
|
884
934
|
# A captured Integer above int64 meeting an integer, which absorption
|
|
885
935
|
# has no answer for: the scalar is supposed to take the other side's
|
|
886
936
|
# width, and this value did not come from a width -- it came from a Ruby
|
|
@@ -932,6 +982,8 @@ class CArray
|
|
|
932
982
|
# Nothing to check: no value is computed.
|
|
933
983
|
when MaskTest
|
|
934
984
|
# Nothing to check: it reads a mask byte.
|
|
985
|
+
when PointerTest
|
|
986
|
+
# Nothing to check: it compares an address with NULL.
|
|
935
987
|
when Clamp
|
|
936
988
|
node.children.each { |child| verify(child) }
|
|
937
989
|
types = node.children.map(&:type)
|
data/lib/carray/jit/version.rb
CHANGED