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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/README.md +49 -208
  4. data/ext/cumo/cuda/cudnn_impl.cpp +2 -3
  5. data/ext/cumo/cuda/driver.c +57 -1
  6. data/ext/cumo/cuda/memory_pool.cpp +12 -2
  7. data/ext/cumo/depend.erb +1 -1
  8. data/ext/cumo/include/cumo/bit_reduce_kernel.h +2 -2
  9. data/ext/cumo/include/cumo/cuda/cumo_thrust.hpp +7 -0
  10. data/ext/cumo/include/cumo/indexer.h +126 -9
  11. data/ext/cumo/include/cumo/intern.h +5 -1
  12. data/ext/cumo/include/cumo/reduce_kernel.h +29 -27
  13. data/ext/cumo/include/cumo/row_kernel.h +77 -15
  14. data/ext/cumo/include/cumo/template.h +1 -5
  15. data/ext/cumo/include/cumo/template_kernel.h +14 -0
  16. data/ext/cumo/include/cumo.h +2 -2
  17. data/ext/cumo/narray/data.c +84 -0
  18. data/ext/cumo/narray/data_kernel.cu +220 -76
  19. data/ext/cumo/narray/gen/cogen_kernel.rb +24 -1
  20. data/ext/cumo/narray/gen/tmpl/binary2_kernel.cu +2 -11
  21. data/ext/cumo/narray/gen/tmpl/binary_kernel.cu +72 -22
  22. data/ext/cumo/narray/gen/tmpl/binary_s_kernel.cu +2 -11
  23. data/ext/cumo/narray/gen/tmpl/clip_kernel.cu +4 -31
  24. data/ext/cumo/narray/gen/tmpl/cond_binary_kernel.cu +2 -11
  25. data/ext/cumo/narray/gen/tmpl/cond_unary_kernel.cu +2 -11
  26. data/ext/cumo/narray/gen/tmpl/cum.c +11 -60
  27. data/ext/cumo/narray/gen/tmpl/cum_kernel.cu +53 -24
  28. data/ext/cumo/narray/gen/tmpl/ewcomp_kernel.cu +2 -11
  29. data/ext/cumo/narray/gen/tmpl/fill.c +11 -3
  30. data/ext/cumo/narray/gen/tmpl/fill_kernel.cu +20 -11
  31. data/ext/cumo/narray/gen/tmpl/frexp_kernel.cu +2 -11
  32. data/ext/cumo/narray/gen/tmpl/layer_norm_kernel.cu +98 -0
  33. data/ext/cumo/narray/gen/tmpl/logseq.c +28 -14
  34. data/ext/cumo/narray/gen/tmpl/logseq_kernel.cu +19 -23
  35. data/ext/cumo/narray/gen/tmpl/poly_kernel.cu +1 -10
  36. data/ext/cumo/narray/gen/tmpl/pow_kernel.cu +3 -21
  37. data/ext/cumo/narray/gen/tmpl/rand.c +9 -3
  38. data/ext/cumo/narray/gen/tmpl/rand_kernel.cu +10 -14
  39. data/ext/cumo/narray/gen/tmpl/rand_norm.c +9 -3
  40. data/ext/cumo/narray/gen/tmpl/rand_norm_kernel.cu +10 -14
  41. data/ext/cumo/narray/gen/tmpl/rms_norm_kernel.cu +67 -0
  42. data/ext/cumo/narray/gen/tmpl/seq.c +9 -3
  43. data/ext/cumo/narray/gen/tmpl/seq_kernel.cu +11 -15
  44. data/ext/cumo/narray/gen/tmpl/set2.c +13 -4
  45. data/ext/cumo/narray/gen/tmpl/set2_kernel.cu +9 -15
  46. data/ext/cumo/narray/gen/tmpl/softmax_kernel.cu +70 -4
  47. data/ext/cumo/narray/gen/tmpl/sort.c +1 -3
  48. data/ext/cumo/narray/gen/tmpl/sort_index.c +1 -1
  49. data/ext/cumo/narray/gen/tmpl/store.c +6 -0
  50. data/ext/cumo/narray/gen/tmpl/store_bit_kernel.cu +1 -10
  51. data/ext/cumo/narray/gen/tmpl/store_from_kernel.cu +6 -21
  52. data/ext/cumo/narray/gen/tmpl/store_numeric.c +9 -5
  53. data/ext/cumo/narray/gen/tmpl/unary2_kernel.cu +2 -11
  54. data/ext/cumo/narray/gen/tmpl/unary_kernel.cu +40 -12
  55. data/ext/cumo/narray/gen/tmpl/unary_ret2_kernel.cu +2 -11
  56. data/ext/cumo/narray/gen/tmpl/unary_s_kernel.cu +40 -12
  57. data/ext/cumo/narray/gen/tmpl_bit/binary_kernel.cu +1 -10
  58. data/ext/cumo/narray/gen/tmpl_bit/fill_kernel.cu +1 -10
  59. data/ext/cumo/narray/gen/tmpl_bit/store_bit_kernel.cu +1 -10
  60. data/ext/cumo/narray/gen/tmpl_bit/store_from_kernel.cu +1 -10
  61. data/ext/cumo/narray/gen/tmpl_bit/unary_kernel.cu +1 -10
  62. data/ext/cumo/narray/index.c +2 -2
  63. data/ext/cumo/narray/narray.c +233 -21
  64. data/ext/cumo/narray/ndloop.c +9 -3
  65. data/ext/cumo/narray/ndloop_kernel.cu +87 -89
  66. data/lib/cumo/cuda/compiler.rb +12 -0
  67. data/lib/cumo/cuda/elementwise_kernel.rb +23 -10
  68. data/lib/cumo/cuda/reduction_kernel.rb +47 -16
  69. data/lib/cumo/cuda/user_kernel.rb +90 -6
  70. data/lib/cumo/narray/extra.rb +5 -2
  71. data/test/cuda/elementwise_kernel_test.rb +122 -1
  72. data/test/cuda/reduction_kernel_test.rb +93 -6
  73. data/test/cuda/stream_test.rb +1 -0
  74. data/test/extra_test.rb +46 -0
  75. data/test/fused_test.rb +24 -3
  76. data/test/narray_test.rb +633 -7
  77. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78444d61470bd2cf534dfadba1facef1d92785cf85db498ca14d893242532591
4
- data.tar.gz: 7bb662f78bd6e130c5a93db7fe6ac44fc2dad14f58f3788de48c04f7830197b4
3
+ metadata.gz: d50aabaf6d47d63e97fc6c68726cac94af2ecb8b737a3441de618860d2f5ab6a
4
+ data.tar.gz: 9d7bfc02015586d0ef533802be1d48558abf93fddf362ce445c55b0b4713ab7b
5
5
  SHA512:
6
- metadata.gz: 46d001da3c8c82e4f835fe6643cec1a1f5a4b28864283286a52f98887d975673914ee475ecd90d97042e5c16e45c525060d595ecd8301a427fed5060a8efc7b0
7
- data.tar.gz: ae0052bea91487469a2dd6052c08f9bb8c74799447ea165d189babc88ccd94b7c686e50d1ff69f1df75ec2d20986c7113ff2d3b949205c4ac72132f7b8d468af
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` prices this with a conjugate gradient solve, 200 iterations over a 512x512 grid on an RTX 5070 Ti Laptop:
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 are worth 2.1x.
202
- The relative residual is identical in every row.
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, a few microseconds for a scalar.
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 cost 0.8 ms that way on the machine above, forty times what the copy costs.
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
- RTX 5070 Ti Laptop, `Cumo::SFloat`, 200 calls a measurement:
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 sixty-fourth of its single-precision one, it costs a great deal.
266
- 512x2048 elements in place on an RTX 5070 Ti Laptop:
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)` takes 268.0 us against 12.6 us for `Cumo::NMath.ldexp(a, 2)`, and the difference there is the doubled arrays it has to allocate rather than the arithmetic.
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, 14.9 us against 219.3 us
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: it takes twice as long that way.
318
- Past an `M * K` of roughly fifty thousand on this card the copy starts paying for itself, and past a few hundred thousand it is worth a fifth of the time.
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
- The table above is two-dimensional, where cuBLAS is given one matrix.
328
- A batched multiplication takes another path through the same flag, and these numbers do not cover it.
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 costs about two microseconds whatever it is handed, so a short row pays for the launches rather than for its bytes.
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
- On an RTX 5070 Ti Laptop, against the same arithmetic spelled with operators, in microseconds:
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 runs 1.1x to 4.3x faster on an RTX 5070 Ti Laptop, in microseconds:
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 runs 1.2x to 4.8x faster on an RTX 5070 Ti Laptop, in microseconds:
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 runs 1.0x to 3.1x faster on an RTX 5070 Ti Laptop, in microseconds:
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. Square matrices on an RTX 5070 Ti Laptop, median of three runs each:
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 takes 0.211 ms with that odd 50257 and 0.205 ms with 50256, a difference inside the noise; the same 768x50257 matrix against 256 rows takes 0.833 ms and 0.481 ms, which is not.
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
- N=32, C=K=64, 56x56, 3x3:
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 is still faster in half, but for the other reason:
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 lands on the same throughput as half. Square matrices on an RTX 5070 Ti Laptop, median of five runs each, one process per case:
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 only GPU these numbers came from is a Blackwell one, and the requirement is read from cuBLAS's documentation rather than reproduced.
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
- `Cumo::Bit`, `Cumo::HFloat`, `Cumo::BFloat`, `Cumo::SComplex`, `Cumo::DComplex` and `Cumo::RObject` cannot be handed to one of these kernels yet.
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
- Some shapes want more than the default. The twenty convolutions of a ResNet-18 forward pass at batch 16, and the single-precision case from [Half Precision](#half-precision):
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
- Measured over the convolutions of a ResNet-18 forward pass at batch 16, with the ceiling raised to 256MB, each layer against a double precision reference:
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 `[4096, 4096]` `SFloat` `gemm` goes from 17.8 to 28.4 TFLOP/s on an RTX 5070 Ti Laptop, 1.60 times over six interleaved rounds.
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 affordable -- cudnnQueryRuntimeError costs 0.1us
658
- // either way -- but the mode is 1.3 to 1.8x faster only where the
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
@@ -458,18 +458,73 @@ typedef union {
458
458
  } kernel_arg_t;
459
459
 
460
460
  static int
461
- kernel_arg_is_narray(VALUE v)
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 used.
211
+ Get the total number of bytes handed out and not taken back.
212
212
 
213
- @return [Integer] The total number of bytes used.
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.