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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 255cd246bcdd06c49b0afd2aeaaa4c3086eb8b9c84f4ad04319129f7415c182f
|
|
4
|
+
data.tar.gz: 80fedb1eb6cfcfece4009d69d26251a58a2caa9d5975d1895bb34b9a7fa18ac7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fa33123ca9012275607181b6808e2497da5e8d1908764bff23200299e9c972dd61e9b9e4d3ab58345b2fbcd5badba39ddd03b214ad9ccadde6413b37a32f3215
|
|
7
|
+
data.tar.gz: 45050b058cba426afc373e50f47797e4720968c1e4a26862322364d44a81a4610a991622b0e35d3d20519fb583e654558728a85393064978c9adfc23db5d9f06
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Releases are recorded here from 0.1.0, which is the first.
|
|
4
|
+
|
|
5
|
+
<!-- Newest first, at both levels: a new release section goes above the
|
|
6
|
+
ones below it, and a new entry goes directly under its own release
|
|
7
|
+
heading -- not at the end of the section. The kind of change is
|
|
8
|
+
carried by the `- Fix:` / `- Change:` / `- New:` that opens the
|
|
9
|
+
entry; there are no per-kind subheadings.
|
|
10
|
+
|
|
11
|
+
An entry says three things and stops: what changed, what to do about
|
|
12
|
+
it (the migration, the replacement, the condition under which nothing
|
|
13
|
+
changes), and what is excluded. It does not say how the code was
|
|
14
|
+
broken, name the internals that were fixed, break down where the
|
|
15
|
+
speed came from, or argue the design -- those belong in the commit
|
|
16
|
+
message. Two to six lines.
|
|
17
|
+
|
|
18
|
+
The version here is this gem's own and is not CArray's. Which CArray
|
|
19
|
+
a release needs is said in the gemspec, and an entry says so only
|
|
20
|
+
when the answer changes. -->
|
|
21
|
+
|
|
22
|
+
## 0.1.0
|
|
23
|
+
|
|
24
|
+
- New: `CArray.jit_for`, `CArray.jit_each` and `CArray.jit_map` compile their
|
|
25
|
+
block rather than running it. The block is read with Prism, translated to C
|
|
26
|
+
if it falls inside the recognized subset, compiled with the system C
|
|
27
|
+
compiler and called through Fiddle. `jit_for` takes the loop extents and
|
|
28
|
+
names the indices, so a cell may reach the ones around it -- a recurrence, a
|
|
29
|
+
stencil written out; `jit_each` writes into arrays of yours and `jit_map`
|
|
30
|
+
hands the value back, both at the cell with no index named. CArray 3.0.1
|
|
31
|
+
defines these three names and raises there; installing this gem is what
|
|
32
|
+
makes them compile.
|
|
33
|
+
|
|
34
|
+
- New: a block outside the subset raises `CArray::JIT::Unsupported`, naming
|
|
35
|
+
the construct and where it is, rather than falling back to a Ruby loop.
|
|
36
|
+
Nobody calls these methods except to make a per-cell computation fast, so
|
|
37
|
+
quietly doing the slow thing would answer a question that was not asked.
|
|
38
|
+
What the subset holds is in
|
|
39
|
+
[docs/03_SupportedFeatures.md](docs/03_SupportedFeatures.md#the-recognized-subset).
|
|
40
|
+
|
|
41
|
+
- New: every operation in a kernel means what Ruby means by it -- integer
|
|
42
|
+
division floors, `%` is not `fmod`, a Complex divides by Smith's method in
|
|
43
|
+
the order `complex.c` writes it. The exception is the order a reduction
|
|
44
|
+
takes its terms in: an accumulator is split into partial sums, which is
|
|
45
|
+
faster and usually the more accurate answer. `reassociate: false` on
|
|
46
|
+
`jit_for`, `CArray::JIT.reassociate = false`, or `CARRAY_JIT_REASSOCIATE=0`
|
|
47
|
+
for a whole process, asks for the serial order, and then the kernel agrees
|
|
48
|
+
with the Ruby loop bit for bit. `**` on a Complex is the one documented
|
|
49
|
+
exception.
|
|
50
|
+
|
|
51
|
+
- New: `CArray.jit_stencil` runs a block over windows onto its arrays, with
|
|
52
|
+
`border:` saying what happens at the edge -- `:mask` by default, because
|
|
53
|
+
CArray can say "not computed" and a border of zeros cannot be told from
|
|
54
|
+
zeros that were computed. `CArray.jit_contract` runs a contraction over a
|
|
55
|
+
repeated index, summing over the indices that do not appear on the left.
|
|
56
|
+
|
|
57
|
+
- New: `CArray.jit_extern` names a C function someone else compiled by quoting
|
|
58
|
+
its declaration, and `CArray.jit_function` compiles a body of your own. Both
|
|
59
|
+
hand back an object a kernel calls by address rather than per cell through
|
|
60
|
+
Fiddle, and either can be called from Ruby too.
|
|
61
|
+
|
|
62
|
+
- New: `CArray.fuse` is compiled where this gem is installed, without asking
|
|
63
|
+
for it and without changing what it computes. The gem registers an
|
|
64
|
+
expression evaluator with CArray at load; a program that only writes
|
|
65
|
+
`CArray.fuse { ... }` gets the same answer with or without it.
|
|
66
|
+
|
|
67
|
+
- New: compiled kernels are cached on disk, in `~/.cache/carray-jit` unless
|
|
68
|
+
`XDG_CACHE_HOME` or `CARRAY_JIT_CACHE` says otherwise, so a kernel is
|
|
69
|
+
compiled once rather than once per run. Entries are kept apart by this gem's
|
|
70
|
+
version, the CArray version they were built against, and the architecture:
|
|
71
|
+
a kernel is handed CArray's memory, on layouts CArray decides, so one
|
|
72
|
+
compiled against another version is rebuilt rather than reused.
|
|
73
|
+
`CARRAY_JIT_NO_CACHE` keeps the cache in a temporary directory that goes
|
|
74
|
+
away with the process, and `CARRAY_JIT_CC` names a different compiler.
|
|
75
|
+
|
|
76
|
+
- New: the `carray-jit` command reports and looks after that cache --
|
|
77
|
+
`status`, `list`, `show`, `clear`. It loads the compiler and nothing else,
|
|
78
|
+
so a cache can be inspected or cleared when CArray itself will not load.
|
|
79
|
+
|
|
80
|
+
- New: needs CArray 3.0.1 or later in the 3.0 series, and Ruby 3.2 or later.
|
|
81
|
+
The floor is where `ca_call_cslab_N`, the expression evaluator hook,
|
|
82
|
+
`__kernel_body__` and `BUILD_FLAGS` arrive. The ceiling is the next minor
|
|
83
|
+
because a kernel reaches CArray's C by address and pastes its kernel bodies
|
|
84
|
+
into generated C, neither of which the frozen author surface covers.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 himotoyoshi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# carray-jit
|
|
2
|
+
|
|
3
|
+
JIT compilation of CArray kernels written in Ruby.
|
|
4
|
+
|
|
5
|
+
This library was written to make explicit, cell-by-cell work on CArray arrays fast. A subset of Ruby chosen for numerical computation is JIT-compiled and evaluated as a C-level loop over CArray arrays: a cell that reads its neighbours, a recurrence, a loop written out. The aim is to combine that with CArray's already fast vectorized arithmetic and reductions, and so speed up numerical computation with CArray as a whole.
|
|
6
|
+
|
|
7
|
+
The block is read with Prism, translated to C if it falls inside that subset, compiled with the system C compiler and called through Fiddle. The compiled object is cached on disk, so a kernel is compiled once.
|
|
8
|
+
|
|
9
|
+
## Status
|
|
10
|
+
|
|
11
|
+
0.1.0 is the first release, and it still moves: behaviour can change between releases — see [CHANGELOG.md](CHANGELOG.md). A companion gem to CArray, it follows CArray's surface, which is not settled until CArray 3.1.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **A JIT compiler for C-level loops.** A block becomes one C function over CArray's own memory, built by the system C compiler and called through Fiddle.
|
|
16
|
+
- **Ordinary Ruby, and enough of it.** The source is parsed with Prism -- no DSL, no `eval` -- and every operation means what Ruby means by it, apart from the order a reduction takes its terms in. The subset is enough to state a numerical algorithm; what falls outside it is refused by name and line, not run as a Ruby loop.
|
|
17
|
+
- **A method for each shape.** `jit_for` for recurrences and loops written out, `jit_stencil` for windows at any rank, `CArray.jit_contract` for contraction over a repeated index, `jit_each` and `jit_map` for a pass that reaches no neighbour.
|
|
18
|
+
- **View- and mask-aware.** Columns, transposes and slices of slices are written in place without a copy, and masks propagate as CArray propagates them.
|
|
19
|
+
- **Pure C functions, in and out.** `jit_extern` binds one from a library and a kernel calls it by address; `jit_function` compiles one from a block and hands back a C function pointer.
|
|
20
|
+
- **The backend for `CArray.fuse`.** An array expression compiles instead of being walked a node at a time, without being asked and without changing the answer.
|
|
21
|
+
- **Compiled once, across processes.** The shared object is cached on disk and reused by later runs.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
gem install carray-jit
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or add it to your `Gemfile`:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
gem "carray-jit"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires:
|
|
36
|
+
|
|
37
|
+
- Ruby >= 3.2
|
|
38
|
+
- CArray >= 3.0.1, < 3.1
|
|
39
|
+
- A C compiler
|
|
40
|
+
- Prism and Fiddle (both ship with Ruby; Fiddle is a bundled gem)
|
|
41
|
+
|
|
42
|
+
## Example
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
# Legendre polynomials at x = 0.5, by the recurrence that defines them.
|
|
46
|
+
# No array expression states this: P[i] needs P[i-1], which the same
|
|
47
|
+
# loop has just written.
|
|
48
|
+
|
|
49
|
+
x = 0.5
|
|
50
|
+
legendre = CArray.double(24)
|
|
51
|
+
legendre[0] = 1.0 # P_0(x) = 1
|
|
52
|
+
legendre[1] = x # P_1(x) = x
|
|
53
|
+
|
|
54
|
+
CArray.jit_for(2...24) { |i| # i is the loop index: 2, 3, ... 23
|
|
55
|
+
w = x * legendre[i-1] # a block local; its type is inferred
|
|
56
|
+
wy = w - legendre[i-2] # reaching back two cells
|
|
57
|
+
legendre[i] = wy + w - wy/i # the cell this pass writes
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
legendre[0..5].to_a
|
|
61
|
+
# => [1.0, 0.5, -0.125, -0.4375, -0.2890625, 0.08984375]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`CArray.jit_for`, `CArray.jit_each` and `CArray.jit_map` are CArray's own names: without this gem they raise and point at `CArray.fuse`, which computes an array expression without a compiler. This gem is the compiler. An expression over whole arrays wants `CArray.fuse` and not these, and gets compiled anyway where this gem is installed.
|
|
65
|
+
|
|
66
|
+
## Documentation
|
|
67
|
+
|
|
68
|
+
* [Introduction](docs/00_Introduction.md) — what carray-jit is: the gap it fills beside CArray, the subset a block is written in, and where a kernel gets its data
|
|
69
|
+
* [Getting started](docs/01_GettingStarted.md) — the block, its extents, what the three methods return, and where a kernel stands beside `a + b * c` and `CArray.fuse`
|
|
70
|
+
* [The shapes a kernel takes](docs/02_KernelShapes.md) — work that reaches no neighbour, extents and subscripts, stencils, reductions, and contraction over a repeated index
|
|
71
|
+
* [Supported features](docs/03_SupportedFeatures.md) — locals and types, branches, raising, the types that are not just a number, calling C, and the recognized subset with what it refuses
|
|
72
|
+
* [Compiling, caching and inspecting](docs/04_Compiling.md) — what the first call costs, where kernels are kept, reading the generated C, the `carray-jit` command, and what the suite checks
|
|
73
|
+
* [Design notes](docs/05_DesignNotes.md) — decisions that were not obvious, and why
|
|
74
|
+
* [Cheatsheet](docs/06_Cheatsheet.md) — the seven `jit_` methods and `CArray.fuse` on one page, to look up rather than to read
|
|
75
|
+
|
|
76
|
+
## Contributing
|
|
77
|
+
|
|
78
|
+
Bug reports and feature requests are welcome — please open an issue.
|
|
79
|
+
|
|
80
|
+
**Before opening a pull request, read [CONTRIBUTING.md](CONTRIBUTING.md).** It is short, and it says which form a contribution is best sent in. A small, self-contained bug fix is fine as a pull request. Anything larger is better started as an issue: code here gets rewritten as a matter of course, so a patch for a larger change is likely to end up reimplemented rather than merged, and describing the problem gets you further than writing one.
|
|
81
|
+
|
|
82
|
+
## Credits
|
|
83
|
+
|
|
84
|
+
carray-jit was designed and reviewed by a human developer; the implementation was produced in collaboration with AI coding tools.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
data/bin/carray-jit
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Cache management for carray-jit.
|
|
5
|
+
#
|
|
6
|
+
# Loads only what it needs -- the compiler and its cache -- so that inspecting
|
|
7
|
+
# or clearing the cache works without CArray or the compiled extension being
|
|
8
|
+
# loadable.
|
|
9
|
+
|
|
10
|
+
require "optparse"
|
|
11
|
+
|
|
12
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
13
|
+
|
|
14
|
+
require "carray/jit/version"
|
|
15
|
+
require "carray/jit/errors"
|
|
16
|
+
require "carray/jit/compiler"
|
|
17
|
+
|
|
18
|
+
module CArrayJITCommand
|
|
19
|
+
|
|
20
|
+
Compiler = CArray::JIT::Compiler
|
|
21
|
+
|
|
22
|
+
BANNER = <<~TEXT
|
|
23
|
+
Usage: carray-jit <command> [options]
|
|
24
|
+
|
|
25
|
+
Commands:
|
|
26
|
+
status where the cache is and how big it is (default)
|
|
27
|
+
list the kernels cached for this environment
|
|
28
|
+
show <prefix> the C source of one cached kernel
|
|
29
|
+
clear remove this environment's kernels
|
|
30
|
+
clear --all remove every environment's kernels
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
TEXT
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
|
|
37
|
+
def run (argv)
|
|
38
|
+
options = { :all => false }
|
|
39
|
+
parser = OptionParser.new do |option|
|
|
40
|
+
option.banner = BANNER
|
|
41
|
+
option.on("--all", "clear every version and architecture") {
|
|
42
|
+
options[:all] = true
|
|
43
|
+
}
|
|
44
|
+
option.on("-v", "--version", "print the carray-jit version") do
|
|
45
|
+
puts CArray::JIT::VERSION
|
|
46
|
+
return 0
|
|
47
|
+
end
|
|
48
|
+
option.on("-h", "--help", "print this message") do
|
|
49
|
+
puts option
|
|
50
|
+
return 0
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
arguments = parser.parse(argv)
|
|
54
|
+
|
|
55
|
+
case arguments.shift
|
|
56
|
+
when nil, "status" then status
|
|
57
|
+
when "list" then list
|
|
58
|
+
when "show" then show(arguments.shift)
|
|
59
|
+
when "clear" then clear(options[:all])
|
|
60
|
+
else
|
|
61
|
+
warn(parser.to_s)
|
|
62
|
+
1
|
|
63
|
+
end
|
|
64
|
+
rescue OptionParser::ParseError => error
|
|
65
|
+
warn("carray-jit: #{error.message}")
|
|
66
|
+
1
|
|
67
|
+
rescue CArray::JIT::Error => error
|
|
68
|
+
warn("carray-jit: #{error.message}")
|
|
69
|
+
1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def status
|
|
75
|
+
puts "root #{Compiler.cache_root}"
|
|
76
|
+
puts "environment #{File.basename(Compiler.cache_directory)}"
|
|
77
|
+
puts "kernels #{Compiler.entry_count}"
|
|
78
|
+
puts "size #{human(Compiler.byte_size)}"
|
|
79
|
+
puts "limit #{Compiler.entry_limit} kernels"
|
|
80
|
+
|
|
81
|
+
others = Compiler.stale_environments
|
|
82
|
+
unless others.empty?
|
|
83
|
+
puts
|
|
84
|
+
puts "other environments:"
|
|
85
|
+
others.sort.each do |path|
|
|
86
|
+
puts format(" %-32s %8s %s",
|
|
87
|
+
File.basename(path), human(directory_size(path)),
|
|
88
|
+
last_used(path))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
0
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def list
|
|
95
|
+
entries = Compiler.entries.sort_by { |path| File.mtime(path) }.reverse
|
|
96
|
+
if entries.empty?
|
|
97
|
+
puts "no kernels cached for #{File.basename(Compiler.cache_directory)}"
|
|
98
|
+
return 0
|
|
99
|
+
end
|
|
100
|
+
entries.each do |path|
|
|
101
|
+
key = File.basename(path).sub(/\.[^.]+\z/, "")
|
|
102
|
+
puts format("%-12s %8s %s %s",
|
|
103
|
+
key[0, 12], human(File.size(path)),
|
|
104
|
+
File.mtime(path).strftime("%Y-%m-%d %H:%M"),
|
|
105
|
+
origin(path))
|
|
106
|
+
end
|
|
107
|
+
0
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def show (prefix)
|
|
111
|
+
unless prefix
|
|
112
|
+
warn("carray-jit: show needs the start of a kernel's key")
|
|
113
|
+
return 1
|
|
114
|
+
end
|
|
115
|
+
matches = Compiler.entries.select { |path|
|
|
116
|
+
File.basename(path).start_with?(prefix)
|
|
117
|
+
}
|
|
118
|
+
case matches.size
|
|
119
|
+
when 0
|
|
120
|
+
warn("carray-jit: no cached kernel starts with #{prefix}")
|
|
121
|
+
1
|
|
122
|
+
when 1
|
|
123
|
+
source = source_path(matches.first)
|
|
124
|
+
unless File.exist?(source)
|
|
125
|
+
warn("carray-jit: the source for #{prefix} is missing")
|
|
126
|
+
return 1
|
|
127
|
+
end
|
|
128
|
+
puts File.read(source)
|
|
129
|
+
0
|
|
130
|
+
else
|
|
131
|
+
warn("carray-jit: #{prefix} matches #{matches.size} kernels")
|
|
132
|
+
1
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def clear (everything)
|
|
137
|
+
removed = Compiler.clear(:everything => everything)
|
|
138
|
+
scope = everything ? "every environment" : File.basename(Compiler.cache_directory)
|
|
139
|
+
puts "removed #{removed} kernel#{'s' unless removed == 1} from #{scope}"
|
|
140
|
+
0
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Where the kernel was written, which is what identifies it to whoever is
|
|
144
|
+
# looking: a hash names a kernel, a file and a line explain it. The
|
|
145
|
+
# generated signature is the fallback, for a kernel compiled from a
|
|
146
|
+
# `source:` string that has no file behind it.
|
|
147
|
+
def origin (object_path)
|
|
148
|
+
source = source_path(object_path)
|
|
149
|
+
return "(source missing)" unless File.exist?(source)
|
|
150
|
+
lines = File.readlines(source).first(20)
|
|
151
|
+
found = lines.find { |text| text =~ %r{\A \*\s+(\S+:\d+)\s*\z} }
|
|
152
|
+
return Regexp.last_match(1) if found
|
|
153
|
+
# A kernel compiled from a `source:` string has no file behind it, so
|
|
154
|
+
# the first line of the block stands in for one.
|
|
155
|
+
body = lines.find { |text| text.start_with?(" * ") }
|
|
156
|
+
return body[6..].strip[0, 60] if body
|
|
157
|
+
"(no origin recorded)"
|
|
158
|
+
rescue StandardError
|
|
159
|
+
"(unreadable)"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def source_path (object_path)
|
|
163
|
+
object_path.sub(/\.[^.]+\z/, ".c")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def directory_size (path)
|
|
167
|
+
Dir[File.join(path, "*")].sum { |entry|
|
|
168
|
+
File.file?(entry) ? File.size(entry) : 0
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def last_used (path)
|
|
173
|
+
newest = Dir[File.join(path, "*")].map { |entry|
|
|
174
|
+
File.mtime(entry) rescue Time.at(0)
|
|
175
|
+
}.max
|
|
176
|
+
newest ? "last used #{newest.strftime('%Y-%m-%d')}" : "empty"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def human (bytes)
|
|
180
|
+
return "#{bytes} B" if bytes < 1024
|
|
181
|
+
units = ["KB", "MB", "GB"]
|
|
182
|
+
value = bytes.to_f
|
|
183
|
+
units.each do |unit|
|
|
184
|
+
value /= 1024
|
|
185
|
+
return format("%.1f %s", value, unit) if value < 1024
|
|
186
|
+
end
|
|
187
|
+
format("%.1f TB", value / 1024)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
exit(CArrayJITCommand.run(ARGV))
|
data/carray-jit.gemspec
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Gem::Specification.new do |spec|
|
|
2
|
+
spec.name = "carray-jit"
|
|
3
|
+
spec.version = File.read(File.expand_path("lib/carray/jit/version.rb", __dir__))[/VERSION\s*=\s*"([^"]+)"/, 1]
|
|
4
|
+
spec.summary = "JIT compilation of CArray kernels written in Ruby"
|
|
5
|
+
spec.description = <<~TEXT
|
|
6
|
+
Write the loop in Ruby, and it runs at the speed of C -- one to two orders
|
|
7
|
+
of magnitude faster than interpreted. It is for what a CArray expression
|
|
8
|
+
cannot say: a cell that reaches its neighbours, a recurrence, a loop
|
|
9
|
+
written out. What may be in a block is a subset of Ruby, and a C compiler
|
|
10
|
+
has to be present at run time.
|
|
11
|
+
TEXT
|
|
12
|
+
spec.authors = ["himotoyoshi"]
|
|
13
|
+
spec.email = ["himotoyoshi@users.noreply.github.com"]
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.homepage = "https://github.com/himotoyoshi/carray-jit"
|
|
16
|
+
|
|
17
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
18
|
+
|
|
19
|
+
spec.files = Dir[
|
|
20
|
+
"lib/**/*.rb",
|
|
21
|
+
"ext/**/*.{c,h,rb}",
|
|
22
|
+
"bin/*",
|
|
23
|
+
"examples/**/*.rb",
|
|
24
|
+
"examples/README.md",
|
|
25
|
+
"README.md",
|
|
26
|
+
"CHANGELOG.md",
|
|
27
|
+
"docs/*.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"carray-jit.gemspec",
|
|
30
|
+
".yardopts",
|
|
31
|
+
]
|
|
32
|
+
spec.require_paths = ["lib"]
|
|
33
|
+
spec.bindir = "bin"
|
|
34
|
+
spec.executables = ["carray-jit"]
|
|
35
|
+
spec.extensions = ["ext/carray_jit_access/extconf.rb"]
|
|
36
|
+
|
|
37
|
+
# A kernel reaches CArray's C by address, so the ceiling is the next minor.
|
|
38
|
+
spec.add_dependency "carray", ">= 3.0.1", "< 3.1"
|
|
39
|
+
# fiddle ships as a bundled gem; depend on it explicitly so Ruby 3.5+ resolves it.
|
|
40
|
+
spec.add_dependency "fiddle"
|
|
41
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Introduction
|
|
2
|
+
|
|
3
|
+
carray-jit compiles a Ruby block into a C kernel that runs over CArray arrays. The block is not evaluated and is not a DSL: it is ordinary Ruby, read as source, translated to C, compiled by the system C compiler and called through Fiddle. What it computes is what the same block would have computed had Ruby run it, one cell at a time, at the speed of compiled C.
|
|
4
|
+
|
|
5
|
+
## What CArray leaves out
|
|
6
|
+
|
|
7
|
+
CArray works on a whole array at once. Arithmetic and the mathematical functions apply to every element, reductions summarise along the axes you name, and sorts and scans walk the data in C. All of that is already fast, and nothing here replaces it.
|
|
8
|
+
|
|
9
|
+
What an array expression has no form for is a computation *between* elements. A cell that reads its neighbours. A cell that depends on the one computed before it. A loop whose length is not known until it runs. These are ordinary algorithms — a tridiagonal solve, a stencil with the boundary rule your problem actually has, a sieve, a recurrence from a paper — and until now writing one in Ruby meant a Ruby loop, and paying what the interpreter charges for every element.
|
|
10
|
+
|
|
11
|
+
This library is for that gap. The parts of an algorithm that have an array form stay with CArray, where they are fastest; the part that does not becomes a kernel. Together they run an algorithm end to end without dropping into a Ruby loop.
|
|
12
|
+
|
|
13
|
+
## A subset, chosen rather than found
|
|
14
|
+
|
|
15
|
+
Not every Ruby block can become C. What may appear in one is a subset — arithmetic and comparison, locals, `if` and `while`, inner loops, subscripts at computed positions, the bitwise operators, the `Math` family, `raise` — and it was chosen deliberately, wide enough to state a numerical algorithm and narrow enough that each construct in it means in C exactly what Ruby means by it. Integer division floors. `%` is not `fmod`. A Complex divides by Smith's method in the order `complex.c` writes it.
|
|
16
|
+
|
|
17
|
+
A block that steps outside the subset is refused, by name and with a line number, rather than quietly running as a Ruby loop. Nobody reaches for a compiler except to make something fast, so falling back silently would answer a question that was not asked.
|
|
18
|
+
|
|
19
|
+
## Where a kernel gets its data
|
|
20
|
+
|
|
21
|
+
A kernel reads and writes CArray arrays directly, in the memory CArray already holds them in. The arrays are the ones the block closes over, so nothing is named twice. Views are cells like any other — a column, a transpose, a slice of a slice — and are written in place without a copy. Masks propagate as CArray propagates them, and a kernel can ask whether a cell is missing.
|
|
22
|
+
|
|
23
|
+
A kernel can also call C. `jit_extern` binds a function from any library Fiddle can open, and the kernel calls it at its address rather than reaching it per cell through Fiddle; `jit_function` compiles one from a Ruby block of your own and hands back a pure C function pointer, which a kernel can call, Ruby can call, and a C library that knows nothing about either can be given.
|
|
24
|
+
|
|
25
|
+
Compiling costs something the first time and nothing afterwards: the shared object is cached on disk, keyed by the generated C and the compiler that built it, so a kernel is compiled once and reused by every later run.
|
|
26
|
+
|
|
27
|
+
## Not only the blocks you write
|
|
28
|
+
|
|
29
|
+
Installing this gem also puts the compiler behind `CArray.fuse`. An array expression written there is compiled rather than walked a node at a time — without being asked for, and without changing the answer. An expression the compiler cannot address goes back to CArray, which walks it and arrives at the same result. So a program that never writes a kernel still gets something from having the gem installed.
|
|
30
|
+
|
|
31
|
+
## How this guide is arranged
|
|
32
|
+
|
|
33
|
+
* [Getting started](01_GettingStarted.md) — the block, its extents, what the three methods return, and where a kernel stands beside `a + b * c` and `CArray.fuse`
|
|
34
|
+
* [The shapes a kernel takes](02_KernelShapes.md) — work that reaches no neighbour, extents and subscripts, stencils, reductions, and contraction over a repeated index
|
|
35
|
+
* [Supported features](03_SupportedFeatures.md) — locals and types, branches, raising, the types that are not just a number, calling C, and the recognized subset with what it refuses
|
|
36
|
+
* [Compiling, caching and inspecting](04_Compiling.md) — what the first call costs, where kernels are kept, reading the generated C, the `carray-jit` command, and what the suite checks
|
|
37
|
+
* [Design notes](05_DesignNotes.md) — decisions that were not obvious, and why
|
|
38
|
+
* [Cheatsheet](06_Cheatsheet.md) — the seven `jit_` methods and `CArray.fuse` on one page, to look up rather than to read
|
|
39
|
+
|
|
40
|
+
[examples/features/](../examples/features) is a tour of the same ground, one file per feature, and [examples/applications/](../examples/applications) holds small programs that use it to do something.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
The block's parameters are the loop indices. Arrays and scalars are the variables it closes over -- `legendre` and `x` here -- so nothing is named twice, and the body reads like the Ruby loop it replaces.
|
|
4
|
+
|
|
5
|
+
An extent is a `Range`, an `Integer` standing for `0...n`, or an `Enumerator::ArithmeticSequence` -- what `(high - 1).step(low, -1)` returns -- one per index. A whole array is therefore `CArray.jit_for(*array.dim) { ... }`.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
CArray.jit_for(1...(rows-1), 1...(columns-1)) { |i, j|
|
|
9
|
+
out[i, j] = 0.25 * (src[i-1, j] + src[i+1, j] + src[i, j-1] + src[i, j+1])
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`jit_for` returns the compiled kernel, whose `#c_source` is the C that ran. So does `jit_each`, the sibling method for work that reaches no neighbour and so needs neither an index nor an extent. `jit_map` is that same method with its value asked for, and returns the array of results instead.
|
|
14
|
+
|
|
15
|
+
[examples/applications/](../examples/applications) holds small programs that use this to do something -- a Game of Life, an implicit heat equation, edge detection, quality control on a sensor record -- each measured against the Ruby loop or the array expression it replaces. [examples/features/](../examples/features) is a tour of everything in these docs, one file per feature. `rake examples` runs them all.
|
|
16
|
+
|
|
17
|
+
## Where the methods come from
|
|
18
|
+
|
|
19
|
+
`CArray.jit_for`, `CArray.jit_each` and `CArray.jit_map` are named by CArray, which defines them to raise: they say that they compile their block, that the compiler is this gem, and that it is not installed. Installing it replaces them with the ones that compile.
|
|
20
|
+
|
|
21
|
+
The name carries the rest. `jit_` says the block is read rather than run, and so has rules about what may be in it; a method called `per_cell` would not, and the subset would be something you found out about later. It also marks which methods need the compiler and which do not: an expression over whole arrays is `CArray.fuse`'s, and that one needs nothing installed.
|
|
22
|
+
|
|
23
|
+
The line between these two falls where the block's names fall -- a block that names indices is `jit_for`'s, a block that names none is `jit_each`'s -- and each turns the other's block away rather than quietly doing something with it.
|
|
24
|
+
|
|
25
|
+
## No fallback
|
|
26
|
+
|
|
27
|
+
A block outside the compilable subset raises `CArray::JIT::Unsupported`, and so does one written with no compiler installed. Neither quietly runs the block as a Ruby loop instead.
|
|
28
|
+
|
|
29
|
+
Nobody writes `jit_for` except to make a per-cell computation fast, so running it a hundred times slower would answer a question that was not asked, and would hide the difference behind a call that looks the same either way. An expression that can be written without a loop is better written as `CArray.fuse`, which is one pass with or without this gem.
|
|
30
|
+
|
|
31
|
+
## Four ways to compute an expression, and what separates them
|
|
32
|
+
|
|
33
|
+
CArray can evaluate `out = (a + b) * (c - a) + b * c - a` four ways now, and the line between them is not a factor -- it is what `n` gets multiplied by. `CArray.fuse` builds the expression rather than evaluating it a step at a time, and CArray walks what it built; requiring this gem registers a second evaluator (`CArray::JIT::Expression`), so an expression over an array worth compiling for is compiled instead. Where that line falls is CArray's to decide, not this gem's -- ten thousand cells today. So `fuse` is two rows here, and which one you get depends on whether the gem is installed.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
n = 4,000,000
|
|
37
|
+
a + b * c the longer expression
|
|
38
|
+
a + b * c ... 1.30 ns/el 4.42 ns/el
|
|
39
|
+
CArray.fuse, walked 0.99 ns/el 2.13 ns/el
|
|
40
|
+
CArray.fuse, compiled here 0.74 ns/el 0.66 ns/el
|
|
41
|
+
jit_each { out = ... } 0.32 ns/el 0.30 ns/el
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
An expression of `k` operations is `k` passes over the data, and walking it rather than materialising it does not change that -- it changes what each pass costs, which is why the first two rows still grow with the expression, one more steeply than the other. Compiled, the expression is one pass however long it is, which is why the last two rows do not move at all.
|
|
45
|
+
|
|
46
|
+
Memory separates them again, on a different line. Summing `(a + b*1) + (a + b*2) + ...`, peak resident over two arrays of eight million doubles, one array being 61 MB (`benchmark/footprint.rb`):
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
terms 1 2 3 4 6
|
|
50
|
+
a + b*k, summed +123 +245 +367 +488 +611
|
|
51
|
+
fuse walked, leaning left +122 +184 +185 +183 +184
|
|
52
|
+
fuse walked, leaning right +123 +184 +246 +306 +428
|
|
53
|
+
fuse compiled, leaning left +62 +61 +61 +62 +61
|
|
54
|
+
fuse compiled, leaning right +61 +62 +63 +62 +62
|
|
55
|
+
jit_each +0 +1 +1 +1 +1
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**CArray** holds one array per term, exactly: each waits as an array until it is combined.
|
|
59
|
+
|
|
60
|
+
**`fuse` walked** answers to the shape rather than the size, and the shape is which way the tree leans: a binary operation pulls its left operand into the buffer it was handed and takes a scratch only for its right, so leaning left descends for free and leaning right holds one buffer per level. The two rows are the same arithmetic written two ways, and they are not the same measurement. The scratch comes from an arena that keeps buffers warm rather than freeing them, so the peak is also what is still held afterwards.
|
|
61
|
+
|
|
62
|
+
**`fuse` compiled** holds the result and nothing else: one array, whatever the expression says and whichever way it leans -- there is no operand to stage when every operation is in one loop.
|
|
63
|
+
|
|
64
|
+
**A compiled kernel** holds nothing at all, because it was given the array to write into: a value never leaves a register between one operation and the next.
|
|
65
|
+
|
|
66
|
+
Only the columns are comparable, not the rows against some other program's numbers: a peak belongs to a process, and the floor moves with whatever it loaded and allocated first.
|
|
67
|
+
|
|
68
|
+
| | needs | passes | intermediates | reaches a neighbour |
|
|
69
|
+
| --- | --- | --- | --- | --- |
|
|
70
|
+
| `a + b * c` | -- | one per operation | one per operation | -- |
|
|
71
|
+
| `CArray.fuse`, walked | -- | one per operation | one per right-spine level | -- |
|
|
72
|
+
| `CArray.fuse`, compiled | a C compiler, or it walks | **one** | **none** | -- |
|
|
73
|
+
| `CArray.jit_each` | a C compiler | **one** | **none** | no |
|
|
74
|
+
| `CArray.jit_for` | a C compiler | **one** | **none** | **yes** |
|
|
75
|
+
|
|
76
|
+
The last two need a compiler outright: without one they raise rather than run slowly, because a block written for them is written to be compiled and a Ruby loop over a million cells is not an answer. `CArray.fuse` never raises for the want of one -- it asks whatever evaluator is registered and walks the expression itself when the answer is nobody, which is what makes it the thing to write where an expression over whole arrays is the whole of it.
|
|
77
|
+
|
|
78
|
+
The last two are the same machinery and differ in what can be said, not in speed: a recurrence, a stencil and a reduction are `jit_for`'s and are not one pass in any of the others -- they are not expressible in them at all.
|
|
79
|
+
|
|
80
|
+
What the table does not show is the compiling, which is about 250 ms the first time and 4 ms in a later process that finds the object on disk. For one pass over a small array the plain expression wins on wall clock and always will; see [What compiling costs](04_Compiling.md#what-compiling-costs-and-where-kernels-are-kept) for where the line falls.
|