Class: CArray::JIT::CompiledKernel

Inherits:
Object
  • Object
show all
Defined in:
lib/carray/jit/kernel.rb

Overview

A compiled kernel, bound to one set of array data types and one set of scalar types.

Every kernel has the same C signature, so the Fiddle::Function shape is fixed and the per-kernel detail travels in the six buffers.

Constant Summary collapse

PLAN_LIMIT =

How many prepared calls one kernel keeps. A stencil asks under its interior's bounds and one pair per axis of frame, so a two-dimensional one needs five before it repeats; a caller alternating between two model states needs two of whatever it was already using. Past this the oldest goes and the call prepares itself again, which is what every call did before.

8

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source:, generator:, analyzer:, storage_types:) ⇒ CompiledKernel

Returns a new instance of CompiledKernel.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/carray/jit/kernel.rb', line 40

def initialize (source:, generator:, analyzer:, storage_types:)
  @source = source
  @c_source = generator.provenance + generator.generate
  @arrays = generator.arrays
  @reals = generator.reals
  @integers = generator.integers
  @complexes = generator.complexes
  @unsigned_integers = generator.unsigned_integers
  # Per array, the axes read at an index only the running kernel knows,
  # and the extents it checks them against.
  @extent_slots = generator.extent_slots
  @dynamic_arrays = @extent_slots.map(&:first).uniq
  @storage_types = storage_types
  @rank = analyzer.rank
  @index_names = analyzer.index_names
  @written_arrays = analyzer.written_arrays
  @array_ranks = analyzer.array_ranks
  @inner_ranges = analyzer.inner_ranges
  # An index the block wrote twice has two identifiers here; the
  # messages speak the name it was written with.
  @index_sources = analyzer.index_sources
  @axis_uses = @arrays.to_h { |array|
    [array, (0...array_rank(array)).map { |axis| analyzer.axis_use(array, axis) }]
  }
  @contracted_names = analyzer.contracted_names
  # Where each index appears, so that a contraction can take its extents
  # from the arrays and say so when they disagree.
  @index_axes = Hash.new { |hash, key| hash[key] = [] }
  @axis_uses.each do |array, uses|
    uses.each_with_index do |(walkers, _), axis|
      walkers.each { |index, _, _| @index_axes[index] << [array, axis] }
    end
  end

  # Which captured scalars decide where the kernel reaches, rather than
  # what it computes.  A pinned subscript, an offset and an inner loop's
  # range are worked out here, in Ruby, before the first cell; every
  # other captured number is packed into a buffer and read by the C.  So
  # these are the ones a prepared call has to be keyed on -- and a
  # coefficient or a clock that changes every step is not one of them,
  # which is what keeps a time-stepping caller on the prepared path.
  @shape_scalar_names = shape_scalar_names
  # Calls prepared earlier, most recently used first.
  @plans = []

  @masked = generator.masked
  @window_reach = analyzer.window_reach
  @window_reaches = analyzer.window_reaches
  # What `raise` in the block said, by the code the cell that raised
  # writes into the error slot.  The message does not travel: C has
  # nothing to carry it in, and it was known when this was compiled.
  @raise_messages = generator.raise_messages
  # The arrays this kernel allocates at its entry, as
  # [name, storage, shape] -- so that a shape it could not use, or an
  # allocation that failed, can be named with the lengths it came to on
  # this call.  The shapes are worked out here as they are there: from
  # the same expressions over the same captured integers.
  @heap_arrays = analyzer.local_array_declarations.filter_map { |entry|
    _scope, name, storage, shape, heap = entry
    [name, storage, shape] if heap
  }
  # Kept so the sweep entry point can be built from it if one is ever
  # asked for.  Compiling it eagerly would pay for a road most kernels
  # never take.
  @generator = generator
  # The names of the C functions the block called, in the order their
  # addresses are packed into the `functions` buffer.  Names, not the
  # functions themselves: a borrowed function is keyed on its signature
  # alone, so this kernel is shared by every function of that shape and
  # the address has to come from the call rather than from the build.
  @c_function_names = generator.address_functions.keys
  # Arrays handed to a C function whole, in the order their addresses
  # are packed into `data`, and what each was promised to be.
  @address_arrays = generator.address_arrays
  @address_parameters = generator.address_parameters
  # One translation unit holds both entry points -- the kernel and the
  # wrapper CArray's sweep calls -- and one build produces both.  They
  # were two builds of the same body until the second was noticed: the
  # wrapper's source already contains the kernel verbatim, so compiling
  # it separately meant compiling everything twice and throwing the
  # first object away whenever the pass swept, which is the usual case
  # for jit_each.  About 230 ms per call site, and an entry in the
  # cache that was never opened.
  @source_text = generator.provenance + generator.generate_slab(SLAB_NAME)
  @handle, @compiled = Compiler.build(generator.generate_slab(SLAB_NAME),
                                      CGenerator::FUNCTION_NAME,
                                      header: generator.provenance)
  @function = Fiddle::Function.new(@handle[CGenerator::FUNCTION_NAME],
                                   [Fiddle::TYPE_VOIDP] * 10,
                                   Fiddle::TYPE_VOID)
end

Instance Attribute Details

#address_arrays ⇒ Object (readonly)

Returns the value of attribute address_arrays.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def address_arrays
  @address_arrays
end

#arrays ⇒ Object (readonly)

Returns the value of attribute arrays.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def arrays
  @arrays
end

#c_source ⇒ String (readonly)

Returns the C this kernel was compiled from.

Returns:

  • (String) —

    the C this kernel was compiled from.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/carray/jit/kernel.rb', line 21

attr_reader :source, :c_source, :arrays, :storage_types, :reals,
:integers, :complexes, :unsigned_integers,
:rank, :index_names, :written_arrays,
# The arrays handed to a C function whole, by address,
# rather than walked a cell at a time.  A caller that lines
# operands up has to leave these alone: their shape is the
# C function's business, not the expression's.
:address_arrays,
:compiled, :masked, :directions, :contracted_names,
:index_axes,
# How far a stencil's windows reach on each axis, as
# [lowest, highest] per axis: what the caller walks the
# interior by.  Empty for a kernel that has no windows.
:window_reach, :window_reaches,
# What a cell that stopped can have been raising about --
# the block's own `raise`s, and those of the bodies pasted
# into this kernel, by the code each reports.
:raise_messages

#compiled ⇒ Boolean (readonly)

Returns whether this call invoked the compiler, rather than reusing a cached object.

Returns:

  • (Boolean) —

    whether this call invoked the compiler, rather than reusing a cached object.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/carray/jit/kernel.rb', line 21

attr_reader :source, :c_source, :arrays, :storage_types, :reals,
:integers, :complexes, :unsigned_integers,
:rank, :index_names, :written_arrays,
# The arrays handed to a C function whole, by address,
# rather than walked a cell at a time.  A caller that lines
# operands up has to leave these alone: their shape is the
# C function's business, not the expression's.
:address_arrays,
:compiled, :masked, :directions, :contracted_names,
:index_axes,
# How far a stencil's windows reach on each axis, as
# [lowest, highest] per axis: what the caller walks the
# interior by.  Empty for a kernel that has no windows.
:window_reach, :window_reaches,
# What a cell that stopped can have been raising about --
# the block's own `raise`s, and those of the bodies pasted
# into this kernel, by the code each reports.
:raise_messages

#complexes ⇒ Object (readonly)

Returns the value of attribute complexes.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def complexes
  @complexes
end

#contracted_names ⇒ Object (readonly)

Returns the value of attribute contracted_names.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def contracted_names
  @contracted_names
end

#directions ⇒ Object (readonly)

Returns the value of attribute directions.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def directions
  @directions
end

#index_axes ⇒ Object (readonly)

Returns the value of attribute index_axes.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def index_axes
  @index_axes
end

#index_names ⇒ Object (readonly)

Returns the value of attribute index_names.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def index_names
  @index_names
end

#integers ⇒ Object (readonly)

Returns the value of attribute integers.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def integers
  @integers
end

#masked ⇒ Object (readonly)

Returns the value of attribute masked.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def masked
  @masked
end

#raise_messages ⇒ Object (readonly)

Returns the value of attribute raise_messages.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def raise_messages
  @raise_messages
end

#rank ⇒ Object (readonly)

Returns the value of attribute rank.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def rank
  @rank
end

#reals ⇒ Object (readonly)

Returns the value of attribute reals.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def reals
  @reals
end

#source ⇒ String (readonly)

Returns the block's Ruby source, as it was read.

Returns:

  • (String) —

    the block's Ruby source, as it was read.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def source
  @source
end

#storage_types ⇒ Object (readonly)

Returns the value of attribute storage_types.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def storage_types
  @storage_types
end

#unsigned_integers ⇒ Object (readonly)

Returns the value of attribute unsigned_integers.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def unsigned_integers
  @unsigned_integers
end

#window_reach ⇒ Object (readonly)

Returns the value of attribute window_reach.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def window_reach
  @window_reach
end

#window_reaches ⇒ Object (readonly)

Returns the value of attribute window_reaches.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def window_reaches
  @window_reaches
end

#written_arrays ⇒ Object (readonly)

Returns the value of attribute written_arrays.



21
22
23
# File 'lib/carray/jit/kernel.rb', line 21

def written_arrays
  @written_arrays
end

Instance Method Details

#call(array_values, scalar_values, bounds, c_function_values = {}, border: false) ⇒ Object

bounds is one [start, limit, step] per axis.

border: runs the frame's entry point instead of the interior's: the same statements, with a window that falls off the array answered by the rule the kernel was compiled for rather than by reading. Which is why the reach is not checked for one -- reaching outside is what it is for, and the C answers for it.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/carray/jit/kernel.rb', line 139

def call (array_values, scalar_values, bounds, c_function_values = {},
          border: false)
  arrays = @arrays.map { |name| array_values.fetch(name) }
  plan = prepared_call(arrays, array_values, bounds, scalar_values, border)

  writable = plan[:writable]
  error = [0].pack("l")
  packed_bounds = plan[:bounds]
  reals = packed_reals(scalar_values)
  # The captured integers change from call to call and are packed here;
  # the extents behind them are the arrays' own and travel in the plan.
  integers = packed_scalar_integers(scalar_values) + plan[:extents]
  functions = @c_function_names.map { |name|
    c_function_values.fetch(name).pointer.to_i
  }.pack("Q*")
  # An array handed over whole is walked contiguously by the C, so it is
  # packed into an entity first and copied back after if the C may have
  # written to it.  This is the same lifecycle CFunction#call keeps, and for
  # the same reason.
  addressed = @address_arrays.map { |name|
    [name, array_values.fetch(name)]
  }
  packed = addressed.map { |name, array| [array, address_buffer(name, array)] }
  data = packed.map { |_, buffer|
    Access.open([buffer], [false], [nil], [nil]) { |bases|
      bases.first[:pointer]
    }
  }.pack("Q*")

  box = plan[:box]
  # Opened for a kernel rather than for a reader: the four buffers come
  # back packed, instead of a hash and two arrays per operand that this
  # would pack back into bytes and throw away.
  Access.open(arrays, writable, box[0], box[1], true) do
             |pointers, strides, mask_pointers, mask_strides|
    entry = border ? border_function : @function
    entry.call(buffer(pointers), buffer(strides), buffer(packed_bounds),
                   buffer(reals), buffer(integers), buffer(functions),
                   buffer(data),
                   buffer(mask_pointers), buffer(mask_strides), error)
  end

  packed.each_with_index do |(array, buffer), index|
    next if array.equal?(buffer)
    next if @address_parameters.fetch(@address_arrays[index]).all?(&:const)
    array[] = buffer
  end

  report(error, scalar_values)
end

#slab_source ⇒ Object

The wrapper CArray's sweep calls, for the same reason #c_source is here: the generated C is the debugging surface. Built on demand, because a kernel that never sweeps never needs it.



252
253
254
# File 'lib/carray/jit/kernel.rb', line 252

def slab_source
  @source_text
end

#sweep(array_values, scalar_values, c_function_values = {}) ⇒ Object

Runs the kernel as CArray's chunked sweep rather than through the addressing here: CArray acquires the operands, broadcasts them, ORs and propagates the masks, and calls back with a chunk at a time.

array_values are the operands in the order the kernel addresses them, which is the order it packs pointers in -- so the same three arguments the kernel's own loop takes are the ones a chunk arrives as.



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/carray/jit/kernel.rb', line 208

def sweep (array_values, scalar_values, c_function_values = {})
  arrays = @arrays.map { |name| array_values.fetch(name) }
  fsync = @arrays.map { |name|
    @written_arrays.include?(name) ? "1" : "0"
  }.join

  error = [0].pack("l")
  reals = packed_reals(scalar_values)
  integers = packed_integers(scalar_values, array_values)
  functions = @c_function_names.map { |name|
    c_function_values.fetch(name).pointer.to_i
  }.pack("Q*")
  addressed = @address_arrays.map { |name|
    [name, array_values.fetch(name)]
  }
  packed = addressed.map { |name, array| [array, address_buffer(name, array)] }
  data = packed.map { |_, item|
    Access.open([item], [false], [nil], [nil]) { |bases|
      bases.first[:pointer]
    }
  }.pack("Q*")
  # The kernel declares these only when it reads a mask, and a body that
  # reads one does not come this way; they are here because the
  # signature has the slots.
  masks = ("\0" * 8)

  held = [buffer(reals), buffer(integers), buffer(functions),
          buffer(data), masks, masks, error]
  context = held.map { |item| Fiddle::Pointer[item].to_i }.pack("Q*")

  Sweep.call(slab_pointer, fsync, arrays, Fiddle::Pointer[context])

  packed.each_with_index do |(array, item), index|
    next if array.equal?(item)
    next if @address_parameters.fetch(@address_arrays[index]).all?(&:const)
    array[] = item
  end

  report(error, scalar_values)
end

#sweepable? ⇒ Boolean

True when this kernel could be run as a sweep at all: the body must not ask about a mask, since a chunk carries no per-cell mask the generated code can test.

Returns:

  • (Boolean)


259
260
261
# File 'lib/carray/jit/kernel.rb', line 259

def sweepable?
  !@masked
end