carray-jit 0.1.5 → 0.1.7
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 +51 -0
- data/README.md +2 -2
- data/carray-jit.gemspec +4 -2
- data/docs/02_KernelShapes.md +2 -2
- data/docs/03_SupportedFeatures.md +2 -2
- data/docs/04_Compiling.md +5 -5
- data/docs/08_LanguageReference.md +2 -2
- data/lib/carray/jit/analyzer.rb +1 -1
- data/lib/carray/jit/c_function.rb +140 -139
- data/lib/carray/jit/c_generator.rb +123 -24
- data/lib/carray/jit/compiler.rb +44 -26
- data/lib/carray/jit/expression.rb +79 -29
- data/lib/carray/jit/kernel.rb +23 -47
- data/lib/carray/jit/native.rb +149 -0
- data/lib/carray/jit/sweep.rb +14 -64
- data/lib/carray/jit/version.rb +1 -1
- data/lib/carray/jit.rb +96 -19
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 27ac95a6f004e19365a64f30d0a6e87384b1e20878b9374d8faf5b2cb328070d
|
|
4
|
+
data.tar.gz: d9ce3b1c9e3ebe5bc6c06288c4480fc25b1d88f58be5af635653e3f72067a637
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 80b9d9cc62dc9d6042fc6a1aac00071e0a29084b00c060872ccc0f72777b15a583c8a052f85126e94922f1e1b5c7fefdbdad70d46f7ecf1d5b26140690bfe4bc
|
|
7
|
+
data.tar.gz: c2ca9876c7d8c1d23e63290e2273e2219004932d19d2ffcf2c118a482c29b50bdd0570c55f4ec687908a916e751a0a58212e312ded6c279f977da2dc62c54cba
|
data/CHANGELOG.md
CHANGED
|
@@ -37,6 +37,57 @@ version you have and a newer one.
|
|
|
37
37
|
a release needs is said in the gemspec, and an entry says so only
|
|
38
38
|
when the answer changes. -->
|
|
39
39
|
|
|
40
|
+
## 0.1.7
|
|
41
|
+
|
|
42
|
+
- Change: needs CArray 3.0.6 or later in the 3.0 series; install it
|
|
43
|
+
alongside this release. An element-wise pass is handed to
|
|
44
|
+
`CArray::AddressBasis.sweep`, which 3.0.6 is the first to have.
|
|
45
|
+
|
|
46
|
+
- Change: the kernel cache keeps 2000 kernels per environment by
|
|
47
|
+
default, up from 512, which is about 60 MB on disk. A program that
|
|
48
|
+
compiled more than 512 rebuilt the ones evicted each time it ran. Set
|
|
49
|
+
`CARRAY_JIT_CACHE_LIMIT` for a different bound.
|
|
50
|
+
|
|
51
|
+
- Change: `jit_for` and `jit_init` cost less per call when called again
|
|
52
|
+
with operands that agree in name, type and mask, and a kernel writing
|
|
53
|
+
an `int64` array is vectorised as a `float64` one is.
|
|
54
|
+
|
|
55
|
+
- Fix: a lazy expression or `CArray.fuse` computed here keeps the sign of
|
|
56
|
+
a NaN as CArray does: `(a * -1).signbit` and a NaN fill of `shift`
|
|
57
|
+
gave other bits with the evaluator registered.
|
|
58
|
+
|
|
59
|
+
- Fix: kernels built at once by several threads, or in Ractors, no
|
|
60
|
+
longer raise `Errno::ENOENT`.
|
|
61
|
+
|
|
62
|
+
## 0.1.6
|
|
63
|
+
|
|
64
|
+
- Change: needs CArray 3.0.5 or later in the 3.0 series; install it
|
|
65
|
+
alongside this release. A Float stored into an integer array in a block
|
|
66
|
+
is refused as CArray 3.0.5's own store refuses it, and under 3.0.4 the
|
|
67
|
+
block would raise where CArray's store does not.
|
|
68
|
+
|
|
69
|
+
- Change: a Float stored into an integer array in a block, captured or
|
|
70
|
+
local, that the type has no integer for raises `RangeError`, or
|
|
71
|
+
`FloatDomainError` for a NaN or an infinity, as CArray 3.0.5's own
|
|
72
|
+
store does. Before, it stored whatever the machine's C conversion gave,
|
|
73
|
+
which differed between x86 and ARM. Round and clamp first where the
|
|
74
|
+
value may leave the range. A masked cell, and a `jit_function`'s return
|
|
75
|
+
value or pointer parameter, are not checked.
|
|
76
|
+
|
|
77
|
+
- Fix: an int64 array handed to a C function declared to take
|
|
78
|
+
`long long *` compiles with gcc, as it did with clang; gcc 14 raised
|
|
79
|
+
CompilationError. An array of the wrong element type raises Unsupported,
|
|
80
|
+
naming the type the function takes, under gcc as under clang.
|
|
81
|
+
|
|
82
|
+
- Fix: `finite?` on an integer cell in a block compiles with gcc; it
|
|
83
|
+
raised CompilationError there, while clang accepted it. It is true for
|
|
84
|
+
every integer, as in Ruby.
|
|
85
|
+
|
|
86
|
+
- Fix: a lazy expression or `CArray.fuse` computed here now gives the
|
|
87
|
+
same bits as CArray's own walk where gcc is the compiler. On x86 with
|
|
88
|
+
`-march=native`, a multiply followed by an add could come out one
|
|
89
|
+
rounding off in the last bit. Builds with clang were not affected.
|
|
90
|
+
|
|
40
91
|
## 0.1.5
|
|
41
92
|
|
|
42
93
|
- Change: needs CArray 3.0.4 or later in the 3.0 series; install it
|
data/README.md
CHANGED
|
@@ -8,7 +8,7 @@ The block is read with Prism, translated to C if it falls inside that subset, co
|
|
|
8
8
|
|
|
9
9
|
## Status
|
|
10
10
|
|
|
11
|
-
0.1.
|
|
11
|
+
0.1.6 is the current release, and it still moves: behaviour can change between releases.
|
|
12
12
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
@@ -35,7 +35,7 @@ gem "carray-jit"
|
|
|
35
35
|
Requires:
|
|
36
36
|
|
|
37
37
|
- Ruby >= 3.2
|
|
38
|
-
- CArray >= 3.0.
|
|
38
|
+
- CArray >= 3.0.6, < 3.1
|
|
39
39
|
- A C compiler
|
|
40
40
|
- Prism and Fiddle (both ship with Ruby; Fiddle is a bundled gem)
|
|
41
41
|
|
data/carray-jit.gemspec
CHANGED
|
@@ -33,7 +33,9 @@ Gem::Specification.new do |spec|
|
|
|
33
33
|
spec.executables = ["carray-jit"]
|
|
34
34
|
|
|
35
35
|
# A kernel reaches CArray's C by address, so the ceiling is the next minor.
|
|
36
|
-
spec.add_dependency "carray", ">= 3.0.
|
|
36
|
+
spec.add_dependency "carray", ">= 3.0.6", "< 3.1"
|
|
37
37
|
# fiddle ships as a bundled gem; depend on it explicitly so Ruby 3.5+ resolves it.
|
|
38
|
-
|
|
38
|
+
# 1.1.1, which Ruby 3.2 ships, is the first with TYPE_UCHAR and the other
|
|
39
|
+
# unsigned types.
|
|
40
|
+
spec.add_dependency "fiddle", ">= 1.1.1"
|
|
39
41
|
end
|
data/docs/02_KernelShapes.md
CHANGED
|
@@ -46,7 +46,7 @@ It is the same shape of block `CArray.jit_function` takes, and not the same thin
|
|
|
46
46
|
|
|
47
47
|
#### Who drives the loop
|
|
48
48
|
|
|
49
|
-
An element-wise pass is the one shape that is a *sweep* -- nothing reaches a neighbour, nothing chooses an order -- so CArray can drive it instead
|
|
49
|
+
An element-wise pass is the one shape that is a *sweep* -- nothing reaches a neighbour, nothing chooses an order -- so CArray can drive it instead: `jit_each` hands its chunked sweep (`ca_call_cslab`) the compiled body and lets it acquire the operands.
|
|
50
50
|
|
|
51
51
|
The body is the same either way. The sweep entry point is a wrapper that calls the same kernel with the chunk as its bounds, so what changes is who opens the arrays, not what they compute.
|
|
52
52
|
|
|
@@ -56,7 +56,7 @@ A **strided view** is the case where there is. A column, a transpose, every othe
|
|
|
56
56
|
|
|
57
57
|
The rank does not stand in the way, and the arrays are not reshaped to get it out of the way. CArray's acquire reads an operand's element count and element size and never looks at its shape, so a chunk is already a flat run of cells; what has to be flat is the *kernel*, and it is compiled at rank one over the same cells rather than as a nest over the axes. One such kernel then serves whatever rank it is given, because the rank has left the loop.
|
|
58
58
|
|
|
59
|
-
Four things keep the driver here. An operand that is a **strided view**, for the reason just measured: it is walked in place here, and re-gathered there for nothing. A **stretched operand**, which is the one thing that cannot be flattened: broadcasting arrives as a stride of zero on an axis, and a flat run has no axes, so cell k of the output would stop lining up with cell k of the operand. A body that **asks about a mask**, because a chunk carries no per-cell mask to ask it of -- CArray ORs and propagates the masks, but `a[i] == UNDEF` is a question about one cell.
|
|
59
|
+
Four things keep the driver here. An operand that is a **strided view**, for the reason just measured: it is walked in place here, and re-gathered there for nothing. A **stretched operand**, which is the one thing that cannot be flattened: broadcasting arrives as a stride of zero on an axis, and a flat run has no axes, so cell k of the output would stop lining up with cell k of the operand. A body that **asks about a mask**, because a chunk carries no per-cell mask to ask it of -- CArray ORs and propagates the masks, but `a[i] == UNDEF` is a question about one cell. And **more than seven operands**, because seven is the widest the sweep family is generated for.
|
|
60
60
|
|
|
61
61
|
Whichever drives it, an operand that is **read while the pass writes the array it is a view of** is copied once before the loop. `a = b * 2.0` reads the whole right-hand side and then assigns; a kernel walks cell by cell, which is the same thing until a cell it reads is one it has written -- `hi = lo * 2.0` over two blocks that share six cells, a transpose written over itself. The copy is what makes those read the values the expression started with, as CArray's own operators do, and it is what lets the chunked sweep take them: what it re-gathers per chunk is then a copy nothing is writing to. Sharing a root is the test rather than overlapping cells, a view knowing what it is a view of; `a = a + 1.0` is not that case, the cell read being the cell written.
|
|
62
62
|
|
|
@@ -854,7 +854,7 @@ A window may be opened inside a window, and `#call` may be made inside one: both
|
|
|
854
854
|
|
|
855
855
|
One flag per compiled object and one per thread, so two functions never share a window and neither do two threads. The second half matters more than it sounds: the object is shared whether or not anybody meant to share it, because two blocks with the same text compile to one function. So a program that never mentions threads can open two windows on one body, and one flag between them is a window neither of them can read -- the second to open puts down what the first had already recorded. The flag and the hook are `_Thread_local`, which costs about half a nanosecond a call, and only on a body that has a flag at all.
|
|
856
856
|
|
|
857
|
-
The thread is the native one, which for Ruby is the thread that opened the window -- except under the M:N scheduler
|
|
857
|
+
The thread is the native one, which for Ruby is the thread that opened the window -- except under the M:N scheduler, where a Ruby thread is not tied to one native thread and the window cannot be relied on to be the same copy throughout. Which threads that is depends on `RUBY_MN_THREADS`: unset or `0`, a non-main Ractor's threads; `1`, the main Ractor's threads other than the main thread as well; and from Ruby 4.1, `2`, the main thread too, so that no thread is outside it. `-1`, also from Ruby 4.1, takes every thread out of it.
|
|
858
858
|
|
|
859
859
|
What this leaves outside the window is a library that calls the body from threads of its own. The flag such a call raises belongs to the thread that raised it: the body stops answering there, as it should, and the window watching from Ruby sees nothing and raises nothing. A library that calls back on the thread it was called from -- which is what GSL and NLopt do -- is inside the window.
|
|
860
860
|
|
|
@@ -1223,6 +1223,6 @@ Everything else: `for` and `until`, `begin ... end while`, strings, hashes, symb
|
|
|
1223
1223
|
- **A local array's subscript counts from the start.** CArray's `w[-1]` is the last cell; a kernel has no such reading, so a literal negative subscript is refused by name and one the kernel works out raises `IndexError`. This is the difference the captured arrays already have. Write `w[n - 1]`.
|
|
1224
1224
|
- **`CArray.empty` leaves its cells as the stack left them.** Reading one before writing it is out of contract, the way reading under a mask is: the value is whatever was there. What it saves is the clearing, which for a 256-cell array inside a per-cell loop is 2 KiB a cell.
|
|
1225
1225
|
- **The stack limits are per function, and a chain of them is not counted.** One local array up to 4 KiB stands in a frame, and the arrays of one kernel body or one function body up to 16 KiB together; past either, a kernel allocates the array at its entry, while a function body -- which is called once per cell -- refuses it and asks the calling kernel for a pointer instead. A pasted function called from a kernel is a second frame, and a chain of three such functions can therefore stand 48 KiB deep while each of them is inside its own limit -- counting along the chain was considered and dropped, since the depth is not knowable where recursion is in the subset at all. What that costs is the bargain a deep recursion already takes: too deep is a SIGSEGV rather than a `SystemStackError`, and a body carrying arrays reaches it sooner. Both numbers are provisional, and neither counts a future thread pool's stacks.
|
|
1226
|
-
- **Storing a real number into an integer array is
|
|
1226
|
+
- **Storing a real number into an integer array is checked.** It keeps the integer part, as CArray's own store does, and a value past the type's range raises `RangeError` -- `FloatDomainError` for a NaN or an infinity -- where CArray's store raises the same; C leaves that conversion undefined, and x86 and ARM answer it differently. Like any error at a cell it is raised when the pass ends, and the cell holds 0 in the meantime. A masked cell is not checked. The check is a comparison per store; round and clamp before storing where the value may leave the range and the cost shows. A compiled function's return value and a write through its pointer parameter are not this case: they are C's conversion, as the declaration says. Nor are the rounding methods: `floor`, `ceil`, `round`, `truncate` and `to_i` on a Float raise `FloatDomainError` for a NaN or an infinity, as Ruby does, and `RangeError` where the Integer is past int64 -- except written straight into a float cell, which holds it whole (`1e20.floor` is `1e20`). The check costs a comparison per call, and in a loop that does little else it shows.
|
|
1227
1227
|
- **A block's source must be recoverable.** Blocks defined in `eval` or in a console have no file to read back; set `RubyVM.keep_script_lines = true` before defining them, or hand the text to `CArray::JIT.compile`, which takes a kernel as a string rather than as a block. This also ties the gem to CRuby, which CArray requires anyway.
|
|
1228
1228
|
- **Nothing existing is replaced.** `jit_for` is a new method, not a faster `each_index`: the two differ in what they reject, and a caller should be able to choose.
|
data/docs/04_Compiling.md
CHANGED
|
@@ -67,7 +67,7 @@ In `~/.cache/carray-jit` (honouring `XDG_CACHE_HOME`), one directory per version
|
|
|
67
67
|
|
|
68
68
|
A kernel is only good for the version that generated it and the architecture it was built for, so those get their own directories. CArray's version is there for the same reason and one of its own: a kernel is handed CArray's memory, on layouts CArray decides, so one compiled against one version and run against the next would answer wrongly rather than fail to load.
|
|
69
69
|
|
|
70
|
-
The two numbers say different things and move on their own clocks: the first is the version that generated the kernel, the second the version it was generated against. This gem is not versioned with CArray, and one release of it meets more than one -- the dependency is `>= 3.0.
|
|
70
|
+
The two numbers say different things and move on their own clocks: the first is the version that generated the kernel, the second the version it was generated against. This gem is not versioned with CArray, and one release of it meets more than one -- the dependency is `>= 3.0.6, < 3.1`, so 3.0.6 and 3.0.7 both satisfy it -- and a layout the kernel reaches into can move between them. A new release does not spend its cache budget on entries nothing can reach any more, and a home directory shared between machines -- over NFS, or between Rosetta and native -- does not have one architecture evicting the other's kernels. A directory whose newest entry has not been touched in 30 days (`CARRAY_JIT_CACHE_MAX_AGE_DAYS`) is removed.
|
|
71
71
|
|
|
72
72
|
```ruby
|
|
73
73
|
CArray::JIT.cache_root #=> "/home/you/.cache/carray-jit"
|
|
@@ -99,7 +99,7 @@ The path is expanded where it is given, so a relative one does not move when the
|
|
|
99
99
|
|
|
100
100
|
`CARRAY_JIT_CACHE` and `CARRAY_JIT_NO_CACHE` still come first, so whoever runs the program has the last word on where a cache may be written and whether there is one at all. `CArray::JIT.cache_root = nil` restores the default.
|
|
101
101
|
|
|
102
|
-
The cache is **bounded**: past `CARRAY_JIT_CACHE_LIMIT` kernels per environment (
|
|
102
|
+
The cache is **bounded**: past `CARRAY_JIT_CACHE_LIMIT` kernels per environment (2000 by default, so about 60 MB) the least recently used are evicted, source and object together. Reuse updates an entry's timestamp, so what a program actually runs stays. This is the one place carray-jit deliberately parts with RubyInline, whose `~/.ruby_inline` has no eviction at all and grows for the life of the account.
|
|
103
103
|
|
|
104
104
|
Removing a cached object never breaks a kernel already in use: unlinking a loaded shared object leaves its mapping intact.
|
|
105
105
|
|
|
@@ -143,7 +143,7 @@ carray_jit_contiguous (char **pointers, int64_t *strides, int64_t *bounds, ...)
|
|
|
143
143
|
const int64_t legendre_s0 = strides[0];
|
|
144
144
|
const double x = reals[0];
|
|
145
145
|
|
|
146
|
-
for (int64_t i = bounds[0]
|
|
146
|
+
for (int64_t i = bounds[0], i__limit = bounds[1]; i < i__limit; i++) {
|
|
147
147
|
double w;
|
|
148
148
|
double wy;
|
|
149
149
|
w = x * ((double *)(p_legendre))[i - 1];
|
|
@@ -165,7 +165,7 @@ root /home/you/.cache/carray-jit
|
|
|
165
165
|
environment 0.1.0-carray3.0.1-arm64-darwin24
|
|
166
166
|
kernels 3
|
|
167
167
|
size 50.1 KB
|
|
168
|
-
limit
|
|
168
|
+
limit 2000 kernels
|
|
169
169
|
|
|
170
170
|
other environments:
|
|
171
171
|
0.0.9-carray3.0.1-arm64-darwin24 1.2 MB last used 2026-01-01
|
|
@@ -217,7 +217,7 @@ carray_jit_contiguous (char **pointers, int64_t *strides, int64_t *bounds, ...)
|
|
|
217
217
|
| `CARRAY_JIT_DUMP` | Print generated C to stderr before compiling |
|
|
218
218
|
| `CARRAY_JIT_CACHE` | Cache directory (default `~/.cache/carray-jit`, or what `CArray::JIT.cache_root=` named), or `none` |
|
|
219
219
|
| `CARRAY_JIT_NO_CACHE` | Keep the cache in a temporary directory, removed on exit |
|
|
220
|
-
| `CARRAY_JIT_CACHE_LIMIT` | Kernels retained on disk, per environment (default
|
|
220
|
+
| `CARRAY_JIT_CACHE_LIMIT` | Kernels retained on disk, per environment (default 2000) |
|
|
221
221
|
| `CARRAY_JIT_CACHE_MAX_AGE_DAYS` | Days an unused environment's directory is kept (default 30) |
|
|
222
222
|
| `CARRAY_JIT_CC` | C compiler (default `RbConfig::CONFIG["CC"]`) |
|
|
223
223
|
| `CARRAY_JIT_REASSOCIATE` | `0` makes the serial accumulator the default for the process |
|
|
@@ -395,7 +395,7 @@ These are deliberate. Everything not listed here gives Ruby's answer, to the las
|
|
|
395
395
|
- **A complex raised to a power** uses C's `cpow`, which can differ from Ruby in the last bit.
|
|
396
396
|
- **A sine beside a cosine of the same argument** may be computed together by the C compiler, which can change the last bit.
|
|
397
397
|
- **A reduction is reassociated** into partial sums unless `reassociate: false` is given, so its last bits can differ from adding in order.
|
|
398
|
-
- **A real stored into an integer array**
|
|
398
|
+
- **A real stored into an integer array** keeps its integer part, as CArray's store does, and one past the type's range raises `RangeError` where Ruby's `to_i` would grow. A NaN or an infinity raises `FloatDomainError`, as `to_i` does.
|
|
399
399
|
|
|
400
400
|
**The language**
|
|
401
401
|
|
|
@@ -425,4 +425,4 @@ A block reads like Ruby run over one cell at a time, and it answers as that Ruby
|
|
|
425
425
|
| `\|` and `&` of a masked boolean (`UNDEF \| true`) | Kleene: `true` | `\|\|` and `&&`: masked whenever an operand is |
|
|
426
426
|
| a `uint64` compared with a negative `int64` | compares by value: `7 < -7` is false | compared as two `uint64`: `7 < -7` is true |
|
|
427
427
|
|
|
428
|
-
Where they agree, the block keeps CArray's answer: integer `/` and `%` floor, a shift means what it means to `Integer`, `INT64_MIN / -1` is `INT64_MIN`, overflow wraps, an Integer meets a Float as two doubles, narrow types compute narrow, `Math` outside its domain is NaN, a real
|
|
428
|
+
Where they agree, the block keeps CArray's answer: integer `/` and `%` floor, a shift means what it means to `Integer`, `INT64_MIN / -1` is `INT64_MIN`, overflow wraps, an Integer meets a Float as two doubles, narrow types compute narrow, `Math` outside its domain is NaN, a real stored into an integer cell keeps its integer part and raises `RangeError` past the type's range (`FloatDomainError` for a NaN or an infinity), and a masked operand gives a masked result.
|
data/lib/carray/jit/analyzer.rb
CHANGED
|
@@ -1880,7 +1880,7 @@ class CArray
|
|
|
1880
1880
|
"`#{name}` to; declare what it points at",
|
|
1881
1881
|
argument.location)
|
|
1882
1882
|
end
|
|
1883
|
-
wanted = CDeclaration::DATA_TYPES.fetch(parameter.element.
|
|
1883
|
+
wanted = CDeclaration::DATA_TYPES.fetch(parameter.element.abi).to_s
|
|
1884
1884
|
unless storage == wanted
|
|
1885
1885
|
raise Unsupported.new(
|
|
1886
1886
|
"`#{c_function}` takes `#{parameter.text}` there, which is a " \
|