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,509 @@
|
|
|
1
|
+
require "fiddle"
|
|
2
|
+
require "carray/jit/sweep"
|
|
3
|
+
|
|
4
|
+
class CArray
|
|
5
|
+
module JIT
|
|
6
|
+
|
|
7
|
+
# A compiled kernel, bound to one set of array data types and one set of
|
|
8
|
+
# scalar types.
|
|
9
|
+
#
|
|
10
|
+
# Every kernel has the same C signature, so the Fiddle::Function shape is
|
|
11
|
+
# fixed and the per-kernel detail travels in the six buffers.
|
|
12
|
+
class CompiledKernel
|
|
13
|
+
|
|
14
|
+
# @!attribute [r] source
|
|
15
|
+
# @return [String] the block's Ruby source, as it was read.
|
|
16
|
+
# @!attribute [r] c_source
|
|
17
|
+
# @return [String] the C this kernel was compiled from.
|
|
18
|
+
# @!attribute [r] compiled
|
|
19
|
+
# @return [Boolean] whether this call invoked the compiler, rather
|
|
20
|
+
# than reusing a cached object.
|
|
21
|
+
attr_reader :source, :c_source, :arrays, :storage_types, :reals,
|
|
22
|
+
:integers, :complexes, :rank, :index_names, :written_arrays,
|
|
23
|
+
:compiled, :masked, :directions, :contracted_names,
|
|
24
|
+
:index_axes,
|
|
25
|
+
# How far a stencil's windows reach on each axis, as
|
|
26
|
+
# [lowest, highest] per axis: what the caller walks the
|
|
27
|
+
# interior by. Empty for a kernel that has no windows.
|
|
28
|
+
:window_reach,
|
|
29
|
+
# What a cell that stopped can have been raising about --
|
|
30
|
+
# the block's own `raise`s, and those of the bodies pasted
|
|
31
|
+
# into this kernel, by the code each reports.
|
|
32
|
+
:raise_messages
|
|
33
|
+
|
|
34
|
+
def initialize (source:, generator:, analyzer:, storage_types:)
|
|
35
|
+
@source = source
|
|
36
|
+
@c_source = generator.provenance + generator.generate
|
|
37
|
+
@arrays = generator.arrays
|
|
38
|
+
@reals = generator.reals
|
|
39
|
+
@integers = generator.integers
|
|
40
|
+
@complexes = generator.complexes
|
|
41
|
+
# Per array, the axes read at an index only the running kernel knows,
|
|
42
|
+
# and the extents it checks them against.
|
|
43
|
+
@extent_slots = generator.extent_slots
|
|
44
|
+
@dynamic_arrays = @extent_slots.map(&:first).uniq
|
|
45
|
+
@storage_types = storage_types
|
|
46
|
+
@rank = analyzer.rank
|
|
47
|
+
@index_names = analyzer.index_names
|
|
48
|
+
@written_arrays = analyzer.written_arrays
|
|
49
|
+
@array_ranks = analyzer.array_ranks
|
|
50
|
+
@inner_ranges = analyzer.inner_ranges
|
|
51
|
+
@axis_uses = @arrays.to_h { |array|
|
|
52
|
+
[array, (0...array_rank(array)).map { |axis| analyzer.axis_use(array, axis) }]
|
|
53
|
+
}
|
|
54
|
+
@contracted_names = analyzer.contracted_names
|
|
55
|
+
# Where each index appears, so that a contraction can take its extents
|
|
56
|
+
# from the arrays and say so when they disagree.
|
|
57
|
+
@index_axes = Hash.new { |hash, key| hash[key] = [] }
|
|
58
|
+
@axis_uses.each do |array, uses|
|
|
59
|
+
uses.each_with_index do |(walkers, _), axis|
|
|
60
|
+
walkers.each { |index, _, _| @index_axes[index] << [array, axis] }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
@masked = generator.masked
|
|
65
|
+
@window_reach = analyzer.window_reach
|
|
66
|
+
# What `raise` in the block said, by the code the cell that raised
|
|
67
|
+
# writes into the error slot. The message does not travel: C has
|
|
68
|
+
# nothing to carry it in, and it was known when this was compiled.
|
|
69
|
+
@raise_messages = generator.raise_messages
|
|
70
|
+
# Kept so the sweep entry point can be built from it if one is ever
|
|
71
|
+
# asked for. Compiling it eagerly would pay for a road most kernels
|
|
72
|
+
# never take.
|
|
73
|
+
@generator = generator
|
|
74
|
+
# The names of the C functions the block called, in the order their
|
|
75
|
+
# addresses are packed into the `functions` buffer. Names, not the
|
|
76
|
+
# functions themselves: a borrowed function is keyed on its signature
|
|
77
|
+
# alone, so this kernel is shared by every function of that shape and
|
|
78
|
+
# the address has to come from the call rather than from the build.
|
|
79
|
+
@c_function_names = generator.address_functions.keys
|
|
80
|
+
# Arrays handed to a C function whole, in the order their addresses
|
|
81
|
+
# are packed into `data`, and what each was promised to be.
|
|
82
|
+
@address_arrays = generator.address_arrays
|
|
83
|
+
@address_parameters = generator.address_parameters
|
|
84
|
+
# One translation unit holds both entry points -- the kernel and the
|
|
85
|
+
# wrapper CArray's sweep calls -- and one build produces both. They
|
|
86
|
+
# were two builds of the same body until the second was noticed: the
|
|
87
|
+
# wrapper's source already contains the kernel verbatim, so compiling
|
|
88
|
+
# it separately meant compiling everything twice and throwing the
|
|
89
|
+
# first object away whenever the pass swept, which is the usual case
|
|
90
|
+
# for jit_each. About 230 ms per call site, and an entry in the
|
|
91
|
+
# cache that was never opened.
|
|
92
|
+
@source_text = generator.provenance + generator.generate_slab(SLAB_NAME)
|
|
93
|
+
@handle, @compiled = Compiler.build(generator.generate_slab(SLAB_NAME),
|
|
94
|
+
CGenerator::FUNCTION_NAME,
|
|
95
|
+
header: generator.provenance)
|
|
96
|
+
@function = Fiddle::Function.new(@handle[CGenerator::FUNCTION_NAME],
|
|
97
|
+
[Fiddle::TYPE_VOIDP] * 10,
|
|
98
|
+
Fiddle::TYPE_VOID)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# `bounds` is one [start, limit, step] per axis.
|
|
102
|
+
#
|
|
103
|
+
# `border:` runs the frame's entry point instead of the interior's: the
|
|
104
|
+
# same statements, with a window that falls off the array answered by
|
|
105
|
+
# the rule the kernel was compiled for rather than by reading. Which is
|
|
106
|
+
# why the reach is not checked for one -- reaching outside is what it is
|
|
107
|
+
# for, and the C answers for it.
|
|
108
|
+
def call (array_values, scalar_values, bounds, c_function_values = {},
|
|
109
|
+
border: false)
|
|
110
|
+
arrays = @arrays.map { |name| array_values.fetch(name) }
|
|
111
|
+
ranges = index_ranges(bounds, scalar_values)
|
|
112
|
+
verify(arrays, bounds, ranges, scalar_values, reach: !border)
|
|
113
|
+
|
|
114
|
+
writable = @arrays.map { |name| @written_arrays.include?(name) }
|
|
115
|
+
error = [0].pack("l")
|
|
116
|
+
packed_bounds = bounds.flatten.pack("q*")
|
|
117
|
+
reals = packed_reals(scalar_values)
|
|
118
|
+
integers = (@integers.map { |name| Integer(scalar_values.fetch(name)) } +
|
|
119
|
+
@extent_slots.map { |name, axis| array_values.fetch(name).dim[axis] })
|
|
120
|
+
.pack("q*")
|
|
121
|
+
functions = @c_function_names.map { |name|
|
|
122
|
+
c_function_values.fetch(name).pointer.to_i
|
|
123
|
+
}.pack("Q*")
|
|
124
|
+
# An array handed over whole is walked contiguously by the C, so it is
|
|
125
|
+
# packed into an entity first and copied back after if the C may have
|
|
126
|
+
# written to it. This is the same lifecycle CFunction#call keeps, and for
|
|
127
|
+
# the same reason.
|
|
128
|
+
addressed = @address_arrays.map { |name|
|
|
129
|
+
[name, array_values.fetch(name)]
|
|
130
|
+
}
|
|
131
|
+
packed = addressed.map { |name, array| [array, address_buffer(name, array)] }
|
|
132
|
+
data = packed.map { |_, buffer|
|
|
133
|
+
Access.open([buffer], [false], [nil], [nil]) { |bases|
|
|
134
|
+
bases.first[:pointer]
|
|
135
|
+
}
|
|
136
|
+
}.pack("Q*")
|
|
137
|
+
|
|
138
|
+
box = region_box(ranges, scalar_values, array_values)
|
|
139
|
+
Access.open(arrays, writable, box[0], box[1]) do |bases|
|
|
140
|
+
pointers = bases.map { |basis| basis[:pointer] }.pack("Q*")
|
|
141
|
+
strides = bases.flat_map { |basis| basis[:strides] }.pack("q*")
|
|
142
|
+
mask_pointers = bases.map { |basis| basis[:mask_pointer] || 0 }.pack("Q*")
|
|
143
|
+
mask_strides = bases.flat_map { |basis|
|
|
144
|
+
basis[:mask_strides] || Array.new(@rank, 0)
|
|
145
|
+
}.pack("q*")
|
|
146
|
+
entry = border ? border_function : @function
|
|
147
|
+
entry.call(buffer(pointers), buffer(strides), buffer(packed_bounds),
|
|
148
|
+
buffer(reals), buffer(integers), buffer(functions),
|
|
149
|
+
buffer(data),
|
|
150
|
+
buffer(mask_pointers), buffer(mask_strides), error)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
packed.each_with_index do |(array, buffer), index|
|
|
154
|
+
next if array.equal?(buffer)
|
|
155
|
+
next if @address_parameters.fetch(@address_arrays[index]).all?(&:const)
|
|
156
|
+
array[] = buffer
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
report(error)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @private
|
|
163
|
+
SLAB_NAME = "carray_jit_slab"
|
|
164
|
+
|
|
165
|
+
# Runs the kernel as CArray's chunked sweep rather than through the
|
|
166
|
+
# addressing here: CArray acquires the operands, broadcasts them, ORs
|
|
167
|
+
# and propagates the masks, and calls back with a chunk at a time.
|
|
168
|
+
#
|
|
169
|
+
# `array_values` are the operands in the order the kernel addresses
|
|
170
|
+
# them, which is the order it packs `pointers` in -- so the same three
|
|
171
|
+
# arguments the kernel's own loop takes are the ones a chunk arrives as.
|
|
172
|
+
def sweep (array_values, scalar_values, c_function_values = {})
|
|
173
|
+
arrays = @arrays.map { |name| array_values.fetch(name) }
|
|
174
|
+
fsync = @arrays.map { |name|
|
|
175
|
+
@written_arrays.include?(name) ? "1" : "0"
|
|
176
|
+
}.join
|
|
177
|
+
|
|
178
|
+
error = [0].pack("l")
|
|
179
|
+
reals = packed_reals(scalar_values)
|
|
180
|
+
integers = (@integers.map { |name| Integer(scalar_values.fetch(name)) } +
|
|
181
|
+
@extent_slots.map { |name, axis|
|
|
182
|
+
array_values.fetch(name).dim[axis]
|
|
183
|
+
}).pack("q*")
|
|
184
|
+
functions = @c_function_names.map { |name|
|
|
185
|
+
c_function_values.fetch(name).pointer.to_i
|
|
186
|
+
}.pack("Q*")
|
|
187
|
+
addressed = @address_arrays.map { |name|
|
|
188
|
+
[name, array_values.fetch(name)]
|
|
189
|
+
}
|
|
190
|
+
packed = addressed.map { |name, array| [array, address_buffer(name, array)] }
|
|
191
|
+
data = packed.map { |_, item|
|
|
192
|
+
Access.open([item], [false], [nil], [nil]) { |bases|
|
|
193
|
+
bases.first[:pointer]
|
|
194
|
+
}
|
|
195
|
+
}.pack("Q*")
|
|
196
|
+
# The kernel declares these only when it reads a mask, and a body that
|
|
197
|
+
# reads one does not come this way; they are here because the
|
|
198
|
+
# signature has the slots.
|
|
199
|
+
masks = ("\0" * 8)
|
|
200
|
+
|
|
201
|
+
held = [buffer(reals), buffer(integers), buffer(functions),
|
|
202
|
+
buffer(data), masks, masks, error]
|
|
203
|
+
context = held.map { |item| Fiddle::Pointer[item].to_i }.pack("Q*")
|
|
204
|
+
|
|
205
|
+
Sweep.call(slab_pointer, fsync, arrays, Fiddle::Pointer[context])
|
|
206
|
+
|
|
207
|
+
packed.each_with_index do |(array, item), index|
|
|
208
|
+
next if array.equal?(item)
|
|
209
|
+
next if @address_parameters.fetch(@address_arrays[index]).all?(&:const)
|
|
210
|
+
array[] = item
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
report(error)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# The wrapper CArray's sweep calls, for the same reason #c_source is
|
|
217
|
+
# here: the generated C is the debugging surface. Built on demand,
|
|
218
|
+
# because a kernel that never sweeps never needs it.
|
|
219
|
+
def slab_source
|
|
220
|
+
@source_text
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# True when this kernel could be run as a sweep at all: the body must
|
|
224
|
+
# not ask about a mask, since a chunk carries no per-cell mask the
|
|
225
|
+
# generated code can test.
|
|
226
|
+
def sweepable?
|
|
227
|
+
!@masked
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
# What the cell that stopped said, raised now that Ruby has control
|
|
233
|
+
# back. C cannot raise, so a failure is a code in a slot and a loop
|
|
234
|
+
# that stops; this is where it becomes the exception the same body run
|
|
235
|
+
# in Ruby would have raised.
|
|
236
|
+
def report (error)
|
|
237
|
+
code = error.unpack1("l")
|
|
238
|
+
case code
|
|
239
|
+
when 0 then nil
|
|
240
|
+
when 1 then raise ZeroDivisionError, "divided by 0"
|
|
241
|
+
when 2 then raise IndexError, "index out of range"
|
|
242
|
+
else
|
|
243
|
+
message = @raise_messages[code]
|
|
244
|
+
# A code with no message behind it is this compiler's bug, not the
|
|
245
|
+
# block's, and says so rather than raising something the block
|
|
246
|
+
# looks responsible for.
|
|
247
|
+
raise Error, "the kernel reported #{code}, which is no failure it " \
|
|
248
|
+
"was compiled to report" unless message
|
|
249
|
+
raise RuntimeError, message
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def slab_pointer
|
|
254
|
+
@slab_pointer ||= @handle[SLAB_NAME]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Resolved when the frame is first walked rather than at build time: a
|
|
258
|
+
# kernel that has one is a stencil with a border rule, and even that one
|
|
259
|
+
# runs its interior first.
|
|
260
|
+
def border_function
|
|
261
|
+
@border_function ||=
|
|
262
|
+
Fiddle::Function.new(@handle[CGenerator::BORDER_NAME],
|
|
263
|
+
[Fiddle::TYPE_VOIDP] * 10, Fiddle::TYPE_VOID)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# What a C function may be handed as a pointer, and what has to be true
|
|
267
|
+
# of it. The checks are the declaration's own: the type it points at,
|
|
268
|
+
# the length it said it was, and whether it may be written through.
|
|
269
|
+
# The `reals` slots the kernel reads. A captured Complex travels as its
|
|
270
|
+
# two parts, after the reals, and the kernel puts it back together with
|
|
271
|
+
# CMPLX -- so both loops pack this the same way, which is why it is one
|
|
272
|
+
# method: #sweep once packed the reals alone, and a kernel that captured
|
|
273
|
+
# a Complex then read slots nothing had written.
|
|
274
|
+
def packed_reals (scalar_values)
|
|
275
|
+
(@reals.map { |name| Float(scalar_values.fetch(name)) } +
|
|
276
|
+
@complexes.flat_map { |name|
|
|
277
|
+
value = scalar_values.fetch(name)
|
|
278
|
+
[Float(value.real), Float(value.imaginary)]
|
|
279
|
+
}).pack("d*")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def address_buffer (name, array)
|
|
283
|
+
@address_parameters.fetch(name).each do |parameter|
|
|
284
|
+
wanted = CDeclaration::DATA_TYPES.fetch(parameter.element.fiddle)
|
|
285
|
+
unless array.data_type_name == wanted.to_s
|
|
286
|
+
raise Unsupported,
|
|
287
|
+
"`#{name}` is handed to a C function as `#{parameter.text}`, " \
|
|
288
|
+
"which takes a #{wanted} array, and `#{name}` is " \
|
|
289
|
+
"#{array.data_type_name}"
|
|
290
|
+
end
|
|
291
|
+
if parameter.sized? && array.elements < parameter.array
|
|
292
|
+
raise Unsupported,
|
|
293
|
+
"`#{name}` is handed to a C function as `#{parameter.text}`, " \
|
|
294
|
+
"which reads #{parameter.array} of them, and `#{name}` has " \
|
|
295
|
+
"#{array.elements}"
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
if array.has_mask?
|
|
299
|
+
# A masked cell's bytes are out of contract, and a C function has no
|
|
300
|
+
# mask to consult -- it would read whatever is underneath. What is
|
|
301
|
+
# refused is carrying a mask at all rather than having a cell under
|
|
302
|
+
# it, because the way through is the same either way: `#strip_mask`
|
|
303
|
+
# is where the caller says what the C should see there, and an array
|
|
304
|
+
# that masks nothing loses the mask and no values.
|
|
305
|
+
raise Unsupported,
|
|
306
|
+
"`#{name}` carries a mask and is handed to a C function, " \
|
|
307
|
+
"which has no mask to read; the values under a mask are not " \
|
|
308
|
+
"values, so `#{name}.strip_mask(fill)` is what says what the " \
|
|
309
|
+
"C should see there"
|
|
310
|
+
end
|
|
311
|
+
Access.classify(array)[:entity] ? array : array.copy
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# A pointer into an empty string is not something Fiddle can hand over,
|
|
315
|
+
# and a kernel with no scalars has an empty buffer.
|
|
316
|
+
def buffer (packed)
|
|
317
|
+
packed.empty? ? "\0" * 8 : packed
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# The region tier transfers a box rather than the whole view. The box
|
|
321
|
+
# is the loop range grown by how far the kernel reaches from the cell it
|
|
322
|
+
# is on -- per array, because two arrays in one kernel need not be read
|
|
323
|
+
# at the same offsets, and per axis, because they need not be read at
|
|
324
|
+
# the same offsets on each of them.
|
|
325
|
+
def region_box (ranges, scalars, array_values)
|
|
326
|
+
starts = []
|
|
327
|
+
counts = []
|
|
328
|
+
@arrays.each do |name|
|
|
329
|
+
per_axis_start = []
|
|
330
|
+
per_axis_count = []
|
|
331
|
+
# An index the kernel works out could reach any cell, so the box is
|
|
332
|
+
# the whole thing. For a view that has to be transferred that costs
|
|
333
|
+
# what a copy of it would have cost -- which is what the caller
|
|
334
|
+
# would otherwise have written by hand.
|
|
335
|
+
if @dynamic_arrays.include?(name)
|
|
336
|
+
starts << Array.new(array_rank(name), 0)
|
|
337
|
+
counts << array_values.fetch(name).dim.dup
|
|
338
|
+
next
|
|
339
|
+
end
|
|
340
|
+
@axis_uses.fetch(name).each do |walkers, pinned|
|
|
341
|
+
positions = pinned_positions(pinned, scalars)
|
|
342
|
+
# One box per axis, so an axis walked by two indices takes the box
|
|
343
|
+
# that covers both.
|
|
344
|
+
walkers.each do |index, offsets|
|
|
345
|
+
low, high = ranges.fetch(index)
|
|
346
|
+
next unless low < high
|
|
347
|
+
minimum, maximum = offset_span(offsets, scalars)
|
|
348
|
+
positions << low + minimum
|
|
349
|
+
positions << high - 1 + maximum
|
|
350
|
+
end
|
|
351
|
+
if positions.empty?
|
|
352
|
+
per_axis_start << 0
|
|
353
|
+
per_axis_count << 0
|
|
354
|
+
next
|
|
355
|
+
end
|
|
356
|
+
per_axis_start << positions.min
|
|
357
|
+
per_axis_count << positions.max - positions.min + 1
|
|
358
|
+
end
|
|
359
|
+
starts << per_axis_start
|
|
360
|
+
counts << per_axis_count
|
|
361
|
+
end
|
|
362
|
+
[starts, counts]
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def array_rank (array)
|
|
366
|
+
@array_ranks.fetch(array, @rank)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Every index's range: the outer ones from the extents, the inner ones
|
|
370
|
+
# from the range each `each` was written with, which is an integer
|
|
371
|
+
# expression over literals and captured scalars.
|
|
372
|
+
def index_ranges (bounds, scalar_values)
|
|
373
|
+
ranges = {}
|
|
374
|
+
@index_names.each_with_index do |name, axis|
|
|
375
|
+
ranges[name] = covered_span(bounds[axis])
|
|
376
|
+
end
|
|
377
|
+
flat = bounds.flatten
|
|
378
|
+
@inner_ranges.each do |name, (from, to)|
|
|
379
|
+
ranges[name] = [evaluate(from, scalar_values, flat),
|
|
380
|
+
evaluate(to, scalar_values, flat)]
|
|
381
|
+
end
|
|
382
|
+
ranges
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# The half-open span an axis actually touches. A step may skip cells,
|
|
386
|
+
# but every cell it lands on has to exist, so the span runs from the
|
|
387
|
+
# first index to the last one visited.
|
|
388
|
+
def covered_span (triple)
|
|
389
|
+
start, limit, step = triple
|
|
390
|
+
return [0, 0] if step.positive? ? start >= limit : start <= limit
|
|
391
|
+
count = ((limit - start).abs + step.abs - 1) / step.abs
|
|
392
|
+
last = start + step * (count - 1)
|
|
393
|
+
step.positive? ? [start, last + 1] : [last, start + 1]
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def evaluate (node, scalars, flat_bounds = nil)
|
|
397
|
+
case node
|
|
398
|
+
when IntegerLiteral then node.value
|
|
399
|
+
when CaptureRead then Integer(scalars.fetch(node.name))
|
|
400
|
+
when BoundsValue then flat_bounds.fetch(node.slot)
|
|
401
|
+
when UnaryMinus then -evaluate(node.operand, scalars, flat_bounds)
|
|
402
|
+
when BinaryOperation
|
|
403
|
+
left = evaluate(node.left, scalars, flat_bounds)
|
|
404
|
+
right = evaluate(node.right, scalars, flat_bounds)
|
|
405
|
+
case node.operator
|
|
406
|
+
when :+ then left + right
|
|
407
|
+
when :- then left - right
|
|
408
|
+
when :* then left * right
|
|
409
|
+
else
|
|
410
|
+
raise Unsupported,
|
|
411
|
+
"an inner loop's range is built from `+`, `-` and `*` only"
|
|
412
|
+
end
|
|
413
|
+
else
|
|
414
|
+
raise Unsupported,
|
|
415
|
+
"an inner loop's range is an integer expression over literals " \
|
|
416
|
+
"and captured scalars"
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def verify (arrays, bounds, ranges, scalars, reach: true)
|
|
421
|
+
# A contraction carries the summed indices' extents after the free
|
|
422
|
+
# ones, because the loops over them are generated rather than written.
|
|
423
|
+
expected = @rank + @contracted_names.size
|
|
424
|
+
unless bounds.size == expected
|
|
425
|
+
raise Unsupported,
|
|
426
|
+
"the kernel has #{expected} index/indices, got #{bounds.size} extent(s)"
|
|
427
|
+
end
|
|
428
|
+
@arrays.each_with_index do |name, position|
|
|
429
|
+
array = arrays[position]
|
|
430
|
+
unless array.is_a?(CArray)
|
|
431
|
+
raise Unsupported, "`#{name}` is not a CArray"
|
|
432
|
+
end
|
|
433
|
+
unless array.rank == array_rank(name)
|
|
434
|
+
raise Unsupported,
|
|
435
|
+
"`#{name}` has rank #{array.rank}, but is indexed with " \
|
|
436
|
+
"#{array_rank(name)} #{array_rank(name) == 1 ? 'index' : 'indices'}"
|
|
437
|
+
end
|
|
438
|
+
unless array.data_type_name == @storage_types.fetch(name)
|
|
439
|
+
raise Unsupported,
|
|
440
|
+
"the kernel was compiled for `#{name}` as " \
|
|
441
|
+
"#{@storage_types.fetch(name)}, got #{array.data_type_name}"
|
|
442
|
+
end
|
|
443
|
+
verify_bounds(name, array, ranges, scalars) if reach
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Every cell the kernel would touch has to exist. Range and offsets and
|
|
448
|
+
# extents are all known here, so reaching outside an array is caught
|
|
449
|
+
# before anything runs rather than read -- or written -- past its end.
|
|
450
|
+
def verify_bounds (name, array, ranges, scalars)
|
|
451
|
+
@axis_uses.fetch(name).each_with_index do |(walkers, pinned), axis|
|
|
452
|
+
extent = array.dim[axis]
|
|
453
|
+
|
|
454
|
+
pinned_positions(pinned, scalars).each do |position|
|
|
455
|
+
next if position >= 0 && position < extent
|
|
456
|
+
raise Unsupported,
|
|
457
|
+
"`#{name}` is indexed at position #{position} on axis " \
|
|
458
|
+
"#{axis}, " \
|
|
459
|
+
"which has an extent of #{extent}"
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
walkers.each do |index, offsets|
|
|
463
|
+
low, high = ranges.fetch(index)
|
|
464
|
+
next if low >= high
|
|
465
|
+
minimum, maximum = offset_span(offsets, scalars)
|
|
466
|
+
if low + minimum < 0
|
|
467
|
+
raise Unsupported,
|
|
468
|
+
"`#{name}` is indexed at " \
|
|
469
|
+
"#{offset_text(name, index, minimum)}, so " \
|
|
470
|
+
"the range on `#{index}` cannot start at #{low}"
|
|
471
|
+
end
|
|
472
|
+
if high - 1 + maximum > extent - 1
|
|
473
|
+
raise Unsupported,
|
|
474
|
+
"`#{name}` is indexed at " \
|
|
475
|
+
"#{offset_text(name, index, maximum)}, so " \
|
|
476
|
+
"the range on `#{index}` cannot end at #{high} " \
|
|
477
|
+
"for an extent of #{extent}"
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# A pinned subscript's position is an integer expression over literals
|
|
484
|
+
# and captured scalars, so its value is known here even though it is not
|
|
485
|
+
# known when the kernel is compiled.
|
|
486
|
+
def pinned_positions (pinned, scalars)
|
|
487
|
+
pinned.map { |node| node.is_a?(Node) ? evaluate(node, scalars) : node }
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# An offset may be an expression over captured integers, so how far the
|
|
491
|
+
# kernel reaches along an axis is known here rather than when it was
|
|
492
|
+
# compiled.
|
|
493
|
+
def offset_span (offsets, scalars)
|
|
494
|
+
values = offsets.map { |offset|
|
|
495
|
+
offset.is_a?(Node) ? evaluate(offset, scalars) : offset
|
|
496
|
+
}
|
|
497
|
+
[values.min, values.max]
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def offset_text (name, index, offset)
|
|
501
|
+
return "`#{name}[#{index}]`" if offset.zero?
|
|
502
|
+
sign = offset.negative? ? "-" : "+"
|
|
503
|
+
"`#{name}[#{index} #{sign} #{offset.abs}]`"
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
end
|
|
509
|
+
end
|