carray-jit 0.1.0 → 0.1.2
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 +126 -4
- data/README.md +2 -2
- data/docs/02_KernelShapes.md +66 -5
- data/docs/04_Compiling.md +11 -1
- data/docs/06_Cheatsheet.md +19 -7
- data/examples/applications/point_cloud.rb +23 -11
- data/lib/carray/jit/analyzer.rb +145 -26
- data/lib/carray/jit/c_function.rb +37 -14
- data/lib/carray/jit/c_generator.rb +12 -0
- data/lib/carray/jit/compiler.rb +39 -2
- data/lib/carray/jit/expression.rb +34 -2
- data/lib/carray/jit/kernel.rb +2 -1
- data/lib/carray/jit/type_assignment.rb +15 -13
- data/lib/carray/jit/version.rb +1 -1
- data/lib/carray/jit.rb +397 -20
- metadata +2 -2
data/lib/carray/jit.rb
CHANGED
|
@@ -216,13 +216,14 @@ class CArray
|
|
|
216
216
|
JIT.run_stencil(arrays, block, border, type, into)
|
|
217
217
|
end
|
|
218
218
|
|
|
219
|
-
# Returns the contraction the block writes:
|
|
220
|
-
# is summed.
|
|
219
|
+
# Returns the contraction the block writes: a repeated index is summed.
|
|
221
220
|
#
|
|
222
221
|
# CArray.jit_contract { |i, j, k| c[i,j] = a[i,k] * b[k,j] }
|
|
223
222
|
#
|
|
224
223
|
# Every block parameter is an index. The ones that appear on the left are
|
|
225
|
-
# the cells written; the rest -- `k` here -- are summed over.
|
|
224
|
+
# the cells written; the rest -- `k` here -- are summed over. An index that
|
|
225
|
+
# repeats is summed however often it repeats: `q[i,i,i]` is one index read
|
|
226
|
+
# at three positions, and the sum runs along the cube's long diagonal.
|
|
226
227
|
#
|
|
227
228
|
# No extent is given, because every index's extent is fixed by the axes it
|
|
228
229
|
# addresses; an index whose axes disagree is an error, which is the shape
|
|
@@ -236,17 +237,61 @@ class CArray
|
|
|
236
237
|
# Assigning into an array of your own says where to put it, and in what
|
|
237
238
|
# order its axes lie; it does not decide what is summed.
|
|
238
239
|
#
|
|
240
|
+
# That a repeated index is summed is a statement about *dimensions*,
|
|
241
|
+
# which is the world the notation comes from: two dimensions met is an inner
|
|
242
|
+
# product, and there is no other reading. An index that numbers things --
|
|
243
|
+
# a point, a sample, a batch -- is not a dimension, and `x[p,k] * y[p,k]`
|
|
244
|
+
# repeating `p` says "the same point", not "sum over points". Naming the
|
|
245
|
+
# result's axes says which is meant:
|
|
246
|
+
#
|
|
247
|
+
# CArray.jit_contract(:p) { |k| x[p,k] * y[p,k] } # one number per point
|
|
248
|
+
# CArray.jit_contract(:a) { square[a,a] } # the diagonal, not the trace
|
|
249
|
+
# CArray.jit_contract(:b, :i, :j) { |k| u[b,i,k] * v[b,k,j] } # a batch of products
|
|
250
|
+
#
|
|
251
|
+
# The arguments are the result's axes, in that order. What they say is
|
|
252
|
+
# which indices are free; what a repetition means is unchanged. So the rule
|
|
253
|
+
# is the convention's, with a third clause: an index that repeats is summed,
|
|
254
|
+
# one that appears once is free, and a named one is free however often it
|
|
255
|
+
# appears -- which is what puts the diagonal and the per-point quantity
|
|
256
|
+
# inside the notation instead of outside it. A free index needs somewhere
|
|
257
|
+
# to go, so a parameter at a single position is refused once the axes are
|
|
258
|
+
# named: it is free, and the axes are already stated. The list is all of
|
|
259
|
+
# the result's axes rather than some of them -- name one and you have named
|
|
260
|
+
# them all -- which is what keeps the order it states complete. Naming is allowed
|
|
261
|
+
# even where the convention would have reached the same answer, which is how
|
|
262
|
+
# the result's axes are put in another order.
|
|
263
|
+
#
|
|
264
|
+
# The sum is split into partial ones, as `jit_for`'s reduction is: a
|
|
265
|
+
# contraction says which indices are summed and nothing about the order, so
|
|
266
|
+
# there is no order here to override. `CArray::JIT.reassociate = false`
|
|
267
|
+
# asks for the serial one, which is what a Ruby loop would take.
|
|
268
|
+
#
|
|
239
269
|
# The block is read and compiled, never called, so it is not yielded to.
|
|
240
270
|
#
|
|
271
|
+
# @param free_indices [Array<Symbol>] the result's axes, in order; empty to
|
|
272
|
+
# let the convention decide, which is an index appearing once.
|
|
241
273
|
# @return [CArray, CArray::JIT::CompiledKernel] the allocated result when the block
|
|
242
274
|
# assigns into nothing, otherwise the compiled kernel.
|
|
243
275
|
# @raise [CArray::JIT::Unsupported] when the block falls outside the
|
|
244
276
|
# recognized subset, or an index's axes disagree.
|
|
245
|
-
def self.jit_contract (&block)
|
|
277
|
+
def self.jit_contract (*free_indices, &block)
|
|
246
278
|
unless block
|
|
247
279
|
raise JIT::Unsupported, "jit_contract needs a block"
|
|
248
280
|
end
|
|
249
|
-
|
|
281
|
+
unless free_indices.all? { |name| name.is_a?(Symbol) }
|
|
282
|
+
raise JIT::Unsupported,
|
|
283
|
+
"jit_contract's arguments are the result's axes, named as symbols, " \
|
|
284
|
+
"as in `CArray.jit_contract(:p) { |k| x[p,k] * y[p,k] }`"
|
|
285
|
+
end
|
|
286
|
+
repeated = free_indices.tally.select { |_, count| count > 1 }.keys
|
|
287
|
+
unless repeated.empty?
|
|
288
|
+
raise JIT::Unsupported,
|
|
289
|
+
"#{repeated.map { |name| "`#{name}`" }.join(', ')} names more than " \
|
|
290
|
+
"one axis of the result; each axis is one index"
|
|
291
|
+
end
|
|
292
|
+
# Nothing named is the convention; naming none of them is a contraction
|
|
293
|
+
# to a single number, and the two are different statements.
|
|
294
|
+
JIT.run_contraction(block, free_indices.empty? ? nil : free_indices)
|
|
250
295
|
end
|
|
251
296
|
|
|
252
297
|
# @!endgroup
|
|
@@ -328,12 +373,254 @@ class CArray
|
|
|
328
373
|
RESULT = :__contraction_result
|
|
329
374
|
|
|
330
375
|
# @private
|
|
331
|
-
def run_contraction (block)
|
|
376
|
+
def run_contraction (block, free_indices = nil)
|
|
332
377
|
node, source, origin = read_block(block)
|
|
333
|
-
|
|
378
|
+
# An index named at the call site is not a parameter of the block, so
|
|
379
|
+
# the block reaches for it the way it reaches for a captured value.
|
|
380
|
+
# It is neither: it is an index, and it is answered here.
|
|
381
|
+
names = capture_names(source, node) - (free_indices || [])
|
|
382
|
+
arrays, scalars, c_functions = split_captures(names, binding_of(block))
|
|
383
|
+
contract(source, arrays, free_indices, node: node, origin: origin,
|
|
384
|
+
scalars: scalars, c_functions: c_functions)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Reads a block as a contraction and returns the terms it is a product
|
|
388
|
+
# of, or nil when it is not one:
|
|
389
|
+
#
|
|
390
|
+
# CArray::JIT.contraction_of(proc { |i, j, k| a[i,k] * b[k,j] })
|
|
391
|
+
# #=> { :terms => [[a, [:i, :k]], [b, [:k, :j]]],
|
|
392
|
+
# # :free => [:i, :j], :summed => [:k], :scale => 1 }
|
|
393
|
+
#
|
|
394
|
+
# A number multiplied into the product is not a term -- it has no
|
|
395
|
+
# indices and no cell -- so it comes back as `:scale`, which is 1 where
|
|
396
|
+
# there is none. `a[i,k] * b[k,j] * 2.0` is the same contraction scaled,
|
|
397
|
+
# and a caller that rearranges it has to put the scale back: multiplying
|
|
398
|
+
# a sum by a number and multiplying each of its terms are the same
|
|
399
|
+
# arithmetic, but not the same rounding, and they are not the same
|
|
400
|
+
# computation type either where the number is wider than the cells.
|
|
401
|
+
#
|
|
402
|
+
# This is the half of `jit_contract` that decides *what* is being
|
|
403
|
+
# computed, without compiling anything, for a caller that wants to
|
|
404
|
+
# rearrange it -- to contract two terms at a time, say, in an order it
|
|
405
|
+
# chose -- and then reach `contract_terms` with the pieces.
|
|
406
|
+
#
|
|
407
|
+
# Nil means "there is nothing here to rearrange", not that the block is
|
|
408
|
+
# wrong: a summand that is more than a product of element reads
|
|
409
|
+
# (`Math.exp(a[i,k]) * b[k,j]`, a division, a captured scalar, an index
|
|
410
|
+
# with an offset), or one that assigns into an array of its own. Such a
|
|
411
|
+
# block is still a contraction and `jit_contract` still compiles it; it
|
|
412
|
+
# is just not a product to be taken apart. A block that is not a
|
|
413
|
+
# contraction at all raises here, as it would there.
|
|
414
|
+
#
|
|
415
|
+
# @param block [Proc] the block, read and not called.
|
|
416
|
+
# @param free_indices [Array<Symbol>] the result's axes, as
|
|
417
|
+
# `jit_contract` takes them.
|
|
418
|
+
# @return [Hash, nil] `{ terms:, free:, summed: }`, or nil.
|
|
419
|
+
# @raise [CArray::JIT::Unsupported] when the block is not a contraction.
|
|
420
|
+
def contraction_of (block, *free_indices)
|
|
421
|
+
node, source, = read_block(block)
|
|
422
|
+
# As `jit_contract` reads them: naming none is the convention, which
|
|
423
|
+
# is not the same as naming an empty list of axes.
|
|
424
|
+
free_indices = nil if free_indices.empty?
|
|
425
|
+
names = capture_names(source, node) - (free_indices || [])
|
|
334
426
|
arrays, scalars, c_functions = split_captures(names, binding_of(block))
|
|
427
|
+
# A compiled function in the summand is not something the structure
|
|
428
|
+
# carries; a captured number is, as the scale.
|
|
429
|
+
return nil unless c_functions.empty?
|
|
430
|
+
|
|
431
|
+
analyzer = Analyzer.new(source, node: node, array_names: arrays.keys,
|
|
432
|
+
contract: :probe, free_indices: free_indices,
|
|
433
|
+
cell_names: cell_names(arrays))
|
|
434
|
+
statements = analyzer.body.statements
|
|
435
|
+
# Locals before the summand are computation the structure cannot
|
|
436
|
+
# carry either; an ElementWrite last is the assigned form.
|
|
437
|
+
return nil unless statements.size == 1
|
|
438
|
+
factors = product_terms(statements.last, arrays, scalars)
|
|
439
|
+
return nil unless factors
|
|
440
|
+
terms, scale = factors
|
|
441
|
+
return nil if terms.empty?
|
|
442
|
+
|
|
443
|
+
free = analyzer.probe_free_names
|
|
444
|
+
{ :terms => terms, :free => free,
|
|
445
|
+
:summed => terms.flat_map(&:last).uniq - free, :scale => scale }
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# The product's terms and what multiplies them, or nil where the tree is
|
|
449
|
+
# anything but a product of cells and numbers.
|
|
450
|
+
def product_terms (node, arrays, scalars)
|
|
451
|
+
case node
|
|
452
|
+
when BinaryOperation
|
|
453
|
+
return nil unless node.operator == :*
|
|
454
|
+
left = product_terms(node.left, arrays, scalars)
|
|
455
|
+
right = left && product_terms(node.right, arrays, scalars)
|
|
456
|
+
right && [left.first + right.first, left.last * right.last]
|
|
457
|
+
when ElementRead
|
|
458
|
+
subscripts = node.subscripts
|
|
459
|
+
return nil unless subscripts.all? { |index, offset|
|
|
460
|
+
index && offset.is_a?(Integer) && offset.zero?
|
|
461
|
+
}
|
|
462
|
+
[[[arrays.fetch(node.array), subscripts.map(&:first)]], 1]
|
|
463
|
+
when IntegerLiteral, FloatLiteral
|
|
464
|
+
[[], node.value]
|
|
465
|
+
when CaptureRead
|
|
466
|
+
value = scalars[node.name]
|
|
467
|
+
value.is_a?(Numeric) ? [[], value] : nil
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# @private
|
|
472
|
+
TERM_PREFIX = "term"
|
|
473
|
+
# @private
|
|
474
|
+
TARGET = :target
|
|
475
|
+
|
|
476
|
+
# Runs the contraction the terms describe, where a term is an array and
|
|
477
|
+
# the indices it is read at:
|
|
478
|
+
#
|
|
479
|
+
# CArray::JIT.contract_terms([[a, [:i, :k]], [b, [:k, :j]]],
|
|
480
|
+
# free: [:i, :j]) # a matrix product
|
|
481
|
+
#
|
|
482
|
+
# It is `jit_contract` with the block already taken apart -- the same
|
|
483
|
+
# rules, the same errors, the same kernels -- for a caller that has the
|
|
484
|
+
# structure rather than a block to read. A contraction that was decided
|
|
485
|
+
# rather than written has no source, which is what this is for.
|
|
486
|
+
#
|
|
487
|
+
# `free:` is the result's axes in order, and is required: a structure has
|
|
488
|
+
# no parameter list, so there is nowhere else for the order to be said.
|
|
489
|
+
# Every index that is not named is summed, and must appear at more than
|
|
490
|
+
# one position, exactly as in a block whose axes are named.
|
|
491
|
+
#
|
|
492
|
+
# The terms are a product of element reads and nothing else. A summand
|
|
493
|
+
# that is more than that -- `Math.exp(a[i,k]) * b[k,j]`, a division, a
|
|
494
|
+
# captured scalar, an index with an offset -- is written as a block and
|
|
495
|
+
# compiled by `jit_contract`; there is no structure here that says it.
|
|
496
|
+
#
|
|
497
|
+
# @param terms [Array<Array>] `[array, [index, ...]]` pairs.
|
|
498
|
+
# @param free [Array<Symbol>] the result's axes, in order.
|
|
499
|
+
# @param into [CArray, nil] an array of yours to write into, which then
|
|
500
|
+
# decides the axis order and is what comes back.
|
|
501
|
+
# @return [CArray] `into` when it is given, otherwise a new array.
|
|
502
|
+
# @raise [CArray::JIT::Unsupported] when the terms are malformed, or the
|
|
503
|
+
# contraction they describe is one the compiler refuses.
|
|
504
|
+
def contract_terms (terms, free:, into: nil)
|
|
505
|
+
names = check_terms(terms)
|
|
506
|
+
indices = terms.flat_map { |_, subscripts| subscripts }
|
|
507
|
+
free = check_free_indices(free, indices)
|
|
508
|
+
arrays = names.zip(terms.map(&:first)).to_h
|
|
509
|
+
|
|
510
|
+
factors = terms.each_with_index.map { |(_, subscripts), position|
|
|
511
|
+
"#{names[position]}[#{subscripts.join(',')}]"
|
|
512
|
+
}
|
|
513
|
+
body = factors.join(" * ")
|
|
514
|
+
if into
|
|
515
|
+
check_destination(into, free)
|
|
516
|
+
arrays[TARGET] = into
|
|
517
|
+
# With every index summed the result is one number, which lives in a
|
|
518
|
+
# one-cell array at a fixed subscript -- as it is written in a block.
|
|
519
|
+
body = "#{TARGET}[#{free.empty? ? '0' : free.join(',')}] = #{body}"
|
|
520
|
+
end
|
|
521
|
+
summed = indices.uniq - free
|
|
522
|
+
parameters = summed.empty? ? "" : "|#{summed.join(', ')}| "
|
|
523
|
+
|
|
524
|
+
# The source the block would have had. It is what the kernel cache is
|
|
525
|
+
# keyed by, so the same terms under the same shapes reach the same
|
|
526
|
+
# kernel however they were arrived at -- and the string is canonical,
|
|
527
|
+
# which is the same service einsum's subscripts perform.
|
|
528
|
+
result = contract("proc { #{parameters}#{body} }", arrays, free)
|
|
529
|
+
into || result
|
|
530
|
+
end
|
|
335
531
|
|
|
336
|
-
|
|
532
|
+
# The names the synthesized source gives the terms. They are the
|
|
533
|
+
# source's own, so nothing outside chose them -- but an index may
|
|
534
|
+
# collide with one, and a collision would silently read an array as an
|
|
535
|
+
# index.
|
|
536
|
+
def check_terms (terms)
|
|
537
|
+
unless terms.is_a?(Array) && !terms.empty?
|
|
538
|
+
raise Unsupported,
|
|
539
|
+
"contract_terms takes the terms of a product, as in " \
|
|
540
|
+
"`[[a, [:i, :k]], [b, [:k, :j]]]`"
|
|
541
|
+
end
|
|
542
|
+
terms.each do |term|
|
|
543
|
+
unless term.is_a?(Array) && term.size == 2 && term.first.is_a?(CArray)
|
|
544
|
+
raise Unsupported,
|
|
545
|
+
"a term is an array and the indices it is read at, as in " \
|
|
546
|
+
"`[a, [:i, :k]]`; got #{term.inspect}"
|
|
547
|
+
end
|
|
548
|
+
array, subscripts = term
|
|
549
|
+
unless subscripts.is_a?(Array) && subscripts.all? { |name| index_name?(name) }
|
|
550
|
+
raise Unsupported,
|
|
551
|
+
"the indices of a term are symbols naming its axes, as in " \
|
|
552
|
+
"`[a, [:i, :k]]`; got #{subscripts.inspect}"
|
|
553
|
+
end
|
|
554
|
+
unless subscripts.size == array.rank
|
|
555
|
+
raise Unsupported,
|
|
556
|
+
"a term names one index per axis: this array has rank " \
|
|
557
|
+
"#{array.rank} and #{subscripts.size} " \
|
|
558
|
+
"#{subscripts.size == 1 ? 'index' : 'indices'} were given"
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
names = terms.each_index.map { |position| :"#{TERM_PREFIX}#{position}" }
|
|
562
|
+
reserved = (names + [TARGET]) & terms.flat_map { |_, subscripts| subscripts }
|
|
563
|
+
unless reserved.empty?
|
|
564
|
+
raise Unsupported,
|
|
565
|
+
"#{reserved.map { |name| "`#{name}`" }.join(', ')} names a term " \
|
|
566
|
+
"here and cannot also be an index"
|
|
567
|
+
end
|
|
568
|
+
names
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def check_free_indices (free, indices)
|
|
572
|
+
unless free.is_a?(Array) && free.all? { |name| index_name?(name) }
|
|
573
|
+
raise Unsupported,
|
|
574
|
+
"`free:` is the result's axes in order, named as symbols"
|
|
575
|
+
end
|
|
576
|
+
repeated = free.tally.select { |_, count| count > 1 }.keys
|
|
577
|
+
unless repeated.empty?
|
|
578
|
+
raise Unsupported,
|
|
579
|
+
"#{repeated.map { |name| "`#{name}`" }.join(', ')} names more " \
|
|
580
|
+
"than one axis of the result; each axis is one index"
|
|
581
|
+
end
|
|
582
|
+
missing = free - indices
|
|
583
|
+
unless missing.empty?
|
|
584
|
+
raise Unsupported,
|
|
585
|
+
"#{missing.map { |name| "`#{name}`" }.join(', ')} " \
|
|
586
|
+
"#{missing.size == 1 ? 'names no axis' : 'name no axis'} here"
|
|
587
|
+
end
|
|
588
|
+
free
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def check_destination (into, free)
|
|
592
|
+
unless into.is_a?(CArray)
|
|
593
|
+
raise Unsupported, "`into:` is an array to write into"
|
|
594
|
+
end
|
|
595
|
+
expected = free.empty? ? 1 : free.size
|
|
596
|
+
unless into.rank == expected
|
|
597
|
+
raise Unsupported,
|
|
598
|
+
"`into:` has rank #{into.rank}, and the result's axes are " \
|
|
599
|
+
"#{free.empty? ? 'none, which is one cell' : free.map { |name| "`#{name}`" }.join(', ')}"
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# An index is interpolated into the source this writes, so it has to be
|
|
604
|
+
# a name Ruby reads back as a local variable. `:end` and `:do` would
|
|
605
|
+
# not parse at all, and `:nil` and `:self` would come back as something
|
|
606
|
+
# else -- all of them as an error about a source the caller never wrote.
|
|
607
|
+
def index_name? (name)
|
|
608
|
+
return false unless name.is_a?(Symbol)
|
|
609
|
+
text = name.to_s
|
|
610
|
+
return false unless text.match?(/\A[a-z_][A-Za-z0-9_]*\z/)
|
|
611
|
+
parsed = Prism.parse("#{text} = 1")
|
|
612
|
+
parsed.errors.empty? &&
|
|
613
|
+
parsed.value.statements.body.first.is_a?(Prism::LocalVariableWriteNode)
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# What a contraction is once its block has been read: a source, the
|
|
617
|
+
# arrays that source names, and which indices are free. Everything
|
|
618
|
+
# before this is about recovering those from a block; everything after
|
|
619
|
+
# is the same whatever recovered them, which is what lets
|
|
620
|
+
# `contract_terms` reach it with a source it wrote itself.
|
|
621
|
+
def contract (source, arrays, free_indices,
|
|
622
|
+
node: nil, origin: nil, scalars: {}, c_functions: {})
|
|
623
|
+
result = allocate_result(source, node, arrays, scalars, free_indices)
|
|
337
624
|
arrays = arrays.merge(RESULT => result) if result
|
|
338
625
|
|
|
339
626
|
kernel = compile(source,
|
|
@@ -346,8 +633,15 @@ class CArray
|
|
|
346
633
|
masked: arrays.each_value.any? { |array| array.has_mask? },
|
|
347
634
|
contract: true,
|
|
348
635
|
result: RESULT,
|
|
636
|
+
# A contraction sums an index; which order it sums it
|
|
637
|
+
# in is not something the caller wrote, so splitting
|
|
638
|
+
# the sum into partial ones does not change what the
|
|
639
|
+
# contraction means. Same licence `jit_for` takes.
|
|
640
|
+
reassociate: JIT.reassociate,
|
|
641
|
+
free_indices: free_indices,
|
|
349
642
|
cell_names: cell_names(arrays))
|
|
350
643
|
|
|
644
|
+
refuse_aliased_result(kernel, arrays)
|
|
351
645
|
extents = contraction_extents(kernel, arrays)
|
|
352
646
|
if kernel.masked
|
|
353
647
|
kernel.written_arrays.each do |name|
|
|
@@ -359,12 +653,41 @@ class CArray
|
|
|
359
653
|
result || kernel
|
|
360
654
|
end
|
|
361
655
|
|
|
656
|
+
# An array a contraction writes and also reads is a recurrence, which
|
|
657
|
+
# the analyzer refuses -- but it compares the names a block gave them,
|
|
658
|
+
# and two names may be one array. `x = a` is one, and so is a view of
|
|
659
|
+
# something being read: cells reached before the write see the old value
|
|
660
|
+
# and cells reached after see the new one, so the answer depends on the
|
|
661
|
+
# order and is not the contraction that was asked for.
|
|
662
|
+
#
|
|
663
|
+
# This is where the arrays themselves are known, so it is where the
|
|
664
|
+
# question can be asked of them rather than of their names. Views are
|
|
665
|
+
# followed to what they are views of, since that is the memory two names
|
|
666
|
+
# would share.
|
|
667
|
+
def refuse_aliased_result (kernel, arrays)
|
|
668
|
+
kernel.written_arrays.each do |written|
|
|
669
|
+
target = root_of(arrays.fetch(written))
|
|
670
|
+
arrays.each do |name, array|
|
|
671
|
+
next if name == written
|
|
672
|
+
next unless root_of(array).equal?(target)
|
|
673
|
+
raise Unsupported,
|
|
674
|
+
"`#{written}` and `#{name}` are the same array, which this " \
|
|
675
|
+
"both writes and reads; that is a recurrence rather than a " \
|
|
676
|
+
"contraction, and is written with jit_for"
|
|
677
|
+
end
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def root_of (array)
|
|
682
|
+
array.respond_to?(:root_array) ? array.root_array : array
|
|
683
|
+
end
|
|
684
|
+
|
|
362
685
|
# A contraction with nothing to assign into needs its result sized and
|
|
363
686
|
# typed before there is a kernel to ask, so the block is analyzed once
|
|
364
687
|
# without being compiled. Returns nil when the block assigns into an
|
|
365
688
|
# array of its own.
|
|
366
|
-
def allocate_result (source, node, arrays, scalars)
|
|
367
|
-
probe = probe_contraction(source, node, arrays, scalars)
|
|
689
|
+
def allocate_result (source, node, arrays, scalars, free_indices = nil)
|
|
690
|
+
probe = probe_contraction(source, node, arrays, scalars, free_indices)
|
|
368
691
|
return nil unless probe
|
|
369
692
|
free, index_axes, type = probe
|
|
370
693
|
shape = free.map do |index|
|
|
@@ -385,13 +708,17 @@ class CArray
|
|
|
385
708
|
end
|
|
386
709
|
|
|
387
710
|
# @private
|
|
388
|
-
def probe_contraction (source, node, arrays, scalars)
|
|
711
|
+
def probe_contraction (source, node, arrays, scalars, free_indices = nil)
|
|
389
712
|
key = [source, arrays.transform_values(&:data_type_name),
|
|
390
713
|
scalars.transform_values { |value| TypeAssignment.scalar_type(value) },
|
|
714
|
+
# The result's axes are named at the call site rather than in
|
|
715
|
+
# the block, so the same source under another naming is another
|
|
716
|
+
# kernel -- and another probe.
|
|
717
|
+
free_indices,
|
|
391
718
|
cell_names(arrays)]
|
|
392
719
|
cached = probe_cache[key]
|
|
393
720
|
return cached unless cached.nil?
|
|
394
|
-
probe_cache[key] = build_probe(source, node, arrays, scalars)
|
|
721
|
+
probe_cache[key] = build_probe(source, node, arrays, scalars, free_indices)
|
|
395
722
|
end
|
|
396
723
|
|
|
397
724
|
# @private
|
|
@@ -400,10 +727,10 @@ class CArray
|
|
|
400
727
|
end
|
|
401
728
|
|
|
402
729
|
# @private
|
|
403
|
-
def build_probe (source, node, arrays, scalars)
|
|
730
|
+
def build_probe (source, node, arrays, scalars, free_indices = nil)
|
|
404
731
|
storage_types = arrays.transform_values(&:data_type_name)
|
|
405
732
|
analyzer = Analyzer.new(source, node: node, array_names: arrays.keys,
|
|
406
|
-
contract: :probe,
|
|
733
|
+
contract: :probe, free_indices: free_indices,
|
|
407
734
|
cell_names: cell_names(arrays))
|
|
408
735
|
return false if analyzer.body.statements.last.is_a?(ElementWrite)
|
|
409
736
|
|
|
@@ -417,9 +744,13 @@ class CArray
|
|
|
417
744
|
end
|
|
418
745
|
end
|
|
419
746
|
end
|
|
747
|
+
# The type the value was computed in decides the array it is collected
|
|
748
|
+
# into, and that table lives with the computation types rather than
|
|
749
|
+
# here -- a copy kept at the allocation site answers after a type is
|
|
750
|
+
# added, and answers wrongly. This one did: float32 and cmplx64 fell
|
|
751
|
+
# through its default and a float contraction came back int64.
|
|
420
752
|
[analyzer.probe_free_names, axes,
|
|
421
|
-
|
|
422
|
-
.fetch(summand.type, :int64)]
|
|
753
|
+
TypeAssignment.result_storage_type(summand.type)]
|
|
423
754
|
end
|
|
424
755
|
|
|
425
756
|
# Each index's extent comes from the axes it addresses. Where it
|
|
@@ -621,6 +952,26 @@ class CArray
|
|
|
621
952
|
"#{shape.inspect}, so there is no cell where the window is " \
|
|
622
953
|
"inside the array"
|
|
623
954
|
end
|
|
955
|
+
# Writing into an array a window reads is not a pass over the array:
|
|
956
|
+
# a cell written here is a neighbour a later cell reads, so what comes
|
|
957
|
+
# back depends on the order the cells were reached in. The question
|
|
958
|
+
# is about that window alone -- a window that reaches nowhere reads
|
|
959
|
+
# only the cell the loop is on, and may be written in place however
|
|
960
|
+
# far the other windows in the same kernel reach.
|
|
961
|
+
if into
|
|
962
|
+
aliased = given.find { |name, array|
|
|
963
|
+
next false unless root_of(array).equal?(root_of(into))
|
|
964
|
+
kernel.window_reaches[name].any? { |low, high|
|
|
965
|
+
!low.zero? || !high.zero?
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if aliased
|
|
969
|
+
raise Unsupported,
|
|
970
|
+
"`into:` is the array `#{aliased.first}` reaches its window " \
|
|
971
|
+
"into, and a cell written there is one a later cell reads; " \
|
|
972
|
+
"a stencil writes into an array of its own"
|
|
973
|
+
end
|
|
974
|
+
end
|
|
624
975
|
result.mask = 0 if (kernel.masked || border == :mask) && !result.has_mask?
|
|
625
976
|
mark_frame(result, bounds, shape) if border == :mask
|
|
626
977
|
operands = given.merge(MAP_RESULT => result)
|
|
@@ -896,7 +1247,8 @@ class CArray
|
|
|
896
1247
|
def compile (source, node: nil, origin: nil, array_names:, storage_types:,
|
|
897
1248
|
scalar_values:, c_functions: {}, masked: false, rank: nil,
|
|
898
1249
|
steps: nil, contract: false, result: nil, map: false,
|
|
899
|
-
reassociate: false, cell_names: [], windows: [], border: nil
|
|
1250
|
+
reassociate: false, cell_names: [], windows: [], border: nil,
|
|
1251
|
+
free_indices: nil)
|
|
900
1252
|
# A kernel that mentions UNDEF is a masked one whatever its arrays
|
|
901
1253
|
# carry, and deciding that here means no caller has to remember it.
|
|
902
1254
|
masked ||= mentions_undef(source, node)
|
|
@@ -909,6 +1261,10 @@ class CArray
|
|
|
909
1261
|
# symbol, which stands for its body -- see `CFunction#kernel_key`.
|
|
910
1262
|
c_functions.transform_values(&:kernel_key),
|
|
911
1263
|
masked, rank, steps, contract, result, map,
|
|
1264
|
+
# A contraction whose free indices were named is not the kernel
|
|
1265
|
+
# the same source is without them, nor with them in another
|
|
1266
|
+
# order: the naming decides what is summed and what comes out.
|
|
1267
|
+
free_indices,
|
|
912
1268
|
# Which names are read at their one cell rather than walked:
|
|
913
1269
|
# the same source over a CScalar is a different kernel from
|
|
914
1270
|
# the same source over a one-cell CArray.
|
|
@@ -929,7 +1285,7 @@ class CArray
|
|
|
929
1285
|
registry[key] = build(source, node, array_names, storage_types,
|
|
930
1286
|
scalar_values, c_functions, masked, rank, steps,
|
|
931
1287
|
contract, result, origin, map, reassociate,
|
|
932
|
-
cell_names, windows, border)
|
|
1288
|
+
cell_names, windows, border, free_indices)
|
|
933
1289
|
end
|
|
934
1290
|
|
|
935
1291
|
# A kernel that mentions UNDEF is a masked kernel whatever its arrays
|
|
@@ -1004,6 +1360,27 @@ class CArray
|
|
|
1004
1360
|
Compiler.cache_root
|
|
1005
1361
|
end
|
|
1006
1362
|
|
|
1363
|
+
# Puts this application's kernels somewhere of its own, rather than in
|
|
1364
|
+
# the cache shared under the home directory. Say it before the first
|
|
1365
|
+
# kernel is compiled -- at the top of the program, beside the other
|
|
1366
|
+
# requires:
|
|
1367
|
+
#
|
|
1368
|
+
# CArray::JIT.cache_root = File.expand_path("../.jit-cache", __dir__)
|
|
1369
|
+
#
|
|
1370
|
+
# Kernels already loaded keep working and what is already on disk stays
|
|
1371
|
+
# where it is; this says where the next one is looked for and written.
|
|
1372
|
+
# `CARRAY_JIT_CACHE` and `CARRAY_JIT_NO_CACHE` still come first, so
|
|
1373
|
+
# whoever runs the program can put the cache somewhere writable or do
|
|
1374
|
+
# without one. A directory inside a project wants to be ignored by the
|
|
1375
|
+
# version control it sits in.
|
|
1376
|
+
#
|
|
1377
|
+
# @param path [String, nil] the directory, relative to where the
|
|
1378
|
+
# program starts; `nil` restores the default.
|
|
1379
|
+
# @return [void]
|
|
1380
|
+
def cache_root= (path)
|
|
1381
|
+
Compiler.cache_root = path
|
|
1382
|
+
end
|
|
1383
|
+
|
|
1007
1384
|
# @return [Array<String>] the environment directories no longer in use --
|
|
1008
1385
|
# another version, or another architecture.
|
|
1009
1386
|
def stale_cache_environments
|
|
@@ -1036,11 +1413,11 @@ class CArray
|
|
|
1036
1413
|
def build (source, node, array_names, storage_types, scalar_values, c_functions,
|
|
1037
1414
|
masked, rank = nil, steps = nil, contract = false, result = nil,
|
|
1038
1415
|
origin = nil, map = false, reassociate = false,
|
|
1039
|
-
cell_names = [], windows = [], border = nil)
|
|
1416
|
+
cell_names = [], windows = [], border = nil, free_indices = nil)
|
|
1040
1417
|
analyzer = Analyzer.new(source, node: node, array_names: array_names,
|
|
1041
1418
|
c_functions: c_functions,
|
|
1042
1419
|
rank: rank, steps: steps, contract: contract,
|
|
1043
|
-
result: result, map: map,
|
|
1420
|
+
result: result, map: map, free_indices: free_indices,
|
|
1044
1421
|
cell_names: cell_names, windows: windows)
|
|
1045
1422
|
assignment = TypeAssignment.new(analyzer.body, storage_types,
|
|
1046
1423
|
scalar_values, c_functions)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: carray-jit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- himotoyoshi
|
|
@@ -133,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
133
133
|
- !ruby/object:Gem::Version
|
|
134
134
|
version: '0'
|
|
135
135
|
requirements: []
|
|
136
|
-
rubygems_version: 4.0.
|
|
136
|
+
rubygems_version: 4.0.8
|
|
137
137
|
specification_version: 4
|
|
138
138
|
summary: JIT compilation of CArray kernels written in Ruby
|
|
139
139
|
test_files: []
|