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/analyzer.rb
CHANGED
|
@@ -179,6 +179,9 @@ class CArray
|
|
|
179
179
|
# radius, kept per side because a window need not be symmetric. It is
|
|
180
180
|
# what the caller walks the interior by.
|
|
181
181
|
attr_reader :window_reach
|
|
182
|
+
# The same, per window: what one array is reached into, rather than the
|
|
183
|
+
# widest reach over all of them.
|
|
184
|
+
attr_reader :window_reaches
|
|
182
185
|
# The names the block gave its windows, which are the arrays a border
|
|
183
186
|
# rule applies to: the ones the block reaches away from the cell in.
|
|
184
187
|
attr_reader :windows
|
|
@@ -233,10 +236,18 @@ class CArray
|
|
|
233
236
|
# returned, and it reaches no array. It is the smallest of the three --
|
|
234
237
|
# with no cell to address there is no extent, no direction and no mask,
|
|
235
238
|
# so most of what follows never runs.
|
|
239
|
+
# `free_indices` are the result's axes, named at the call site rather
|
|
240
|
+
# than taken from the block's parameters. Naming them puts the
|
|
241
|
+
# contraction in its explicit form: these are free however often they
|
|
242
|
+
# appear, and a parameter is summed by repeating as it is under the
|
|
243
|
+
# convention. Nil is "nothing was named" and an empty list is "named,
|
|
244
|
+
# and none of them are free" -- which is a contraction to a single
|
|
245
|
+
# number, and is not the same statement.
|
|
236
246
|
def initialize (source, node: nil, array_names: [], c_functions: {}, rank: nil,
|
|
237
247
|
steps: nil, contract: false, result: nil, function: false,
|
|
238
248
|
pointers: {}, map: false, cell_names: [],
|
|
239
|
-
recursion: nil, windows: [], returns: true
|
|
249
|
+
recursion: nil, windows: [], returns: true,
|
|
250
|
+
free_indices: nil)
|
|
240
251
|
@source = source
|
|
241
252
|
@node = node
|
|
242
253
|
@array_names = array_names
|
|
@@ -277,6 +288,12 @@ class CArray
|
|
|
277
288
|
@whole_array = false
|
|
278
289
|
# How far the windows reach on each axis, filled in as they are read.
|
|
279
290
|
@window_reach = Array.new(rank.to_i) { [0, 0] }
|
|
291
|
+
# And how far each window reaches on its own. The kernel walks the
|
|
292
|
+
# interior by the widest of them, but whether one array may be written
|
|
293
|
+
# in place is a question about that array's window alone.
|
|
294
|
+
@window_reaches = Hash.new { |reaches, name|
|
|
295
|
+
reaches[name] = Array.new(rank.to_i) { [0, 0] }
|
|
296
|
+
}
|
|
280
297
|
@uses_undef = false
|
|
281
298
|
@calls_for_effect = false
|
|
282
299
|
@outer_names = []
|
|
@@ -293,6 +310,7 @@ class CArray
|
|
|
293
310
|
@given_rank = rank
|
|
294
311
|
@steps = steps
|
|
295
312
|
@contract = contract
|
|
313
|
+
@free_indices = free_indices
|
|
296
314
|
@result = result
|
|
297
315
|
@contracted_names = []
|
|
298
316
|
@free_names = []
|
|
@@ -544,10 +562,9 @@ class CArray
|
|
|
544
562
|
|
|
545
563
|
# Turns `c[i,j] = a[i,k] * b[k,j]` into the loops it stands for.
|
|
546
564
|
#
|
|
547
|
-
# Which indices are summed is not the assignment's business:
|
|
548
|
-
#
|
|
549
|
-
#
|
|
550
|
-
# once is free. The left-hand side says where the result goes and in
|
|
565
|
+
# Which indices are summed is not the assignment's business: a repeated
|
|
566
|
+
# index is summed, and that repetition is the notation -- it is what
|
|
567
|
+
# stands in for the sigma. An index appearing once is free. The left-hand side says where the result goes and in
|
|
551
568
|
# what order its axes lie; it cannot make an index disappear.
|
|
552
569
|
# The block's value is what every cell of the result gets, so the last
|
|
553
570
|
# expression becomes a write into the result at the cell the loop is on.
|
|
@@ -641,8 +658,9 @@ class CArray
|
|
|
641
658
|
end
|
|
642
659
|
|
|
643
660
|
# Counts where each index sits on a tensor, on the right-hand side only.
|
|
644
|
-
# Once is free
|
|
645
|
-
#
|
|
661
|
+
# Once is free and repeated is summed, however often it repeats: three
|
|
662
|
+
# positions are not a pair to choose between but one index read at three
|
|
663
|
+
# of them, and `a[i,i,i]` is the sum along the cube's long diagonal.
|
|
646
664
|
def classify_indices (statements, write)
|
|
647
665
|
counts = Hash.new(0)
|
|
648
666
|
subscripts_of(statements, write).each { |index, _| counts[index] += 1 }
|
|
@@ -653,16 +671,23 @@ class CArray
|
|
|
653
671
|
"#{missing.map { |name| "`#{name}`" }.join(', ')} " \
|
|
654
672
|
"#{missing.size == 1 ? 'names no axis' : 'name no axis'} here")
|
|
655
673
|
end
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
674
|
+
# Naming the result's axes says which indices are free; it does not
|
|
675
|
+
# say what a repetition means, and a repetition still means a sum. So
|
|
676
|
+
# a named index is free however often it appears -- twice is what a
|
|
677
|
+
# point number does, and `square[a,a]` is the diagonal rather than a
|
|
678
|
+
# trace -- while a parameter is summed by repeating, here as under the
|
|
679
|
+
# convention. One that appears once is free and was not named, which
|
|
680
|
+
# is a sum along an axis and is not a contraction.
|
|
681
|
+
if @free_indices
|
|
682
|
+
parameters = @index_names - @free_indices
|
|
683
|
+
alone = parameters.select { |name| counts[name] == 1 }
|
|
684
|
+
unless alone.empty?
|
|
685
|
+
raise Unsupported.new(describe_a_lone_parameter(alone))
|
|
686
|
+
end
|
|
687
|
+
return [@free_indices, parameters]
|
|
662
688
|
end
|
|
663
|
-
|
|
664
689
|
[@index_names.select { |name| counts[name] == 1 },
|
|
665
|
-
@index_names.select { |name| counts[name]
|
|
690
|
+
@index_names.select { |name| counts[name] > 1 }]
|
|
666
691
|
end
|
|
667
692
|
|
|
668
693
|
# Every subscript on the right-hand side: the summand, and whatever the
|
|
@@ -681,20 +706,44 @@ class CArray
|
|
|
681
706
|
collected.reject { |index, _| index.nil? }
|
|
682
707
|
end
|
|
683
708
|
|
|
709
|
+
# A parameter at one position only. Nothing there stands in for a
|
|
710
|
+
# sigma, so summing it would be the argument list quietly meaning more
|
|
711
|
+
# than it says -- the same reason the convention refuses it.
|
|
712
|
+
def describe_a_lone_parameter (alone)
|
|
713
|
+
listed = alone.map { |name| "`#{name}`" }.join(", ")
|
|
714
|
+
named = (@free_indices + alone).map { |name| ":#{name}" }.join(", ")
|
|
715
|
+
"#{listed} #{alone.size == 1 ? 'appears' : 'appear'} once, so " \
|
|
716
|
+
"#{alone.size == 1 ? 'it is' : 'they are'} free rather than summed. " \
|
|
717
|
+
"A contraction sums the indices that repeat; name " \
|
|
718
|
+
"#{alone.size == 1 ? 'it' : 'them'} as " \
|
|
719
|
+
"#{alone.size == 1 ? 'an axis' : 'axes'} of the result " \
|
|
720
|
+
"(`CArray.jit_contract(#{named})`) to keep " \
|
|
721
|
+
"#{alone.size == 1 ? 'it' : 'them'}, or use sum(axis:) to sum along " \
|
|
722
|
+
"the axis"
|
|
723
|
+
end
|
|
724
|
+
|
|
684
725
|
def describe_index_mismatch (written, free, summed)
|
|
685
726
|
summed_on_left = written & summed
|
|
686
727
|
unless summed_on_left.empty?
|
|
728
|
+
# What to name is the whole left-hand side, in its order: those are
|
|
729
|
+
# the result's axes, and the one that repeats is only the reason
|
|
730
|
+
# the convention could not see it.
|
|
731
|
+
named = written.map { |name| ":#{name}" }.join(", ")
|
|
687
732
|
return "#{summed_on_left.map { |name| "`#{name}`" }.join(', ')} " \
|
|
688
|
-
"#{summed_on_left.size == 1 ? '
|
|
689
|
-
"the right, so #{summed_on_left.size == 1 ? 'it is' : 'they are'} " \
|
|
690
|
-
"summed over and cannot also be free"
|
|
733
|
+
"#{summed_on_left.size == 1 ? 'is repeated' : 'are repeated'} " \
|
|
734
|
+
"on the right, so #{summed_on_left.size == 1 ? 'it is' : 'they are'} " \
|
|
735
|
+
"summed over and cannot also be free. That a repeated index " \
|
|
736
|
+
"is summed is the convention for dimensions; an index " \
|
|
737
|
+
"that numbers things -- a point, a sample -- is not one, and " \
|
|
738
|
+
"naming the result's axes says so: " \
|
|
739
|
+
"`CArray.jit_contract(#{named})`"
|
|
691
740
|
end
|
|
692
741
|
dropped = free - written
|
|
693
742
|
"#{dropped.map { |name| "`#{name}`" }.join(', ')} " \
|
|
694
743
|
"#{dropped.size == 1 ? 'appears' : 'appear'} once, so " \
|
|
695
744
|
"#{dropped.size == 1 ? 'it is' : 'they are'} free and must be on the " \
|
|
696
|
-
"left. A contraction sums the indices that
|
|
697
|
-
"
|
|
745
|
+
"left. A contraction sums the indices that repeat; to sum one that " \
|
|
746
|
+
"does not, write the loop with jit_for, or use sum(axis:)"
|
|
698
747
|
end
|
|
699
748
|
|
|
700
749
|
# The sum starts from zero of whatever the summand is; which zero that
|
|
@@ -714,9 +763,16 @@ class CArray
|
|
|
714
763
|
|
|
715
764
|
private
|
|
716
765
|
|
|
766
|
+
# The accumulator is a local this writes rather than one the block
|
|
767
|
+
# named, so it has to avoid every name that is already something: the
|
|
768
|
+
# block's locals, and the indices -- an index called `contraction` would
|
|
769
|
+
# have shared the identifier with the accumulator and the sum would have
|
|
770
|
+
# come out zero, with nothing said.
|
|
717
771
|
def free_local_name
|
|
772
|
+
taken = @local_names + @index_names + @outer_names + @inner_names +
|
|
773
|
+
@contracted_names
|
|
718
774
|
name = :contraction
|
|
719
|
-
name = :"#{name}_" while
|
|
775
|
+
name = :"#{name}_" while taken.include?(name)
|
|
720
776
|
name
|
|
721
777
|
end
|
|
722
778
|
|
|
@@ -777,6 +833,27 @@ class CArray
|
|
|
777
833
|
return
|
|
778
834
|
end
|
|
779
835
|
|
|
836
|
+
if @contract && @free_indices
|
|
837
|
+
# The explicit form: the result's axes were named at the call site,
|
|
838
|
+
# so what the block names are the indices that are summed -- and a
|
|
839
|
+
# contraction may now take no parameters at all, which is how the
|
|
840
|
+
# diagonal of one array is written.
|
|
841
|
+
summed = requireds.map(&:name)
|
|
842
|
+
both = @free_indices & summed
|
|
843
|
+
unless both.empty?
|
|
844
|
+
raise Unsupported.new(
|
|
845
|
+
"#{both.map { |name| "`#{name}`" }.join(', ')} " \
|
|
846
|
+
"#{both.size == 1 ? 'is' : 'are'} named as " \
|
|
847
|
+
"#{both.size == 1 ? 'an axis' : 'axes'} of the result and again " \
|
|
848
|
+
"as a block parameter; the parameters are the indices that are " \
|
|
849
|
+
"summed")
|
|
850
|
+
end
|
|
851
|
+
@index_names = @free_indices + summed
|
|
852
|
+
refuse_reserved_indices
|
|
853
|
+
@outer_names = @index_names.dup
|
|
854
|
+
return
|
|
855
|
+
end
|
|
856
|
+
|
|
780
857
|
if @windows.any?
|
|
781
858
|
# The parameters were read before this analyzer was built -- the
|
|
782
859
|
# caller had to, to know which array each window is onto -- so what
|
|
@@ -798,9 +875,23 @@ class CArray
|
|
|
798
875
|
else
|
|
799
876
|
@index_names = requireds.map(&:name)
|
|
800
877
|
end
|
|
878
|
+
refuse_reserved_indices
|
|
801
879
|
@outer_names = @index_names.dup
|
|
802
880
|
end
|
|
803
881
|
|
|
882
|
+
# An index becomes a variable in the C this writes, alongside the
|
|
883
|
+
# kernel's own parameters, so a name C has taken already is refused
|
|
884
|
+
# here rather than by the compiler -- which would complain about a
|
|
885
|
+
# source the block's author never saw.
|
|
886
|
+
def refuse_reserved_indices
|
|
887
|
+
taken = @index_names & CGenerator::RESERVED_NAMES
|
|
888
|
+
return if taken.empty?
|
|
889
|
+
raise Unsupported.new(
|
|
890
|
+
"#{taken.map { |name| "`#{name}`" }.join(', ')} " \
|
|
891
|
+
"#{taken.size == 1 ? 'is a name' : 'are names'} the kernel's own C " \
|
|
892
|
+
"uses, so #{taken.size == 1 ? 'it cannot be an index' : 'they cannot be indices'}")
|
|
893
|
+
end
|
|
894
|
+
|
|
804
895
|
# `printf` writes to the terminal from inside the loop, which is what
|
|
805
896
|
# it is for: the kernel is otherwise silent until it finishes. The
|
|
806
897
|
# format has to be a literal, since C reads it at compile time.
|
|
@@ -873,6 +964,19 @@ class CArray
|
|
|
873
964
|
if @whole_array && @array_names.include?(node.name)
|
|
874
965
|
return whole_array_write(node.name, expression, node.location)
|
|
875
966
|
end
|
|
967
|
+
# An index is the loop's, not the block's. Ruby reads `i = 2` as
|
|
968
|
+
# rebinding the parameter and leaves the loop alone; the C would
|
|
969
|
+
# assign to the counter, so the loop would walk somewhere else --
|
|
970
|
+
# to cells outside the array, given a value outside its extent.
|
|
971
|
+
# The two do not mean the same thing, so this is not compiled.
|
|
972
|
+
if index_in_scope?(node.name)
|
|
973
|
+
raise Unsupported.new(
|
|
974
|
+
"`#{node.name}` is a loop index, and assigning to it here would " \
|
|
975
|
+
"move the loop rather than the value: in Ruby the same line " \
|
|
976
|
+
"rebinds the parameter and the loop runs on. Use a local of " \
|
|
977
|
+
"another name",
|
|
978
|
+
node.location)
|
|
979
|
+
end
|
|
876
980
|
@local_names << node.name unless @local_names.include?(node.name)
|
|
877
981
|
Assignment.new(node.name, expression, node.location)
|
|
878
982
|
when Prism::CallNode
|
|
@@ -1642,6 +1746,9 @@ class CArray
|
|
|
1642
1746
|
end
|
|
1643
1747
|
@window_reach[axis] = [[@window_reach[axis][0], offset].min,
|
|
1644
1748
|
[@window_reach[axis][1], offset].max]
|
|
1749
|
+
own = @window_reaches[array][axis]
|
|
1750
|
+
@window_reaches[array][axis] = [[own[0], offset].min,
|
|
1751
|
+
[own[1], offset].max]
|
|
1645
1752
|
[@outer_names[axis], offset]
|
|
1646
1753
|
}
|
|
1647
1754
|
end
|
|
@@ -1661,12 +1768,10 @@ class CArray
|
|
|
1661
1768
|
# An index expression is `j`, `j + c` or `j - c`, where `j` is any index
|
|
1662
1769
|
# in scope, so that the offset is a compile-time constant.
|
|
1663
1770
|
def read_subscript (node, location)
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
end
|
|
1771
|
+
name = index_name(node)
|
|
1772
|
+
return [name, 0] if name && index_in_scope?(name)
|
|
1667
1773
|
if node.is_a?(Prism::CallNode) && [:+, :-].include?(node.name) &&
|
|
1668
|
-
node.receiver
|
|
1669
|
-
index_in_scope?(node.receiver.name)
|
|
1774
|
+
(receiver = index_name(node.receiver)) && index_in_scope?(receiver)
|
|
1670
1775
|
return walked_subscript(node)
|
|
1671
1776
|
end
|
|
1672
1777
|
# Anything else pins the axis at a position the loop does not walk:
|
|
@@ -1741,6 +1846,20 @@ class CArray
|
|
|
1741
1846
|
[receiver.name, node.name == :+ ? offset : UnaryMinus.new(offset)]
|
|
1742
1847
|
end
|
|
1743
1848
|
|
|
1849
|
+
# An index the block names as a parameter reads as a local variable.
|
|
1850
|
+
# One named at the call site is a parameter of nothing, so Ruby reads it
|
|
1851
|
+
# as a method call -- or as a local variable, where the scope the block
|
|
1852
|
+
# was written in happens to have one by that name. Both spell the same
|
|
1853
|
+
# index, and which it is says nothing about what it means.
|
|
1854
|
+
def index_name (node)
|
|
1855
|
+
case node
|
|
1856
|
+
when Prism::LocalVariableReadNode
|
|
1857
|
+
node.name
|
|
1858
|
+
when Prism::CallNode
|
|
1859
|
+
node.name if node.receiver.nil? && node.arguments.nil? && node.block.nil?
|
|
1860
|
+
end
|
|
1861
|
+
end
|
|
1862
|
+
|
|
1744
1863
|
def index_in_scope? (name)
|
|
1745
1864
|
@outer_names.include?(name) || @inner_names.include?(name)
|
|
1746
1865
|
end
|
|
@@ -368,25 +368,32 @@ class CArray
|
|
|
368
368
|
Fiddle::TYPE_USHORT => :uint16,
|
|
369
369
|
Fiddle::TYPE_INT => :int32,
|
|
370
370
|
Fiddle::TYPE_UINT => :uint32,
|
|
371
|
-
Fiddle::TYPE_LONG
|
|
372
|
-
Fiddle::TYPE_LONG_LONG
|
|
371
|
+
Fiddle::TYPE_LONG => :int64,
|
|
372
|
+
Fiddle::TYPE_LONG_LONG => :int64,
|
|
373
|
+
# `unsigned long` and `size_t` arrive as the same code, and
|
|
374
|
+
# `uint64_t` as the other one.
|
|
375
|
+
Fiddle::TYPE_ULONG => :uint64,
|
|
376
|
+
Fiddle::TYPE_ULONG_LONG => :uint64,
|
|
373
377
|
}.freeze
|
|
374
378
|
|
|
375
379
|
# Codes Fiddle may return, mapped to what a kernel computes them in.
|
|
376
380
|
# Absent means the type may be written down but holds no value a body
|
|
377
|
-
# can compute with
|
|
378
|
-
# uint64 array: no computation type holds it without losing a bit.
|
|
381
|
+
# can compute with -- `void`, and a pointer to it.
|
|
379
382
|
COMPUTATION = {
|
|
380
|
-
Fiddle::TYPE_DOUBLE
|
|
381
|
-
Fiddle::TYPE_FLOAT
|
|
382
|
-
Fiddle::TYPE_CHAR
|
|
383
|
-
Fiddle::TYPE_UCHAR
|
|
384
|
-
Fiddle::TYPE_SHORT
|
|
385
|
-
Fiddle::TYPE_USHORT
|
|
386
|
-
Fiddle::TYPE_INT
|
|
387
|
-
Fiddle::TYPE_UINT
|
|
388
|
-
Fiddle::TYPE_LONG
|
|
389
|
-
Fiddle::TYPE_LONG_LONG
|
|
383
|
+
Fiddle::TYPE_DOUBLE => :double,
|
|
384
|
+
Fiddle::TYPE_FLOAT => :double,
|
|
385
|
+
Fiddle::TYPE_CHAR => :int64,
|
|
386
|
+
Fiddle::TYPE_UCHAR => :int64,
|
|
387
|
+
Fiddle::TYPE_SHORT => :int64,
|
|
388
|
+
Fiddle::TYPE_USHORT => :int64,
|
|
389
|
+
Fiddle::TYPE_INT => :int64,
|
|
390
|
+
Fiddle::TYPE_UINT => :int64,
|
|
391
|
+
Fiddle::TYPE_LONG => :int64,
|
|
392
|
+
Fiddle::TYPE_LONG_LONG => :int64,
|
|
393
|
+
# uint64 is a computation type of its own, precisely because int64
|
|
394
|
+
# cannot carry what it holds; CArray has the array to match.
|
|
395
|
+
Fiddle::TYPE_ULONG => :uint64,
|
|
396
|
+
Fiddle::TYPE_ULONG_LONG => :uint64,
|
|
390
397
|
}.freeze
|
|
391
398
|
|
|
392
399
|
module_function
|
|
@@ -681,6 +688,22 @@ class CArray
|
|
|
681
688
|
"the signature rather than a value; the body cannot read it"
|
|
682
689
|
end)
|
|
683
690
|
end
|
|
691
|
+
# A body is handed its values through the kernel's three scalar buses,
|
|
692
|
+
# which carry doubles, int64s and complexes. A uint64 argument is the
|
|
693
|
+
# one numeric type none of them can carry whole -- that is what having
|
|
694
|
+
# its own computation type means -- so it is refused here, where the
|
|
695
|
+
# declaration is, rather than deeper down as a kernel that cannot be
|
|
696
|
+
# built. A `uint64_t *` reaches an array of them, and a uint64_t
|
|
697
|
+
# value comes back out of a body unharmed.
|
|
698
|
+
names.zip(parameters).each do |parameter, type|
|
|
699
|
+
next if type.pointer || type.computation != :uint64
|
|
700
|
+
raise Unsupported,
|
|
701
|
+
"`#{parameter}` is declared `#{type.text}`, and a value is " \
|
|
702
|
+
"handed to a body as a double, an int64 or a complex -- a " \
|
|
703
|
+
"uint64 fits none of them without losing a bit. Take " \
|
|
704
|
+
"`#{type.text} *` and index it, or take an int64 where the " \
|
|
705
|
+
"values are small enough to be one"
|
|
706
|
+
end
|
|
684
707
|
types = names.zip(parameters).reject { |_, type| type.pointer }
|
|
685
708
|
.to_h { |parameter, type| [parameter, type.computation] }
|
|
686
709
|
|
|
@@ -68,6 +68,18 @@ class CArray
|
|
|
68
68
|
"double *reals, int64_t *integers, void **functions, void **data, " \
|
|
69
69
|
"char **mask_pointers, int64_t *mask_strides, int32_t *error)".freeze
|
|
70
70
|
|
|
71
|
+
# Names an index may not take, because the C it becomes is written with
|
|
72
|
+
# them: the kernel's own parameters above, and the words C keeps for
|
|
73
|
+
# itself. A name from either list compiles to something else or to
|
|
74
|
+
# nothing, and the compiler's complaint is about a source the block's
|
|
75
|
+
# author did not write.
|
|
76
|
+
RESERVED_NAMES =
|
|
77
|
+
(SIGNATURE.scan(/\*?(\w+)[,)]/).flatten +
|
|
78
|
+
%w[auto break case char const continue default do double else enum
|
|
79
|
+
extern float for goto if inline int long register restrict return
|
|
80
|
+
short signed sizeof static struct switch typedef union unsigned
|
|
81
|
+
void volatile while]).map(&:to_sym).freeze
|
|
82
|
+
|
|
71
83
|
ARGUMENTS =
|
|
72
84
|
"pointers, strides, bounds, reals, integers, functions, data, " \
|
|
73
85
|
"mask_pointers, mask_strides, error".freeze
|
data/lib/carray/jit/compiler.rb
CHANGED
|
@@ -40,7 +40,26 @@ class CArray
|
|
|
40
40
|
# being lower was not a decision -- the flag list is the one the first
|
|
41
41
|
# milestone was scaffolded with, and only the contraction flag beside
|
|
42
42
|
# it was ever argued for.
|
|
43
|
-
|
|
43
|
+
|
|
44
|
+
# Every generated object exports the same fixed names -- carray_jit_kernel,
|
|
45
|
+
# carray_jit_slab, the border and error helpers -- because each one is a
|
|
46
|
+
# module of its own. Fiddle.dlopen passes RTLD_GLOBAL, so on ELF those
|
|
47
|
+
# names all land in one flat namespace and an intra-module call resolves
|
|
48
|
+
# to the first definition loaded: a later kernel's carray_jit_slab calls
|
|
49
|
+
# an earlier kernel's carray_jit_kernel, which takes a different number
|
|
50
|
+
# of operands, reads past the pointers it was given, and the process
|
|
51
|
+
# segfaults. -Wl,-Bsymbolic binds each object's own definitions before
|
|
52
|
+
# the global scope, which is what one object per kernel assumed all
|
|
53
|
+
# along. Nothing else needs saying so: Mach-O binds within each dylib's
|
|
54
|
+
# own two-level namespace and PE within each DLL, so the flag is named
|
|
55
|
+
# only where it means something -- and Apple's linker rejects it
|
|
56
|
+
# outright, which would trade a Linux crash for a macOS build that never
|
|
57
|
+
# compiles at all.
|
|
58
|
+
SYMBOLIC =
|
|
59
|
+
(RbConfig::CONFIG["host_os"] =~ /darwin|mswin|mingw|cygwin/ ?
|
|
60
|
+
[] : ["-Wl,-Bsymbolic"]).freeze
|
|
61
|
+
|
|
62
|
+
FLAGS = ["-O3", "-fPIC", "-shared", "-ffp-contract=off", *SYMBOLIC].freeze
|
|
44
63
|
|
|
45
64
|
# Kernels retained on disk. Each costs about 17 KB, so the default is
|
|
46
65
|
# roughly 9 MB -- far more than any real program compiles, but a bound
|
|
@@ -63,11 +82,29 @@ class CArray
|
|
|
63
82
|
# directory for this one.
|
|
64
83
|
def cache_root
|
|
65
84
|
return ephemeral_directory if ephemeral?
|
|
66
|
-
ENV["CARRAY_JIT_CACHE"] ||
|
|
85
|
+
ENV["CARRAY_JIT_CACHE"] || @cache_root ||
|
|
67
86
|
File.join(ENV["XDG_CACHE_HOME"] || File.join(Dir.home, ".cache"),
|
|
68
87
|
"carray-jit")
|
|
69
88
|
end
|
|
70
89
|
|
|
90
|
+
# An application saying where its own kernels live, rather than
|
|
91
|
+
# sharing the one cache under the home directory. Set it before the
|
|
92
|
+
# first kernel is compiled; kernels already loaded keep working, and
|
|
93
|
+
# what is already on disk stays where it is. `nil` restores the
|
|
94
|
+
# default.
|
|
95
|
+
#
|
|
96
|
+
# The environment still comes first: `CARRAY_JIT_CACHE` redirects an
|
|
97
|
+
# application that names a directory here, and `CARRAY_JIT_NO_CACHE`
|
|
98
|
+
# takes the cache away, so whoever runs a program can still put it
|
|
99
|
+
# somewhere writable, or do without.
|
|
100
|
+
#
|
|
101
|
+
# The path is expanded when it is given, not when it is read: a
|
|
102
|
+
# relative one would otherwise name a different directory after the
|
|
103
|
+
# program changes its working directory.
|
|
104
|
+
def cache_root= (path)
|
|
105
|
+
@cache_root = path && File.expand_path(path)
|
|
106
|
+
end
|
|
107
|
+
|
|
71
108
|
def cache_directory
|
|
72
109
|
File.join(cache_root, environment_tag)
|
|
73
110
|
end
|
|
@@ -16,13 +16,24 @@ class CArray
|
|
|
16
16
|
# goes back to CArray, which walks it and arrives at the same answer.
|
|
17
17
|
class Expression
|
|
18
18
|
|
|
19
|
+
# A kernel that can divide by zero calls ca_zerodiv, which the CArray
|
|
20
|
+
# extension defines and this object does not. ELF is content to leave
|
|
21
|
+
# the name undefined and resolve it when the object is loaded, which is
|
|
22
|
+
# what the declaration in #source_for assumes; Mach-O refuses to link at
|
|
23
|
+
# all, so on macOS every expression holding an integer `/` or `%` failed
|
|
24
|
+
# to compile and was silently handed back to CArray to walk. The flag
|
|
25
|
+
# says to look the name up at load time, which is what Ruby builds its
|
|
26
|
+
# own extensions with and the only thing this object leaves undefined.
|
|
27
|
+
DYNAMIC_LOOKUP =
|
|
28
|
+
(RbConfig::CONFIG["host_os"] =~ /darwin/ ? ["-Wl,-undefined,dynamic_lookup"] : []).freeze
|
|
29
|
+
|
|
19
30
|
# Built the way CArray's own kernels were, since that is what the answer
|
|
20
31
|
# is being compared against. The Prism front end wants the opposite of
|
|
21
32
|
# this on one point -- it answers to a Ruby loop, which does not fuse a
|
|
22
33
|
# multiply and an add into one rounding, so it compiles with
|
|
23
34
|
# -ffp-contract=off. Here the reference is the eager kernel, which was
|
|
24
35
|
# built with whatever CArray settled on.
|
|
25
|
-
FLAGS = ["-fPIC", "-shared", *CArray::BUILD_FLAGS.split].freeze
|
|
36
|
+
FLAGS = ["-fPIC", "-shared", *CArray::BUILD_FLAGS.split, *DYNAMIC_LOOKUP].freeze
|
|
26
37
|
|
|
27
38
|
C_TYPES = {
|
|
28
39
|
float64: "double", float32: "float",
|
|
@@ -37,7 +48,9 @@ class CArray
|
|
|
37
48
|
@kernels = {}
|
|
38
49
|
end
|
|
39
50
|
|
|
40
|
-
# Fills `out` and returns true, or
|
|
51
|
+
# Fills `out` and returns true, or returns false and leaves it to
|
|
52
|
+
# CArray. A decline may have written part of `out` first, which is
|
|
53
|
+
# what walking it over again then settles.
|
|
41
54
|
def call (plan, out)
|
|
42
55
|
return false unless C_TYPES.key?(plan.data_type)
|
|
43
56
|
aliased = plan.leaves.any? { |array| array.equal?(out) }
|
|
@@ -51,6 +64,25 @@ class CArray
|
|
|
51
64
|
kernel.call(out.elements, *pointers(plan, bases))
|
|
52
65
|
end
|
|
53
66
|
true
|
|
67
|
+
rescue ZeroDivisionError
|
|
68
|
+
# A zero divisor is an answer, not a fault: it is what ca_zerodiv --
|
|
69
|
+
# the only thing a kernel here calls out to -- reports, and the same
|
|
70
|
+
# expression walked reaches the same place and raises the same error.
|
|
71
|
+
#
|
|
72
|
+
# CArray cannot tell the two apart, though. It catches whatever an
|
|
73
|
+
# evaluator raises, retires it for the rest of the process and says so
|
|
74
|
+
# on stderr, which is right for an evaluator that is broken and wrong
|
|
75
|
+
# for one that has just met a zero. So this declines instead, and
|
|
76
|
+
# CArray walks the expression and raises it there. Anything else
|
|
77
|
+
# still reaches CArray and still retires this, which is what that net
|
|
78
|
+
# is for.
|
|
79
|
+
#
|
|
80
|
+
# What it costs: a kernel that raised where the walk does not -- over
|
|
81
|
+
# a masked zero, say -- now reads as slow rather than as wrong, since
|
|
82
|
+
# the walk answers and nobody sees the difference. The answer is
|
|
83
|
+
# right either way, and the masked-divisor test asks the evaluator
|
|
84
|
+
# directly for that reason.
|
|
85
|
+
false
|
|
54
86
|
end
|
|
55
87
|
|
|
56
88
|
private
|
data/lib/carray/jit/kernel.rb
CHANGED
|
@@ -25,7 +25,7 @@ class CArray
|
|
|
25
25
|
# How far a stencil's windows reach on each axis, as
|
|
26
26
|
# [lowest, highest] per axis: what the caller walks the
|
|
27
27
|
# interior by. Empty for a kernel that has no windows.
|
|
28
|
-
:window_reach,
|
|
28
|
+
:window_reach, :window_reaches,
|
|
29
29
|
# What a cell that stopped can have been raising about --
|
|
30
30
|
# the block's own `raise`s, and those of the bodies pasted
|
|
31
31
|
# into this kernel, by the code each reports.
|
|
@@ -63,6 +63,7 @@ class CArray
|
|
|
63
63
|
|
|
64
64
|
@masked = generator.masked
|
|
65
65
|
@window_reach = analyzer.window_reach
|
|
66
|
+
@window_reaches = analyzer.window_reaches
|
|
66
67
|
# What `raise` in the block said, by the code the cell that raised
|
|
67
68
|
# writes into the error slot. The message does not travel: C has
|
|
68
69
|
# nothing to carry it in, and it was known when this was compiled.
|
|
@@ -8,15 +8,14 @@ class CArray
|
|
|
8
8
|
# propagates bottom-up.
|
|
9
9
|
#
|
|
10
10
|
# The distinction that matters is between *storage* type and *computation*
|
|
11
|
-
# type
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# evaluator, so reads from any float array are typed :double here and the
|
|
15
|
-
# generator casts once, at the store.
|
|
11
|
+
# type: what an array holds, and what a kernel works on a cell of it in.
|
|
12
|
+
# They are not the same -- int8 is computed in int64 -- and the table
|
|
13
|
+
# below is what says which.
|
|
16
14
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
15
|
+
# A narrow float is computed narrow: float32 in `float` and cmplx64 in
|
|
16
|
+
# `float _Complex`, read narrow and stored narrow. That is what CArray's
|
|
17
|
+
# own kernels do, so the two agree; the Ruby evaluator, which would widen
|
|
18
|
+
# to a double, is what they both differ from. See docs/03 and docs/05.
|
|
20
19
|
class TypeAssignment
|
|
21
20
|
|
|
22
21
|
STORAGE_COMPUTATION_TYPES = {
|
|
@@ -60,11 +59,14 @@ class CArray
|
|
|
60
59
|
# the reason KINDS gives: a table of names kept somewhere else keeps
|
|
61
60
|
# answering after a computation type is added, and answers wrongly.
|
|
62
61
|
RESULT_STORAGE_TYPES = {
|
|
63
|
-
:int64
|
|
64
|
-
:uint64
|
|
65
|
-
:float
|
|
66
|
-
:double
|
|
67
|
-
:
|
|
62
|
+
:int64 => :int64,
|
|
63
|
+
:uint64 => :uint64,
|
|
64
|
+
:float => :float32,
|
|
65
|
+
:double => :float64,
|
|
66
|
+
# cmplx64 is to cmplx128 what float32 is to float64: the value was
|
|
67
|
+
# computed narrow, and cmplx64 holds it without widening or losing it.
|
|
68
|
+
:float_complex => :cmplx64,
|
|
69
|
+
:complex => :cmplx128,
|
|
68
70
|
}.freeze
|
|
69
71
|
|
|
70
72
|
# Raises rather than falling back, because there is no type to fall back
|
data/lib/carray/jit/version.rb
CHANGED