cumo 0.10.0 → 0.11.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 +4 -4
- data/CHANGELOG.md +42 -0
- data/README.md +49 -208
- data/ext/cumo/cuda/cudnn_impl.cpp +2 -3
- data/ext/cumo/cuda/driver.c +57 -1
- data/ext/cumo/cuda/memory_pool.cpp +12 -2
- data/ext/cumo/depend.erb +1 -1
- data/ext/cumo/include/cumo/bit_reduce_kernel.h +2 -2
- data/ext/cumo/include/cumo/cuda/cumo_thrust.hpp +7 -0
- data/ext/cumo/include/cumo/indexer.h +126 -9
- data/ext/cumo/include/cumo/intern.h +5 -1
- data/ext/cumo/include/cumo/reduce_kernel.h +29 -27
- data/ext/cumo/include/cumo/row_kernel.h +77 -15
- data/ext/cumo/include/cumo/template.h +1 -5
- data/ext/cumo/include/cumo/template_kernel.h +14 -0
- data/ext/cumo/include/cumo.h +2 -2
- data/ext/cumo/narray/data.c +84 -0
- data/ext/cumo/narray/data_kernel.cu +220 -76
- data/ext/cumo/narray/gen/cogen_kernel.rb +24 -1
- data/ext/cumo/narray/gen/tmpl/binary2_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/binary_kernel.cu +72 -22
- data/ext/cumo/narray/gen/tmpl/binary_s_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/clip_kernel.cu +4 -31
- data/ext/cumo/narray/gen/tmpl/cond_binary_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/cond_unary_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/cum.c +11 -60
- data/ext/cumo/narray/gen/tmpl/cum_kernel.cu +53 -24
- data/ext/cumo/narray/gen/tmpl/ewcomp_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/fill.c +11 -3
- data/ext/cumo/narray/gen/tmpl/fill_kernel.cu +20 -11
- data/ext/cumo/narray/gen/tmpl/frexp_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/layer_norm_kernel.cu +98 -0
- data/ext/cumo/narray/gen/tmpl/logseq.c +28 -14
- data/ext/cumo/narray/gen/tmpl/logseq_kernel.cu +19 -23
- data/ext/cumo/narray/gen/tmpl/poly_kernel.cu +1 -10
- data/ext/cumo/narray/gen/tmpl/pow_kernel.cu +3 -21
- data/ext/cumo/narray/gen/tmpl/rand.c +9 -3
- data/ext/cumo/narray/gen/tmpl/rand_kernel.cu +10 -14
- data/ext/cumo/narray/gen/tmpl/rand_norm.c +9 -3
- data/ext/cumo/narray/gen/tmpl/rand_norm_kernel.cu +10 -14
- data/ext/cumo/narray/gen/tmpl/rms_norm_kernel.cu +67 -0
- data/ext/cumo/narray/gen/tmpl/seq.c +9 -3
- data/ext/cumo/narray/gen/tmpl/seq_kernel.cu +11 -15
- data/ext/cumo/narray/gen/tmpl/set2.c +13 -4
- data/ext/cumo/narray/gen/tmpl/set2_kernel.cu +9 -15
- data/ext/cumo/narray/gen/tmpl/softmax_kernel.cu +70 -4
- data/ext/cumo/narray/gen/tmpl/sort.c +1 -3
- data/ext/cumo/narray/gen/tmpl/sort_index.c +1 -1
- data/ext/cumo/narray/gen/tmpl/store.c +6 -0
- data/ext/cumo/narray/gen/tmpl/store_bit_kernel.cu +1 -10
- data/ext/cumo/narray/gen/tmpl/store_from_kernel.cu +6 -21
- data/ext/cumo/narray/gen/tmpl/store_numeric.c +9 -5
- data/ext/cumo/narray/gen/tmpl/unary2_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/unary_kernel.cu +40 -12
- data/ext/cumo/narray/gen/tmpl/unary_ret2_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/unary_s_kernel.cu +40 -12
- data/ext/cumo/narray/gen/tmpl_bit/binary_kernel.cu +1 -10
- data/ext/cumo/narray/gen/tmpl_bit/fill_kernel.cu +1 -10
- data/ext/cumo/narray/gen/tmpl_bit/store_bit_kernel.cu +1 -10
- data/ext/cumo/narray/gen/tmpl_bit/store_from_kernel.cu +1 -10
- data/ext/cumo/narray/gen/tmpl_bit/unary_kernel.cu +1 -10
- data/ext/cumo/narray/index.c +2 -2
- data/ext/cumo/narray/narray.c +233 -21
- data/ext/cumo/narray/ndloop.c +9 -3
- data/ext/cumo/narray/ndloop_kernel.cu +87 -89
- data/lib/cumo/cuda/compiler.rb +12 -0
- data/lib/cumo/cuda/elementwise_kernel.rb +23 -10
- data/lib/cumo/cuda/reduction_kernel.rb +47 -16
- data/lib/cumo/cuda/user_kernel.rb +90 -6
- data/lib/cumo/narray/extra.rb +5 -2
- data/test/cuda/elementwise_kernel_test.rb +122 -1
- data/test/cuda/reduction_kernel_test.rb +93 -6
- data/test/cuda/stream_test.rb +1 -0
- data/test/extra_test.rb +46 -0
- data/test/fused_test.rb +24 -3
- data/test/narray_test.rb +633 -7
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d50aabaf6d47d63e97fc6c68726cac94af2ecb8b737a3441de618860d2f5ab6a
|
|
4
|
+
data.tar.gz: 9d7bfc02015586d0ef533802be1d48558abf93fddf362ce445c55b0b4713ab7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 99f47260f8dbbb9320d2f74984262e328fd42c2dac0321300b6087712dd129d03f80d65a2089f12f624ea2814e59eb8400748c9bf08f3937b09c0bceb452f417
|
|
7
|
+
data.tar.gz: a3ccc77b320c0bfdaa64abed339cc22ce5099638f8a52e9abc5a44a1e144295f56581b43ebf99f5a431d54bbe7bed2fe590975abd59cb6b7e1a9008878c91f23
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
# 0.11.0 (2026/09/27)
|
|
2
|
+
|
|
3
|
+
Breaking changes:
|
|
4
|
+
|
|
5
|
+
* `sort`, `sort_index`, `cumsum` and `cumprod` raise `ArgumentError` on `keepdims:`, which they accepted and ignored, since they keep the shape of their input. They take `axis:` and `nan:`, and `nan:` stays on every type as it does for `sum` and `median`. Numo still accepts `keepdims:` here without a word (PR #552)
|
|
6
|
+
* `reverse` raises `ArgumentError` on `keepdims:` and `nan:`, which it accepted and ignored, and takes `axis:` only. Numo still accepts both without a word (PR #551)
|
|
7
|
+
|
|
8
|
+
Fixes:
|
|
9
|
+
|
|
10
|
+
* Fix a store from an overlapping part of the same array, such as `a[0...-k] = a[k..]`, writing wrong values, since threads read elements other threads had already overwritten. With 10 million SFloat elements and a shift of 1024 every run was wrong, on every path and every dtype. The source is now copied first when it shares an element with the destination, which gives numpy's answer (PR #553, PR #542)
|
|
11
|
+
* Fix `ElementwiseKernel` and `ReductionKernel` racing when an input shares memory with an output but is not read element for element, such as a contiguous input shifted against its output. Such an input is copied first (PR #535)
|
|
12
|
+
* Fix a leak of the new shape when `expand_dims` cannot allocate its strides (PR #563)
|
|
13
|
+
|
|
14
|
+
Changes:
|
|
15
|
+
|
|
16
|
+
* `cumsum` and `cumprod` down axis 0, or along a transposed view, gather and scatter their buffer through a shared-memory tile: on a 2048 x 2048 SFloat, `a.cumsum(axis: 0)` spends 133 us on the GPU where it spent 315, and the gather for `a.transpose.cumsum(axis: 1)` takes 51 us where it took 154 (PR #565)
|
|
17
|
+
* An elementwise op on a view built on an index array gathers it into a buffer a typed element at a time rather than with a memcpy of a run-time size: `a[idx, true] + 1` on a 512 x 512 SFloat spends 2.75 us in the gather where it spent 5.2 (PR #564)
|
|
18
|
+
* Say in the API docs of `MemoryPool.used_bytes` and `total_bytes` what each counts and when, and in the README that `used_bytes` waits for the garbage collector where CuPy's `used_bytes()` and PyTorch's `memory_allocated()` do not (PR #562)
|
|
19
|
+
* `ElementwiseKernel` and `ReductionKernel` accept `Cumo::HFloat` and `Cumo::BFloat` as `float16` and `bfloat16`, CUDA's `__half` and `__nv_bfloat16`. A Ruby number handed to one is rounded on the host the way `Cumo::HFloat[x]` and `Cumo::BFloat[x]` round it (PR #561)
|
|
20
|
+
* CI builds against CUDA 13.4.1 and 12.9.2 where it built against 13.2.1, and keeps 12.8.1 (PR #560)
|
|
21
|
+
* `concatenate`, and `hstack`, `vstack` and `dstack` through it, write every part in one kernel launch when every part is a contiguous array of the result's class, and no longer fill the result with zeros first: `hstack` of six SFloat parts of 1500 x 64 takes 5.2 us where it took 18.3 (PR #559)
|
|
22
|
+
* Elementwise ops, bias adds and math functions such as `gelu` move 16 bytes of each operand a thread when the last axis is contiguous in every operand: at 1500 x 384 and 1500 x 1536 SFloat, an add takes 4.18 us where it took 5.67, a bias add 15.1 where it took 27.4, and `gelu` 16.7 where it took 23.7. An HFloat add is 2.3 times faster and an Int8 add 3.6 times. Integer `div`, `mod` and `reciprocal`, and DComplex, keep the old loop (PR #558)
|
|
23
|
+
* Say in the README and in comments why one spelling is faster than another rather than the timings of one GPU, which read as wrong on another card (PR #557, PR #556)
|
|
24
|
+
* `layer_norm` and `rms_norm` hold a row in registers when it divides into sixteen-byte vectors and fits in eight of them a thread, and read it once: `layer_norm` over 1500 x 384 takes 6.7 us where it took 18.5. `layer_norm` takes the variance about the mean in a second pass, which is also more accurate, and a row whose sum overflows near the top of the range stays finite (PR #555)
|
|
25
|
+
* `softmax` holds a row of up to 16 elements a thread in registers and reads it once: 1500 x 1500 takes 18.8 us where it took 34.4. SFloat and DFloat answers move by a few ulp, and HFloat and BFloat come closer to the reference, since `exp` is no longer rounded to half precision before the scale (PR #554)
|
|
26
|
+
* `sort`, `sort_index`, `cumsum`, `cumprod` and `reverse` accept an empty array and answer an empty array of the same shape, where they raised. A missing axis still raises `DimensionError` (PR #550, PR #549)
|
|
27
|
+
* A copy that the wide byte copy cannot take, as reductions, `argmax` and `sort` make of a non-contiguous input, goes a typed element at a time: `argmax` over a column slice runs 1.9 times faster and a copy of a column slice 1.3 times (PR #548)
|
|
28
|
+
* A kernel over a view of 2 to 4 dimensions whose offsets fit an int32 does its address arithmetic in 32 bits: a store with a cast, `abs` or `clip` on such a view runs 1.17 to 1.32 times faster (PR #547)
|
|
29
|
+
* A DFloat `logseq` of base 10 or 2 calls `exp10` or `exp2` rather than `pow`: 200,000 elements take 20 us where they took 84 (PR #546)
|
|
30
|
+
* The rank switch of every generated kernel launcher comes from one helper in the kernel generator, and the generated sources are token for token what they were (PR #545)
|
|
31
|
+
* `logseq` writes a strided, index or transposed view in one launch rather than one per row: a 6250 x 32 DFloat view takes 87 us where it took 14 ms (PR #544)
|
|
32
|
+
* A reduction along a last axis of 16 elements or fewer gives a row that spans more than one 32-byte sector two threads: `Cumo::SFloat.ones(1536, 64)[true, 0...16].sum(axis: 1)` takes 2.18 us where it took 4.20, and `max`, `mean`, `mulsum` and `argmax` over short rows run 1.2 to 1.8 times faster (PR #543)
|
|
33
|
+
* A store between views of the same type moves 16, 8, 4 or 2 bytes a thread over a run contiguous and aligned on both sides, through one set of kernels the copy behind `dup` shares: the store of an im2col unfold runs about 2 times faster (PR #541)
|
|
34
|
+
* `cumsum` and `cumprod` no longer wait for the GPU. A 1-D scan shorter than 8192 elements ran on the host after a device synchronize, and every other shape waited for the stream to drain: `x = a * 1; x.cumsum + 1` takes about 9 us where it took 0.3 to 0.8 ms (PR #540)
|
|
35
|
+
* `rand`, `rand_norm` and `seq` write a view built on an index array directly rather than through a buffer copied out and back, and so do `real=` and `imag=`: `rand` into a `[6250, 1024][true, idx64]` SFloat view takes 12.7 us where it took 80.7. An axis of length one merges with its neighbour, so a `[N, 1]` column runs what a flat array runs. A seed draws the same values as before (PR #539)
|
|
36
|
+
* `ElementwiseKernel` and `ReductionKernel` read a reversed or stepped view, or a frozen array, where it is rather than copying it on every call, and `ReductionKernel` reads a transposed view the same way: a reduction along the first axis of a transposed 4000 by 4000 array no longer pays a copy that took longer than the reduction. An `ElementwiseKernel` input that is transposed is still copied, in tiles, which beats reading it across rows (PR #538, PR #535)
|
|
37
|
+
* `ReductionKernel` over a leading axis gives a block up to 32 outputs that lie end to end, so a warp reads neighbouring outputs: a sum over axis 0 of a [4000, 4000] SFloat takes 0.169 ms where it took 0.413, what Cumo's own `sum` takes (PR #537)
|
|
38
|
+
* `ElementwiseKernel` and `ReductionKernel` walk neighbouring dimensions that every array lays out end to end as one: a `ReductionKernel` sum over everything of a [160, 100, 1000] SFloat takes 0.162 ms where it took 0.193 (PR #536)
|
|
39
|
+
* Fix a stream test that failed about once in a thousand runs, since its fill on a non-blocking stream was not ordered before its launch on the null stream (PR #534)
|
|
40
|
+
* Say in the README where `CUMO_ALLOW_TF32` pays and where it does not: it speeds up matrix products and convolutions, buys nothing where reading the weights takes the time, and its rounding can push a training step past a tolerance single precision meets (PR #533)
|
|
41
|
+
* A number stored with `a[...] = x`, `a[] = x` or `store(x)` goes through `fill`, one kernel launch with nothing reserved, where it filled a 0-dimensional array first. `fill` writes a view built on an index array or a mask directly rather than through a copy of the whole view (PR #532)
|
|
42
|
+
|
|
1
43
|
# 0.10.0 (2026/09/22)
|
|
2
44
|
|
|
3
45
|
Breaking changes:
|
data/README.md
CHANGED
|
@@ -185,21 +185,11 @@ The 0-dimensional return is what lets an iterative loop stay on the GPU.
|
|
|
185
185
|
Reading a scalar back to the host waits for everything queued behind it, so every read caps how far ahead the GPU is allowed to run.
|
|
186
186
|
What a read costs is not a fixed price either: it is however much work happens to be queued when it is taken.
|
|
187
187
|
|
|
188
|
-
`bench/cg_bench.rb`
|
|
189
|
-
|
|
190
|
-
```
|
|
191
|
-
scalars convergence test us/iter readbacks/iter
|
|
192
|
-
Ruby Floats every iteration 136.7 2.02
|
|
193
|
-
Ruby Floats never 134.4 2.02
|
|
194
|
-
0-dim NArray every iteration 142.6 1.02
|
|
195
|
-
0-dim NArray every 20th 65.6 0.06
|
|
196
|
-
0-dim NArray never 60.3 0.02
|
|
197
|
-
```
|
|
198
|
-
|
|
188
|
+
`bench/cg_bench.rb` measures this with a conjugate gradient solve over a 512x512 grid, with the scalars kept either way and the convergence test taken every iteration, every twentieth or never.
|
|
199
189
|
Written with Ruby Floats the loop reads back twice an iteration whatever the convergence test does, since `alpha` needs `pap` and `beta` needs `rs_new` as Floats.
|
|
200
|
-
Thinning the test cannot get under that floor, and keeping the scalars as 0-dimensional NArrays buys nothing on its own.
|
|
201
|
-
The two only pay together, and together they
|
|
202
|
-
The relative residual is identical
|
|
190
|
+
Thinning the test cannot get under that floor, and keeping the scalars as 0-dimensional NArrays buys nothing on its own, since the test still reads back every iteration.
|
|
191
|
+
The two only pay together, and together they let the loop run well ahead of the host.
|
|
192
|
+
The relative residual is identical either way.
|
|
203
193
|
|
|
204
194
|
```ruby
|
|
205
195
|
alpha = rs_old / pap # a 0-dimensional NArray, divided on the device
|
|
@@ -208,8 +198,8 @@ x += p_dir * alpha # and consumed there, without crossing the bus
|
|
|
208
198
|
|
|
209
199
|
Read the value back once the loop is done, or every k iterations if it has to test something.
|
|
210
200
|
|
|
211
|
-
A read costs the wait and a copy of the block it needs into pinned host memory
|
|
212
|
-
Reading managed memory from the host directly would fault its page over instead, and a small block shares a page with other live blocks that the next kernel touches, so a fresh scalar
|
|
201
|
+
A read costs the wait and a copy of the block it needs into pinned host memory.
|
|
202
|
+
Reading managed memory from the host directly would fault its page over instead, and a small block shares a page with other live blocks that the next kernel touches, so a fresh scalar costs far more that way than the copy does.
|
|
213
203
|
The reads that answer values, from `Float(x)` and `to_a` to `each` and `inspect`, take the copy.
|
|
214
204
|
|
|
215
205
|
### Reshape Copies, Reshape! Does Not
|
|
@@ -226,18 +216,7 @@ a.reshape(2, 12)[0, 0] = 99.0 # a copy, so a does not
|
|
|
226
216
|
This is what Numo does too, and numpy is where the expectation comes from: there `reshape` answers a view whenever the strides allow one.
|
|
227
217
|
On a GPU the difference is an allocation and a copy kernel, paid every call.
|
|
228
218
|
`reshape!` changes the receiver in place and costs neither.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
```
|
|
232
|
-
shape reshape reshape! the copy allocates
|
|
233
|
-
1x768 5.1 us 0.04 us 3 KB
|
|
234
|
-
1024x768 39.1 us 0.18 us 3 MB
|
|
235
|
-
4096x768 58.1 us 0.22 us 12 MB
|
|
236
|
-
8192x768 142.7 us 0.43 us 24 MB
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
`reshape!` is host-side bookkeeping, so it stays under a microsecond whatever the array weighs.
|
|
240
|
-
The `reshape` column is the copy, and it grows with the bytes.
|
|
219
|
+
It is host-side bookkeeping, so its cost does not depend on what the array weighs, where the copy `reshape` makes grows with the bytes.
|
|
241
220
|
|
|
242
221
|
The catch is that `reshape!` changes the array everything else is holding.
|
|
243
222
|
It fits a temporary the calling expression owns, and not an argument, an ivar, or anything a cache still points at:
|
|
@@ -262,28 +241,17 @@ a + 2.0 #=> Cumo::SFloat
|
|
|
262
241
|
```
|
|
263
242
|
|
|
264
243
|
Numo promotes the same way, and on a CPU it costs nothing: Numo's single-precision math computes in double and narrows the result anyway.
|
|
265
|
-
On a GeForce card, whose double-precision rate is a
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
```
|
|
269
|
-
SFloat DFloat
|
|
270
|
-
a * 2.0 11.8 us 12.8 us
|
|
271
|
-
sqrt 11.0 us 43.0 us
|
|
272
|
-
sin 11.5 us 97.5 us
|
|
273
|
-
atan 11.1 us 122.9 us
|
|
274
|
-
atan2 12.5 us 192.4 us
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
Only the transcendentals pay for the promotion; a double multiply runs at the speed of a single one.
|
|
244
|
+
On a GeForce card, whose double-precision rate is a small fraction of its single-precision one, it costs a great deal.
|
|
245
|
+
Only the transcendentals pay for the promotion, and the more work the function does the more they pay: a double multiply runs at about the speed of a single one, while `sqrt`, `sin`, `atan` and `atan2` in double take several times as long as in single.
|
|
278
246
|
The methods a Float can reach as a second argument are `atan2`, `hypot` and `ldexp`.
|
|
279
|
-
`ldexp` pays a different way, since scaling by a power of two is cheap in either precision: `Cumo::NMath.ldexp(a, 2.0)`
|
|
247
|
+
`ldexp` pays a different way, since scaling by a power of two is cheap in either precision: `Cumo::NMath.ldexp(a, 2.0)` is still far slower than `Cumo::NMath.ldexp(a, 2)`, and the difference there is the doubled arrays it has to allocate rather than the arithmetic.
|
|
280
248
|
|
|
281
249
|
Pass a 0-dimensional array instead of a Float and the call stays single precision.
|
|
282
250
|
That is what `[]` hands back, so a scalar taken out of an array is already in the right form:
|
|
283
251
|
|
|
284
252
|
```ruby
|
|
285
253
|
two = Cumo::SFloat[2.0][0] # a 0-dimensional Cumo::SFloat
|
|
286
|
-
Cumo::NMath.atan2(a, two) #=> Cumo::SFloat
|
|
254
|
+
Cumo::NMath.atan2(a, two) #=> Cumo::SFloat
|
|
287
255
|
```
|
|
288
256
|
|
|
289
257
|
Naming the module directly works too, under both libraries:
|
|
@@ -300,22 +268,9 @@ The 0-dimensional form has no effect under Numo, where `[]` returns a Ruby Float
|
|
|
300
268
|
That saves the copy and the memory it needs.
|
|
301
269
|
What it costs is the kernel cuBLAS then picks, which for most shapes is slower than the one it picks for an operand already laid out its way.
|
|
302
270
|
|
|
303
|
-
RTX 5070 Ti Laptop, `Cumo::SFloat`, `q[M,K].dot(k[N,K].transpose)`, medians of nine rounds:
|
|
304
|
-
|
|
305
|
-
```
|
|
306
|
-
M K N M*K as it is copied first the copy
|
|
307
|
-
1 64 1500 64 5.6 us 11.4 us 2.8 us
|
|
308
|
-
512 64 512 32,768 8.5 11.2 2.9
|
|
309
|
-
1500 64 1500 96,000 33.0 29.3 3.0
|
|
310
|
-
512 256 512 131,072 21.8 20.7 2.8
|
|
311
|
-
4096 64 4096 262,144 229.1 179.3 4.2
|
|
312
|
-
512 768 512 393,216 54.0 39.2 3.3
|
|
313
|
-
256 3072 768 786,432 131.1 103.0 12.5
|
|
314
|
-
```
|
|
315
|
-
|
|
316
271
|
The copy weighs `N * K`, and the faster kernel it buys is worth `M * N * K`, so `M` is what decides.
|
|
317
|
-
A matrix-vector product, where `M` is one, is the clearest case against copying
|
|
318
|
-
|
|
272
|
+
A matrix-vector product, where `M` is one, is the clearest case against copying.
|
|
273
|
+
As `M * K` grows the copy starts paying for itself; where it starts depends on the card, so measure the shape that matters before choosing.
|
|
319
274
|
|
|
320
275
|
Where a profile says one of these multiplications matters, hand it an operand that is already contiguous:
|
|
321
276
|
|
|
@@ -324,13 +279,13 @@ kt = k.transpose.dup # or build k transposed in the first place
|
|
|
324
279
|
q.dot(kt)
|
|
325
280
|
```
|
|
326
281
|
|
|
327
|
-
|
|
328
|
-
A batched multiplication takes another path through the same flag
|
|
282
|
+
All of this is about the two-dimensional case, where cuBLAS is given one matrix.
|
|
283
|
+
A batched multiplication takes another path through the same flag.
|
|
329
284
|
|
|
330
285
|
### Fused Operations
|
|
331
286
|
|
|
332
287
|
`layer_norm`, `rms_norm` and `softmax` normalize along the last axis in one kernel each, and `quantize_symmetric` takes it to 8-bit integers in one more.
|
|
333
|
-
Written out of the operators they take nine launches, six, five and six, and a launch
|
|
288
|
+
Written out of the operators they take nine launches, six, five and six, and a launch has a fixed cost whatever it is handed, so a short row pays for the launches rather than for its bytes.
|
|
334
289
|
|
|
335
290
|
```ruby
|
|
336
291
|
y = x.layer_norm(gamma, beta, eps: 1e-5) # (x - mean) / sqrt(var + eps) * gamma + beta
|
|
@@ -351,47 +306,8 @@ numpy, torch -2 -2 0 0 2 2
|
|
|
351
306
|
|
|
352
307
|
`rint` is the other rule, at the cost of writing the quantization out: `(x / scale[false, :new]).rint.clip(-127, 127)` answers what numpy answers. A tie needs `x` to be an exact odd multiple of half the scale, so whether one ever comes up is a property of the data rather than of the arithmetic. Over a million random single-precision elements the two spellings disagreed four times, and every disagreement was a tie.
|
|
353
308
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
```
|
|
357
|
-
layer_norm SFloat DFloat HFloat
|
|
358
|
-
shape fused written fused written fused written
|
|
359
|
-
1 x 768 3.9 30.1 9.7 30.3 3.9 29.8
|
|
360
|
-
256 x 768 6.6 22.5 22.0 35.4 7.8 22.4
|
|
361
|
-
4096 x 768 32.7 177.4 300.7 610.5 28.2 142.9
|
|
362
|
-
1 x 1000000 19.0 59.4 125.4 175.4 22.3 68.5
|
|
363
|
-
|
|
364
|
-
softmax SFloat DFloat HFloat
|
|
365
|
-
shape fused written fused written fused written
|
|
366
|
-
1 x 768 4.3 19.8 7.4 26.5 6.3 34.4
|
|
367
|
-
256 x 768 8.8 13.9 23.4 35.1 6.8 18.5
|
|
368
|
-
4096 x 768 49.2 122.7 318.3 549.0 28.2 109.8
|
|
369
|
-
1 x 1000000 27.8 44.0 108.6 148.5 32.1 53.8
|
|
370
|
-
|
|
371
|
-
rms_norm SFloat DFloat HFloat
|
|
372
|
-
shape fused written fused written fused written
|
|
373
|
-
1 x 768 2.9 14.2 5.8 14.1 3.1 15.1
|
|
374
|
-
256 x 768 4.6 16.7 6.9 20.2 5.2 15.5
|
|
375
|
-
4096 x 768 38.5 122.5 130.8 367.0 19.2 80.8
|
|
376
|
-
1 x 1000000 12.5 35.8 23.7 102.2 12.0 33.2
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
```
|
|
380
|
-
quantize_symmetric SFloat DFloat HFloat
|
|
381
|
-
shape fused written fused written fused written
|
|
382
|
-
1 x 768 3.7 20.2 6.0 15.7 2.3 11.9
|
|
383
|
-
256 x 768 4.7 15.0 20.2 21.3 4.7 13.0
|
|
384
|
-
4096 x 768 24.4 105.4 289.5 333.3 23.8 80.3
|
|
385
|
-
1 x 1000000 18.8 40.8 97.1 81.6 15.7 37.5
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
The tables were taken in separate sessions, so read each row against the row beside it and not across the tables.
|
|
389
|
-
|
|
390
|
-
`quantize_symmetric` in `Cumo::DFloat` is worth less than the others and loses outright on a million elements, whatever shape they are in. The division it does per element is what costs: on this card one row of a million takes 44.0 us to divide in double against 10.8 in single, where the reduction over the same row takes 25.3 and 15.4. Six kernels give that division a kernel of its own to fill the device with, and one kernel leaves it behind the reduction.
|
|
391
|
-
|
|
392
|
-
All three pay off in every precision, by the most where the row is short enough that the launches were all it was doing, and by the least in double, where the reduction itself costs more than the launches ever did.
|
|
393
|
-
The memory clock on this card steps between 9001 and 11001 MHz under a benchmark this short, and the absolute figures move with it.
|
|
394
|
-
The ratios hold across the steps.
|
|
309
|
+
Against the same arithmetic spelled with operators they are faster, by the most where the row is short enough that the launches were all it was doing, and by the least in double, where the arithmetic itself costs more than the launches ever did.
|
|
310
|
+
The exception is `quantize_symmetric` in `Cumo::DFloat`, which can lose on a long row. The division it does per element is what costs, since double is slow on a card with a low double-precision rate: six kernels give that division a kernel of its own to fill the device with, and one kernel leaves it behind the reduction.
|
|
395
311
|
|
|
396
312
|
All three normalize along the last axis only.
|
|
397
313
|
Other axes are reachable through `transpose`.
|
|
@@ -447,17 +363,7 @@ It is also called Swish.
|
|
|
447
363
|
Cumo::NMath.silu(x) # x / (1 + exp(-x)), in one kernel rather than five
|
|
448
364
|
```
|
|
449
365
|
|
|
450
|
-
Written out of the operators it costs five launches, and one kernel
|
|
451
|
-
|
|
452
|
-
```
|
|
453
|
-
silu SFloat DFloat HFloat
|
|
454
|
-
elements fused written fused written fused written
|
|
455
|
-
768 3.6 13.4 3.6 9.5 3.4 14.3
|
|
456
|
-
786432 7.0 25.9 90.5 102.4 5.2 19.2
|
|
457
|
-
16777216 353.2 1312.9 1984.7 3258.8 199.6 851.8
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
The three rows are three regimes rather than one curve: 768 elements pay for the launches, 786432 fit in L2 and move faster than this card reads from memory, and 16777216 are what it costs from DRAM.
|
|
366
|
+
Written out of the operators it costs five launches, and one kernel is faster in every precision, by the most on an array small enough that the launches were all it was doing.
|
|
461
367
|
|
|
462
368
|
It follows `torch.nn.functional.silu`, answering within one unit in the last place of the true value at the nine double points measured, as torch does.
|
|
463
369
|
At the edges it matches torch exactly: `-Float::INFINITY` answers `NaN`, since that is what infinity times zero is, and a large negative `x` answers a signed zero.
|
|
@@ -474,15 +380,7 @@ Half reaches it at -21, because the round back to half gets there first.
|
|
|
474
380
|
Cumo::NMath.sigmoid(x) # in one kernel rather than four
|
|
475
381
|
```
|
|
476
382
|
|
|
477
|
-
Written out of the operators it costs four launches, and one kernel
|
|
478
|
-
|
|
479
|
-
```
|
|
480
|
-
sigmoid SFloat DFloat HFloat
|
|
481
|
-
elements fused written fused written fused written
|
|
482
|
-
768 2.9 9.0 2.5 8.8 2.0 9.6
|
|
483
|
-
786432 5.6 23.0 94.6 111.3 7.3 23.6
|
|
484
|
-
16777216 343.5 1234.7 1978.5 2941.1 210.5 798.1
|
|
485
|
-
```
|
|
383
|
+
Written out of the operators it costs four launches, and one kernel is faster in every precision, by the most on an array small enough that the launches were all it was doing.
|
|
486
384
|
|
|
487
385
|
The kernel keeps the exponent's argument negative, which the plain quotient does not: writing `1 / (1 + exp(-x))` out asks `exp` for a value it cannot hold once `x` is negative enough, and the quotient then answers a zero where the curve is still a number the type carries.
|
|
488
386
|
|
|
@@ -513,17 +411,8 @@ A non-negative `x` takes the plain quotient unchanged, bit for bit, so only the
|
|
|
513
411
|
Cumo::NMath.softplus(x) # log(1 + exp(x)), in one kernel rather than three
|
|
514
412
|
```
|
|
515
413
|
|
|
516
|
-
Written out of the operators it costs three launches, and one kernel
|
|
517
|
-
|
|
518
|
-
```
|
|
519
|
-
softplus SFloat DFloat HFloat
|
|
520
|
-
elements fused written fused written fused written
|
|
521
|
-
768 2.0 6.1 2.3 6.2 2.1 6.0
|
|
522
|
-
786432 5.9 18.3 166.6 171.8 5.7 15.0
|
|
523
|
-
16777216 344.2 939.5 3503.7 3938.2 206.2 591.2
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
Double barely moves at the two larger sizes, the arithmetic rather than the launches being what it pays for there.
|
|
414
|
+
Written out of the operators it costs three launches, and one kernel is faster, by the most on an array small enough that the launches were all it was doing.
|
|
415
|
+
Double gains little on a large array, the arithmetic rather than the launches being what it pays for there.
|
|
527
416
|
|
|
528
417
|
Writing it out also gives out earlier than the kernel does, because the intermediate is an array of the receiver's type:
|
|
529
418
|
|
|
@@ -603,54 +492,22 @@ a.gemm(b, alpha: 0.001) #=> 102.375, the true 102400 scaled down
|
|
|
603
492
|
|
|
604
493
|
#### What half is faster at
|
|
605
494
|
|
|
606
|
-
`gemm` reaches the tensor cores
|
|
607
|
-
|
|
608
|
-
```
|
|
609
|
-
HFloat SFloat DFloat
|
|
610
|
-
1024x1024 0.043 ms 49.7 TF 0.161 ms 13.3 TF 5.370 ms 0.40 TF
|
|
611
|
-
2048x2048 0.362 ms 47.4 TF 1.360 ms 12.6 TF 43.46 ms 0.40 TF
|
|
612
|
-
4096x4096 3.335 ms 41.2 TF 10.40 ms 13.2 TF 328.1 ms 0.42 TF
|
|
613
|
-
```
|
|
495
|
+
`gemm` reaches the tensor cores, which single and double precision do not, so a large matrix product runs several times faster in half than in single.
|
|
614
496
|
|
|
615
497
|
An odd number of columns costs half far more than it costs the others, because a row then starts on a two-byte boundary and the vectorized path is gone.
|
|
616
|
-
It is the column count of either operand that matters, not the row count.
|
|
617
|
-
1024x1024 times 1024x1024, with one dimension made odd at a time:
|
|
618
|
-
|
|
619
|
-
```
|
|
620
|
-
all even M odd K odd N odd
|
|
621
|
-
HFloat 51.8 TF 49.2 TF 23.3 TF 23.3 TF
|
|
622
|
-
SFloat 11.9 TF - 11.6 TF 10.8 TF
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
The run-to-run spread on these is a few per cent and reaches fifteen at the top end, so the M column says the row count does not matter rather than that it costs 5 per cent.
|
|
498
|
+
It is the column count of either operand that matters, not the row count: making `K` or `N` odd loses a large share of the speed, where making `M` odd does not.
|
|
626
499
|
|
|
627
500
|
The penalty is on the arithmetic, so it does not reach a matrix-vector product, which is bound by how fast the matrix can be read whatever its shape.
|
|
628
|
-
A 1x768 by 768x50257 gemv
|
|
501
|
+
A 1x768 by 768x50257 gemv runs as fast with that odd 50257 as with 50256, while the same matrix against 256 rows does not.
|
|
629
502
|
Pad the inner dimensions of a real matrix product; leave a gemv alone.
|
|
630
503
|
|
|
631
504
|
`conv` is a different story, and worth reading before reaching for half in a network.
|
|
632
505
|
cuDNN chooses its algorithm from the ones that fit in a scratch buffer, and the half algorithms that use the tensor cores ask for a lot of it.
|
|
633
|
-
The default ceiling reaches them; the 8MB one Cumo used to ship does not.
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
```
|
|
637
|
-
HFloat BFloat SFloat
|
|
638
|
-
CUMO_CUDNN_MAX_WORKSPACE_SIZE=8388608 1.27 ms 1.26 ms 1.19 ms
|
|
639
|
-
unset (128MB) 0.54 ms 0.60 ms 1.19 ms
|
|
640
|
-
CUMO_CUDNN_MAX_WORKSPACE_SIZE=268435456 0.52 ms 0.59 ms 0.59 ms
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
This shape's single-precision algorithm wants more than the default, which is the other half of the reason to look at the ceiling for a network that spends its time in `conv`.
|
|
506
|
+
The default ceiling reaches them; the 8MB one Cumo used to ship does not, and under it a half convolution such as N=32, C=K=64, 56x56, 3x3 runs no faster than single.
|
|
507
|
+
The single-precision algorithm for that shape wants more than the default, which is the other half of the reason to look at the ceiling for a network that spends its time in `conv`.
|
|
644
508
|
|
|
645
509
|
Tensor cores also want the channel counts to be multiples of eight, which the first layer of a network never satisfies.
|
|
646
|
-
That layer
|
|
647
|
-
|
|
648
|
-
```
|
|
649
|
-
C=3, K=64, 56x56, 3x3 0.085 ms 0.170 ms
|
|
650
|
-
C=K=64, 56x56, 1x1 0.037 ms 0.120 ms
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
Neither of those reaches a tensor core; they move half the bytes.
|
|
510
|
+
That layer can still be faster in half, but for the other reason: it moves half the bytes rather than reaching a tensor core.
|
|
654
511
|
|
|
655
512
|
#### Batch normalization takes single-precision parameters
|
|
656
513
|
|
|
@@ -713,23 +570,8 @@ What replaces it is the mantissa: a bfloat16 resolves about three decimal digits
|
|
|
713
570
|
|
|
714
571
|
#### What bfloat16 is faster at
|
|
715
572
|
|
|
716
|
-
`gemm` reaches the tensor cores and
|
|
717
|
-
|
|
718
|
-
```
|
|
719
|
-
BFloat HFloat SFloat
|
|
720
|
-
1024x1024 0.044 ms 48.9 TF 0.044 ms 48.5 TF 0.144 ms 14.9 TF
|
|
721
|
-
2048x2048 0.298 ms 57.6 TF 0.303 ms 56.7 TF 1.404 ms 12.2 TF
|
|
722
|
-
4096x4096 2.839 ms 48.4 TF 2.898 ms 47.4 TF 9.540 ms 14.4 TF
|
|
723
|
-
```
|
|
724
|
-
|
|
573
|
+
`gemm` reaches the tensor cores and runs at the same speed as half.
|
|
725
574
|
The odd-column penalty is the same as half's and for the same reason, a row starting on a two-byte boundary.
|
|
726
|
-
1024x1024 times 1024x1024, with one dimension made odd at a time:
|
|
727
|
-
|
|
728
|
-
```
|
|
729
|
-
all even M odd K odd N odd
|
|
730
|
-
BFloat 48.9 TF 48.7 TF 23.5 TF 23.0 TF
|
|
731
|
-
HFloat 48.5 TF - 24.0 TF 23.1 TF
|
|
732
|
-
```
|
|
733
575
|
|
|
734
576
|
So the choice between the two sixteen-bit types is about range and resolution, not speed.
|
|
735
577
|
Take bfloat16 where the magnitudes came from somewhere else and binary16 where three more mantissa bits are worth having.
|
|
@@ -739,7 +581,7 @@ Take bfloat16 where the magnitudes came from somewhere else and binary16 where t
|
|
|
739
581
|
cuBLAS supports `CUDA_R_16BF`, which is what `gemm` and `dot` reach, from compute capability 8.0.
|
|
740
582
|
Storing, casting and elementwise arithmetic have no such floor, because every operation is computed in single precision and rounded back, and they build and run wherever cumo does.
|
|
741
583
|
A `dot` on a pre-Ampere card is the one to expect trouble from.
|
|
742
|
-
**This is not measured here**: the
|
|
584
|
+
**This is not measured here**: the requirement is read from cuBLAS's documentation rather than reproduced on a card older than Ampere.
|
|
743
585
|
|
|
744
586
|
cuDNN reaches bfloat16 as `CUDNN_DATA_BFLOAT16`, and its own bfloat16 kernels want Ampere for the same reason cuBLAS does.
|
|
745
587
|
A convolution is given `CUDNN_DATA_FLOAT` to accumulate in, so it passes 256 the way a reduction does.
|
|
@@ -879,13 +721,19 @@ squared_diff.call(x, y) # => the (2, 5) array of squared differences
|
|
|
879
721
|
squared_diff.call(x, 5) # => the same against a scalar
|
|
880
722
|
```
|
|
881
723
|
|
|
882
|
-
A type is one of `float64`, `float32`, `int64`, `int32`, `int16`, `int8`, `uint64`, `uint32`, `uint16` and `uint8`, or a single letter that stands for whichever dtype the argument has.
|
|
724
|
+
A type is one of `float64`, `float32`, `float16`, `bfloat16`, `int64`, `int32`, `int16`, `int8`, `uint64`, `uint32`, `uint16` and `uint8`, or a single letter that stands for whichever dtype the argument has.
|
|
883
725
|
Outputs decide a letter before inputs do, and a letter that only a Ruby number reaches becomes `int64` or `float64`.
|
|
884
726
|
Array arguments are broadcast against each other, and an output may be given after the inputs, or is allocated.
|
|
727
|
+
An input is read through its own strides, so a reversed or stepped view goes to the kernel as it is.
|
|
728
|
+
A transposed view is copied first, since the copy reorders it in tiles where the kernel would read it a row apart per thread, and so is a view built on an index array, or an input that shares memory with an output other than element for element, since the kernel would read what another thread has already written; a raw argument has to be contiguous and is copied when it is not.
|
|
885
729
|
An argument marked `raw T y` is handed over as a pointer for the operation to index itself, with `i` the element index and `_ind.size()` the element count, and when every argument is raw or a number, `size:` says how many elements there are.
|
|
886
730
|
Inputs are `const`, so an operation that writes one does not compile, and a number handed to an integer type has to be an Integer that fits.
|
|
887
731
|
`preamble:` is placed before the kernel, after the typedefs of the letters, so a device function can be written in terms of `T`.
|
|
888
|
-
`
|
|
732
|
+
`float16` and `bfloat16` are CUDA's `__half` and `__nv_bfloat16`, compiled with the toolkit's `cuda_fp16.h` and `cuda_bf16.h`, which are found through `CUDA_PATH` or the `nvcc` on `PATH`.
|
|
733
|
+
They convert to and from every arithmetic type, so an operation that mixes one with another type or a literal does not compile until one side is cast: `y = x * T(2)`, `y = x > T(0) ? x : T(0)` or `y = (float)x * 2`.
|
|
734
|
+
With CUDA 11, arithmetic on `float16` does not compile for a GPU below compute capability 5.3, which has none.
|
|
735
|
+
A number handed to one is rounded to it the way `Cumo::HFloat[x]` and `Cumo::BFloat[x]` round it.
|
|
736
|
+
`Cumo::Bit`, `Cumo::SComplex`, `Cumo::DComplex` and `Cumo::RObject` cannot be handed to one of these kernels yet.
|
|
889
737
|
|
|
890
738
|
### Writing A Reduction Kernel
|
|
891
739
|
|
|
@@ -940,7 +788,8 @@ Cumo::CUDA::MemoryPool.total_bytes # what the pool has taken from the card
|
|
|
940
788
|
Cumo::CUDA::MemoryPool.used_bytes # what it has handed out and not taken back
|
|
941
789
|
```
|
|
942
790
|
|
|
943
|
-
`used_bytes` still counts blocks whose last reference is gone but which Ruby's garbage collector has not reached, so read it after `GC.start` when you mean the arrays that are alive
|
|
791
|
+
`used_bytes` still counts blocks whose last reference is gone but which Ruby's garbage collector has not reached, so read it after `GC.start` when you mean the arrays that are alive.
|
|
792
|
+
Python frees an array when its last reference goes, so CuPy's `used_bytes()` and PyTorch's `memory_allocated()` drop at once where this waits for the collector:
|
|
944
793
|
|
|
945
794
|
```ruby
|
|
946
795
|
a = Cumo::SFloat.new(1024, 1024).seq
|
|
@@ -1084,14 +933,8 @@ export CUMO_SHOW_WARNING_ONCE=OFF
|
|
|
1084
933
|
cuDNN picks a convolution algorithm by benchmarking the ones that fit in a scratch buffer, and the ceiling on that buffer is 128MB.
|
|
1085
934
|
The search reserves the whole ceiling whatever the convolution's size and hands it back to the pool afterwards, so the ceiling costs a peak rather than a residency.
|
|
1086
935
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
```
|
|
1090
|
-
ResNet-18 N=32, C=K=64, 56x56, 3x3
|
|
1091
|
-
CUMO_CUDNN_MAX_WORKSPACE_SIZE=8388608 9.80 ms 1.19 ms
|
|
1092
|
-
unset (128MB) 5.23 ms 1.19 ms
|
|
1093
|
-
CUMO_CUDNN_MAX_WORKSPACE_SIZE=268435456 5.24 ms 0.59 ms
|
|
1094
|
-
```
|
|
936
|
+
The old 8MB ceiling kept the twenty convolutions of a ResNet-18 forward pass at batch 16 off their faster algorithms, which the default reaches.
|
|
937
|
+
Some shapes want more than the default, such as the single-precision case from [Half Precision](#half-precision), which reaches its faster algorithm only above it.
|
|
1095
938
|
|
|
1096
939
|
To raise it:
|
|
1097
940
|
|
|
@@ -1116,20 +959,18 @@ To trade the accuracy for the speed:
|
|
|
1116
959
|
export CUMO_ALLOW_TF32=1
|
|
1117
960
|
```
|
|
1118
961
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
```
|
|
1122
|
-
pass worst layer
|
|
1123
|
-
tensor cores off (default) 5.24 ms 1.4e-05
|
|
1124
|
-
tensor cores on 4.31 ms 2.4e-04
|
|
1125
|
-
```
|
|
962
|
+
Over the convolutions of a ResNet-18 forward pass at batch 16, with the ceiling raised to 256MB, the pass gets faster and the worst layer moves from 1.4e-05 to 2.4e-04 against a double precision reference.
|
|
1126
963
|
|
|
1127
964
|
The same flag puts `SFloat` and `SComplex` `gemm` on the tensor cores as TF32, and `dot` where it goes through `gemm`.
|
|
1128
965
|
Off, the answer is the one cuBLAS gives at single precision, bit for bit.
|
|
1129
|
-
On, a
|
|
1130
|
-
Its answer is then about 3e-04 from a double precision reference, where it was 4e-07.
|
|
966
|
+
On, a large `SFloat` `gemm` runs faster, and its answer is then about 3e-04 from a double precision reference, where it was 4e-07.
|
|
1131
967
|
The double types are not affected either way.
|
|
1132
968
|
|
|
969
|
+
What the flag buys depends on where the time goes.
|
|
970
|
+
It pays where matrix products and convolutions take the time, as in training or in a batch through a convolutional network.
|
|
971
|
+
It buys nothing where reading the operands takes the time, as in decoding one token at a time, where each step reads every weight once and the GEMM waits on memory rather than on arithmetic.
|
|
972
|
+
Over a training run the rounding adds up, so a loss that meets a tolerance at single precision can miss it on some steps; compare a few steps with the flag off before relying on it.
|
|
973
|
+
|
|
1133
974
|
`Cumo.allow_tf32?` reads back the value in force.
|
|
1134
975
|
|
|
1135
976
|
## Contributing
|
|
@@ -654,9 +654,8 @@ cumo_cuda_cudnn_GetBatchNormMode(size_t ndim, int* axis) {
|
|
|
654
654
|
if ((ndim == 3 && axis[0] == 0 && axis[1] == 2 && axis[2] == 3) ||
|
|
655
655
|
(ndim == 4 && axis[0] == 0 && axis[1] == 2 && axis[2] == 3 && axis[3] == 4)) { // (1, channels, (1, )1, 1)
|
|
656
656
|
// CUDNN_BATCHNORM_SPATIAL_PERSISTENT is not worth taking. The overflow
|
|
657
|
-
// check it asks for is
|
|
658
|
-
//
|
|
659
|
-
// spatial extent is large, and 1.6x slower on 32x256x14x14 backward.
|
|
657
|
+
// check it asks for is cheap, but the mode is faster only where the
|
|
658
|
+
// spatial extent is large, and slower on 32x256x14x14 backward.
|
|
660
659
|
// Worse, on 32x64x56x56 it raises CUDNN_STATUS_RUNTIME_FP_OVERFLOW for x
|
|
661
660
|
// in [-2, 2] with gamma 1 and beta 0, answering bit for bit what SPATIAL
|
|
662
661
|
// does; honouring that means running SPATIAL again on top. The mode, and
|
data/ext/cumo/cuda/driver.c
CHANGED
|
@@ -458,18 +458,73 @@ typedef union {
|
|
|
458
458
|
} kernel_arg_t;
|
|
459
459
|
|
|
460
460
|
static int
|
|
461
|
-
|
|
461
|
+
kernel_arg_is_typed_narray(VALUE v)
|
|
462
462
|
{
|
|
463
463
|
if (!rb_obj_is_kind_of(v, cumo_cNArray)) return 0;
|
|
464
464
|
if (rb_obj_is_kind_of(v, cumo_cBit) || rb_obj_is_kind_of(v, cumo_cRObject)) {
|
|
465
465
|
rb_raise(rb_eTypeError, "a %s cannot be handed to a kernel", rb_obj_classname(v));
|
|
466
466
|
}
|
|
467
|
+
return 1;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
static int
|
|
471
|
+
kernel_arg_is_narray(VALUE v)
|
|
472
|
+
{
|
|
473
|
+
if (!kernel_arg_is_typed_narray(v)) return 0;
|
|
467
474
|
if (cumo_na_check_contiguous(v) != Qtrue) {
|
|
468
475
|
rb_raise(rb_eArgError, "a kernel takes a contiguous NArray, and this one is a view with a stride or an index");
|
|
469
476
|
}
|
|
470
477
|
return 1;
|
|
471
478
|
}
|
|
472
479
|
|
|
480
|
+
/*
|
|
481
|
+
@param [Cumo::NArray] narray
|
|
482
|
+
@return [Array, nil] [address, strides in elements, first byte, last byte + 1], nil for an index view
|
|
483
|
+
*/
|
|
484
|
+
static VALUE
|
|
485
|
+
rb_narray_view_layout(VALUE self, VALUE narray)
|
|
486
|
+
{
|
|
487
|
+
cumo_narray_t *na;
|
|
488
|
+
VALUE strides;
|
|
489
|
+
ssize_t elmsz, s, lo = 0, hi = 0;
|
|
490
|
+
int k;
|
|
491
|
+
char *ptr;
|
|
492
|
+
|
|
493
|
+
if (!kernel_arg_is_typed_narray(narray)) {
|
|
494
|
+
rb_raise(rb_eTypeError, "a %s has no layout to hand to a kernel", rb_obj_classname(narray));
|
|
495
|
+
}
|
|
496
|
+
CumoGetNArray(narray, na);
|
|
497
|
+
elmsz = (ssize_t)cumo_na_element_stride(narray);
|
|
498
|
+
strides = rb_ary_new_capa(na->ndim);
|
|
499
|
+
if (CUMO_NA_TYPE(na) == CUMO_NARRAY_VIEW_T) {
|
|
500
|
+
for (k = 0; k < na->ndim; k++) {
|
|
501
|
+
cumo_stridx_t sdx = CUMO_NA_VIEW_STRIDX(na)[k];
|
|
502
|
+
if (CUMO_SDX_IS_INDEX(sdx)) return Qnil;
|
|
503
|
+
s = CUMO_SDX_GET_STRIDE(sdx);
|
|
504
|
+
if (s % elmsz != 0) return Qnil;
|
|
505
|
+
rb_ary_push(strides, SSIZET2NUM(na->shape[k] == 1 ? 0 : s / elmsz));
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
s = 1;
|
|
509
|
+
for (k = na->ndim; k--;) {
|
|
510
|
+
rb_ary_store(strides, k, SSIZET2NUM(na->shape[k] == 1 ? 0 : s));
|
|
511
|
+
s *= (ssize_t)na->shape[k];
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (CUMO_NA_TYPE(na) == CUMO_NARRAY_DATA_T && CUMO_NA_DATA_PTR(na) == NULL && na->size > 0) {
|
|
515
|
+
rb_funcall(narray, rb_intern("allocate"), 0);
|
|
516
|
+
}
|
|
517
|
+
ptr = cumo_na_get_offset_pointer_for_read(narray);
|
|
518
|
+
for (k = 0; k < na->ndim; k++) {
|
|
519
|
+
ssize_t d = NUM2SSIZET(RARRAY_AREF(strides, k)) * (ssize_t)(na->shape[k] - 1);
|
|
520
|
+
if (d < 0) lo += d; else hi += d;
|
|
521
|
+
}
|
|
522
|
+
RB_GC_GUARD(narray);
|
|
523
|
+
return rb_ary_new_from_args(4, ULL2NUM((unsigned long long)(uintptr_t)ptr), strides,
|
|
524
|
+
ULL2NUM((unsigned long long)(uintptr_t)(ptr + lo * elmsz)),
|
|
525
|
+
ULL2NUM((unsigned long long)(uintptr_t)(na->size == 0 ? ptr : ptr + (hi + 1) * elmsz)));
|
|
526
|
+
}
|
|
527
|
+
|
|
473
528
|
static size_t
|
|
474
529
|
kernel_arg_set(VALUE v, kernel_arg_t *slot, void **param)
|
|
475
530
|
{
|
|
@@ -592,6 +647,7 @@ Init_cumo_cuda_driver()
|
|
|
592
647
|
rb_define_singleton_method(mDriver, "cuModuleLoadData", rb_cuModuleLoadData, 1);
|
|
593
648
|
rb_define_singleton_method(mDriver, "cuModuleUnload", rb_cuModuleUnload, 1);
|
|
594
649
|
rb_define_singleton_method(mDriver, "cuLaunchKernel", rb_cuLaunchKernel, 10);
|
|
650
|
+
rb_define_singleton_method(mDriver, "narray_view_layout", rb_narray_view_layout, 1);
|
|
595
651
|
|
|
596
652
|
rb_define_singleton_method(mDriver, "cuDeviceGet", rb_cuDeviceGet, 1);
|
|
597
653
|
rb_define_singleton_method(mDriver, "cuCtxCreate", rb_cuCtxCreate, 2);
|
|
@@ -208,9 +208,15 @@ rb_memory_pool_n_free_blocks(VALUE self)
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
/*
|
|
211
|
-
Get the total number of bytes
|
|
211
|
+
Get the total number of bytes handed out and not taken back.
|
|
212
212
|
|
|
213
|
-
|
|
213
|
+
A block counts until Ruby's garbage collector frees the array that holds it,
|
|
214
|
+
so an array whose last reference is gone still counts until the collector
|
|
215
|
+
reaches it. Read this after GC.start for the arrays that are alive. Python
|
|
216
|
+
frees an array when its last reference goes, so CuPy's used_bytes and
|
|
217
|
+
PyTorch's memory_allocated drop at once where this waits for the collector.
|
|
218
|
+
|
|
219
|
+
@return [Integer] The total number of bytes handed out and not taken back.
|
|
214
220
|
*/
|
|
215
221
|
static VALUE
|
|
216
222
|
rb_memory_pool_used_bytes(VALUE self)
|
|
@@ -232,6 +238,10 @@ rb_memory_pool_free_bytes(VALUE self)
|
|
|
232
238
|
/*
|
|
233
239
|
Get the total number of bytes acquired in the pool.
|
|
234
240
|
|
|
241
|
+
It counts the free blocks the pool keeps as well as the ones handed out, and
|
|
242
|
+
does not move with the garbage collector, so it answers how much the program
|
|
243
|
+
took rather than how much is alive.
|
|
244
|
+
|
|
235
245
|
@return [Integer] The total number of bytes acquired in the pool.
|
|
236
246
|
*/
|
|
237
247
|
static VALUE
|
data/ext/cumo/depend.erb
CHANGED
|
@@ -19,7 +19,7 @@ CU_TMPL = <%=Dir.glob("#{__dir__}/narray/gen/tmpl*/*.cu").join(" ")%>
|
|
|
19
19
|
C_COGEN = <%= __dir__ %>/narray/gen/cogen.rb
|
|
20
20
|
CU_COGEN = <%= __dir__ %>/narray/gen/cogen_kernel.rb
|
|
21
21
|
C_DEPENDS = $(C_TMPL) <%= __dir__ %>/narray/gen/*.rb
|
|
22
|
-
CU_DEPENDS = $(CU_TMPL) <%= __dir__ %>/narray/gen/*.rb
|
|
22
|
+
CU_DEPENDS = $(CU_TMPL) <%= __dir__ %>/narray/gen/*.rb <%= __dir__ %>/include/cumo/indexer.h
|
|
23
23
|
|
|
24
24
|
<%
|
|
25
25
|
list_type_c = []
|
|
@@ -273,7 +273,7 @@ static inline bit_reduce_plan make_bit_reduce_plan(const TArg& arg) {
|
|
|
273
273
|
p.wa = make_bit_word_addr(arg, p.ad, p.reduce_total_size);
|
|
274
274
|
p.unit_total_size = p.wa.unit_total;
|
|
275
275
|
|
|
276
|
-
cumo_detail::reduce_block_split(p.ad, p.unit_total_size, &p.out_block_size, &p.reduce_block_size);
|
|
276
|
+
cumo_detail::reduce_block_split(p.ad, p.unit_total_size, &p.out_block_size, &p.reduce_block_size, false);
|
|
277
277
|
p.out_block_num = (p.out_total_size + p.out_block_size - 1) / p.out_block_size;
|
|
278
278
|
|
|
279
279
|
p.n_split = reduce_split_count(p.unit_total_size, p.out_block_num, p.wa.words != 0);
|
|
@@ -283,7 +283,7 @@ static inline bit_reduce_plan make_bit_reduce_plan(const TArg& arg) {
|
|
|
283
283
|
p.partial_block_num = 0;
|
|
284
284
|
if (p.n_split > 1) {
|
|
285
285
|
p.chunk = (p.unit_total_size + p.n_split - 1) / p.n_split;
|
|
286
|
-
cumo_detail::reduce_block_split(p.ad, p.chunk, &p.split_out_block_size, &p.split_reduce_block_size);
|
|
286
|
+
cumo_detail::reduce_block_split(p.ad, p.chunk, &p.split_out_block_size, &p.split_reduce_block_size, false);
|
|
287
287
|
p.partial_block_num = (p.out_total_size * p.n_split + p.split_out_block_size - 1) / p.split_out_block_size;
|
|
288
288
|
// Splitting buys nothing when the narrower block it leaves takes the
|
|
289
289
|
// block count back down to where it started.
|