cumo 0.7.0 → 0.8.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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +590 -152
  3. data/CHANGELOG.md +29 -0
  4. data/README.md +140 -0
  5. data/bench/cumo_probe.rb +1 -1
  6. data/cumo.gemspec +1 -1
  7. data/ext/cumo/cuda/cudnn.c +18 -0
  8. data/ext/cumo/cuda/cudnn_impl.cpp +60 -14
  9. data/ext/cumo/cuda/driver.c +11 -9
  10. data/ext/cumo/cuda/runtime.c +56 -0
  11. data/ext/cumo/cumo.c +4 -0
  12. data/ext/cumo/extconf.rb +2 -0
  13. data/ext/cumo/include/cumo/bit_reduce_kernel.h +6 -6
  14. data/ext/cumo/include/cumo/cuda/cudnn.h +40 -0
  15. data/ext/cumo/include/cumo/cuda/runtime.h +2 -0
  16. data/ext/cumo/include/cumo/intern.h +9 -0
  17. data/ext/cumo/include/cumo/narray.h +1 -0
  18. data/ext/cumo/include/cumo/reduce_kernel.h +32 -28
  19. data/ext/cumo/include/cumo/row_kernel.h +170 -0
  20. data/ext/cumo/include/cumo/row_method.h +48 -0
  21. data/ext/cumo/include/cumo/template_kernel.h +26 -0
  22. data/ext/cumo/include/cumo/types/bf16_def.h +82 -0
  23. data/ext/cumo/include/cumo/types/bf16_def_kernel.h +28 -0
  24. data/ext/cumo/include/cumo/types/bf16_macro.h +186 -0
  25. data/ext/cumo/include/cumo/types/bf16_macro_kernel.h +213 -0
  26. data/ext/cumo/include/cumo/types/bfloat.h +22 -0
  27. data/ext/cumo/include/cumo/types/bfloat_kernel.h +16 -0
  28. data/ext/cumo/include/cumo/types/bit.h +2 -0
  29. data/ext/cumo/include/cumo/types/bit_kernel.h +2 -0
  30. data/ext/cumo/include/cumo/types/complex_macro.h +2 -0
  31. data/ext/cumo/include/cumo/types/complex_macro_kernel.h +2 -0
  32. data/ext/cumo/include/cumo/types/float_def.h +8 -0
  33. data/ext/cumo/include/cumo/types/float_def_kernel.h +8 -0
  34. data/ext/cumo/include/cumo/types/float_macro.h +19 -17
  35. data/ext/cumo/include/cumo/types/float_macro_kernel.h +19 -19
  36. data/ext/cumo/include/cumo/types/half_macro.h +15 -17
  37. data/ext/cumo/include/cumo/types/half_macro_kernel.h +15 -0
  38. data/ext/cumo/include/cumo/types/robj_macro.h +2 -0
  39. data/ext/cumo/include/cumo/types/xint_macro.h +2 -6
  40. data/ext/cumo/include/cumo/types/xint_macro_kernel.h +2 -0
  41. data/ext/cumo/include/cumo.h +2 -2
  42. data/ext/cumo/narray/gen/def/bfloat.rb +56 -0
  43. data/ext/cumo/narray/gen/def/bit.rb +11 -1
  44. data/ext/cumo/narray/gen/def/dcomplex.rb +11 -1
  45. data/ext/cumo/narray/gen/def/dfloat.rb +10 -1
  46. data/ext/cumo/narray/gen/def/hfloat.rb +10 -1
  47. data/ext/cumo/narray/gen/def/int16.rb +11 -1
  48. data/ext/cumo/narray/gen/def/int32.rb +11 -1
  49. data/ext/cumo/narray/gen/def/int64.rb +11 -1
  50. data/ext/cumo/narray/gen/def/int8.rb +11 -1
  51. data/ext/cumo/narray/gen/def/robject.rb +11 -1
  52. data/ext/cumo/narray/gen/def/scomplex.rb +11 -1
  53. data/ext/cumo/narray/gen/def/sfloat.rb +10 -1
  54. data/ext/cumo/narray/gen/def/uint16.rb +11 -1
  55. data/ext/cumo/narray/gen/def/uint32.rb +11 -1
  56. data/ext/cumo/narray/gen/def/uint64.rb +11 -1
  57. data/ext/cumo/narray/gen/def/uint8.rb +11 -1
  58. data/ext/cumo/narray/gen/narray_def.rb +0 -5
  59. data/ext/cumo/narray/gen/spec.rb +11 -14
  60. data/ext/cumo/narray/gen/tmpl/accum_arg.c +1 -1
  61. data/ext/cumo/narray/gen/tmpl/accum_binary_kernel.cu +9 -9
  62. data/ext/cumo/narray/gen/tmpl/accum_index.c +1 -1
  63. data/ext/cumo/narray/gen/tmpl/batch_norm.c +3 -3
  64. data/ext/cumo/narray/gen/tmpl/batch_norm_backward.c +3 -3
  65. data/ext/cumo/narray/gen/tmpl/complex_accum_kernel.cu +15 -5
  66. data/ext/cumo/narray/gen/tmpl/conv.c +88 -121
  67. data/ext/cumo/narray/gen/tmpl/conv_grad_w.c +89 -81
  68. data/ext/cumo/narray/gen/tmpl/conv_transpose.c +88 -121
  69. data/ext/cumo/narray/gen/tmpl/cum.c +8 -7
  70. data/ext/cumo/narray/gen/tmpl/cum_kernel.cu +7 -6
  71. data/ext/cumo/narray/gen/tmpl/fixed_batch_norm.c +3 -3
  72. data/ext/cumo/narray/gen/tmpl/float_accum_kernel.cu +22 -10
  73. data/ext/cumo/narray/gen/tmpl/gemm.c +11 -35
  74. data/ext/cumo/narray/gen/tmpl/layer_norm.c +91 -0
  75. data/ext/cumo/narray/gen/tmpl/layer_norm_kernel.cu +54 -0
  76. data/ext/cumo/narray/gen/tmpl/lib.c +1 -0
  77. data/ext/cumo/narray/gen/tmpl/lib_kernel.cu +1 -0
  78. data/ext/cumo/narray/gen/tmpl/median.c +1 -1
  79. data/ext/cumo/narray/gen/tmpl/poly.c +0 -3
  80. data/ext/cumo/narray/gen/tmpl/poly_kernel.cu +1 -1
  81. data/ext/cumo/narray/gen/tmpl/pooling_backward.c +2 -1
  82. data/ext/cumo/narray/gen/tmpl/pooling_forward.c +2 -1
  83. data/ext/cumo/narray/gen/tmpl/rand.c +9 -9
  84. data/ext/cumo/narray/gen/tmpl/rand_kernel.cu +9 -9
  85. data/ext/cumo/narray/gen/tmpl/rand_norm.c +4 -4
  86. data/ext/cumo/narray/gen/tmpl/rand_norm_kernel.cu +7 -7
  87. data/ext/cumo/narray/gen/tmpl/real_accum_kernel.cu +6 -6
  88. data/ext/cumo/narray/gen/tmpl/softmax.c +56 -0
  89. data/ext/cumo/narray/gen/tmpl/softmax_kernel.cu +157 -0
  90. data/ext/cumo/narray/gen/tmpl/sort.c +9 -32
  91. data/ext/cumo/narray/gen/tmpl/sort_index.c +16 -95
  92. data/ext/cumo/narray/gen/tmpl/store_array.c +12 -9
  93. data/ext/cumo/narray/gen/tmpl_bit/mask.c +11 -11
  94. data/ext/cumo/narray/gen/tmpl_bit/store_array.c +5 -9
  95. data/ext/cumo/narray/gen/tmpl_bit/where.c +50 -11
  96. data/ext/cumo/narray/gen/tmpl_bit/where2.c +11 -12
  97. data/ext/cumo/narray/gen/tmpl_bit/where_kernel.cu +1 -2
  98. data/ext/cumo/narray/index.c +2 -1
  99. data/ext/cumo/narray/math.c +2 -0
  100. data/ext/cumo/narray/narray.c +27 -1
  101. data/ext/cumo/narray/ndloop.c +73 -4
  102. data/ext/cumo/narray/sort_kernel.cu +61 -41
  103. data/ext/cumo/narray/struct.c +3 -0
  104. data/lib/cumo/cuda/cudnn.rb +1 -1
  105. data/lib/cumo/narray/extra.rb +2 -2
  106. data/test/bfloat_test.rb +238 -0
  107. data/test/cuda/memory_pool_test.rb +8 -7
  108. data/test/cuda/runtime_test.rb +25 -0
  109. data/test/cudnn_test.rb +108 -24
  110. data/test/extra_test.rb +1 -1
  111. data/test/fused_test.rb +311 -0
  112. data/test/math_test.rb +77 -2
  113. data/test/narray_alt_coverage_test.rb +1 -0
  114. data/test/narray_test.rb +70 -10
  115. data/test/test_helper.rb +1 -0
  116. metadata +20 -4
  117. data/ext/cumo/narray/gen/tmpl/qsort.c +0 -205
data/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ # 0.8.0 (2026/09/16)
2
+
3
+ Breaking changes:
4
+
5
+ * `sort` and `sort_index` with `nan: true` put every NaN last. The host comparator they used answered -1 for a NaN against anything, so the partition carried elements other than the NaN to the wrong side: of 4156 cases over lengths 5 to 129, 2121 came back with the numbers out of order. `nan:` no longer changes the answer, and 1M DFloat costs 0.405 ms rather than 73.0 (PR #447)
6
+ * `conv` and `conv_transpose` refuse a bias that is not one value per output channel. A bias of length 1 was broadcast over every channel and answered as if it had been right, and a bias of two dimensions passed whenever its size fitted (PR #443)
7
+ * `var` and `stddev` answer NaN for a row holding an infinity, where the answer turned on where the infinity sat and how long the row was (PR #430)
8
+
9
+ Fixes:
10
+
11
+ * Fix every `dot` after `Cumo::CUDA::Runtime.cudaSetDevice` raising `CUBLAS_STATUS_EXECUTION_FAILED` for the life of the process. Cumo took a context of its own when it loaded, and `cudaSetDevice` binds the device's primary one, so the event inside the cuBLAS handle was left in a context nothing ran in (PR #451)
12
+ * Fix the device memory a rejected kernel launch left in the pool, `rb_raise` leaving by longjmp past the free below it. A sort lost 24MB and a convolution given a bias of the wrong class 5MB, every time; reductions, `softmax`, `layer_norm`, `Cumo::Bit#count`, `where`, `poly` and a store from a Ruby Array lost theirs the same way (PR #439, PR #438, PR #437, PR #436, PR #434, PR #433, PR #432, PR #431)
13
+ * Fix two cuDNN descriptor leaks, one where `batch_norm` raises after building its descriptors and one where a convolution finds no algorithm inside `CUMO_CUDNN_MAX_WORKSPACE_SIZE` (PR #441, PR #440)
14
+ * Fix a `std::bad_alloc` from the convolution algorithm cache killing the process, `cudnn_impl.cpp` being one `extern "C"` block with no handler in it (PR #444)
15
+ * Fix the nine cuDNN methods and `layer_norm` reading the wrong memory when `dup` answers a different class, a different shape or a non-contiguous view; `conv` took a reversed view and read past it (PR #425)
16
+ * Fix the spelling of "propagate" and "position" in the `nan:` documentation on `sort`, `sort_index`, `median`, `argmin`, `argmax`, `min_index` and `max_index` (PR #446)
17
+
18
+ Changes:
19
+
20
+ * Add `Cumo::BFloat`, the bfloat16 dtype, aliased `Cumo::BFloat16`. It keeps a float's exponent and gives up mantissa bits for it, so it holds the magnitudes a `Cumo::HFloat` overflows on, which is how published weights are written. Everything the dtype has is generated: the four operations, the comparisons, the reductions, `Cumo::NMath`, `sort`, `rand`, `dot` through cuBLAS and the cuDNN methods, both with a float compute type. `BFloat` and `HFloat` promote to `SFloat` when they meet, neither format containing the other. The accelerated paths want compute capability 8.0 (PR #455, PR #453, PR #452, PR #450)
21
+ * Add `layer_norm`, which normalizes a row in one kernel rather than the nine it is written out of, and `softmax`, which takes one rather than five. On SFloat `[1, 768]` the first goes from 19.9 to 2.9us and the second from 11.0 to 2.2, and `[4096, 768]` from 198.3 to 31.4 and 109.6 to 31.6. No shape is slower (PR #428, PR #426, PR #424, PR #423)
22
+ * Add `gelu` and `gelu_tanh` to `Cumo::NMath`. Two of them because there are two: `gelu` is the error function form PyTorch answers by default, and `gelu_tanh` the approximation GPT-2 and the models after it were trained with. They differ by 4.1e-4 at most, too little to tell apart by eye and far too much to swap under trained weights (PR #427)
23
+ * Take one reciprocal instead of two divides in the moments accumulator, a double divide being what this class of card charges most for; `var` goes from 172.7 to 106.5us in DFloat and `layer_norm` from 179.6 to 138.0 (PR #429)
24
+ * Store a Ruby Array into a strided or indexed view through one buffer held by the loop rather than one a row, a row having cost 2.53 times what the same payload written contiguously does and now costing 1.04 (PR #445)
25
+ * Stop `Cumo::RObject#poly` stopping the whole device once per element. It runs with `CUMO_NO_LOOP`, so ndloop calls its iterator once an element, and there is nothing left there to wait for (PR #435)
26
+ * Say once what `conv` and `conv_transpose` had each grown a copy of, the run struct and the bias block, and build the bias descriptor from a shape rather than lending the narray one (PR #442)
27
+ * Declare the gem's license as MIT. RubyGems reads the gemspec, which said BSD-3-Clause while the `LICENSE.txt` inside the same gem has carried the MIT text from the start (PR #448)
28
+ * Skip the CUDA build matrix for a README, license or benchmark edit no build step opens, and give ccache a ceiling one build fits in (PR #454, PR #449)
29
+
1
30
  # 0.7.0 (2026/09/13)
2
31
 
3
32
  Breaking changes:
data/README.md CHANGED
@@ -222,6 +222,68 @@ Cumo::SFloat::Math.atan2(a, 2.0) #=> Cumo::SFloat
222
222
 
223
223
  The 0-dimensional form has no effect under Numo, where `[]` returns a Ruby Float.
224
224
 
225
+ ### Fused Operations
226
+
227
+ `layer_norm` and `softmax` normalize along the last axis in one kernel each.
228
+ Written out of the operators they take nine launches and five, and a launch costs about two microseconds whatever it is handed, so a short row pays for the launches rather than for its bytes.
229
+
230
+ ```ruby
231
+ y = x.layer_norm(gamma, beta, eps: 1e-5) # (x - mean) / sqrt(var + eps) * gamma + beta
232
+ probs = scores.softmax # exp(x - max) / sum(exp(x - max))
233
+ ```
234
+
235
+ On an RTX 5070 Ti Laptop, against the same arithmetic spelled with operators, in microseconds:
236
+
237
+ ```
238
+ layer_norm SFloat DFloat HFloat
239
+ shape fused written fused written fused written
240
+ 1 x 768 3.9 30.1 9.7 30.3 3.9 29.8
241
+ 256 x 768 6.6 22.5 22.0 35.4 7.8 22.4
242
+ 4096 x 768 32.7 177.4 300.7 610.5 28.2 142.9
243
+ 1 x 1000000 19.0 59.4 125.4 175.4 22.3 68.5
244
+
245
+ softmax SFloat DFloat HFloat
246
+ shape fused written fused written fused written
247
+ 1 x 768 4.3 19.8 7.4 26.5 6.3 34.4
248
+ 256 x 768 8.8 13.9 23.4 35.1 6.8 18.5
249
+ 4096 x 768 49.2 122.7 318.3 549.0 28.2 109.8
250
+ 1 x 1000000 27.8 44.0 108.6 148.5 32.1 53.8
251
+ ```
252
+
253
+ Both 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.
254
+ The memory clock on this card steps between 9001 and 11001 MHz under a benchmark this short, and the absolute figures move with it.
255
+ The ratios hold across the steps.
256
+
257
+ Both methods normalize along the last axis only.
258
+ Other axes are reachable through `transpose`.
259
+ `layer_norm` divides the variance by the row length and not by one less, which is how the layer is defined and unlike `var`.
260
+ Both answer a new array and ignore `inplace!`, and a non-contiguous receiver is copied once rather than walked.
261
+ `gamma` and `beta` must be one-dimensional, as long as the last axis, and of the same class as the receiver.
262
+
263
+ `softmax` subtracts the row maximum before exponentiating, so a row masked with `-Float::INFINITY` answers zeros rather than `NaN`.
264
+ A row that is entirely `-Float::INFINITY` answers `NaN`, as the written-out form does.
265
+
266
+ Those two and `gelu_tanh` from the section below are where a transformer spends most of its launches.
267
+ Decoding one token of GPT-2 124M written against Numo's API takes 640 of them, and calling these three instead removes 332: 200 for the layer norms, 84 for the activations and 48 for the softmaxes.
268
+
269
+ ### Two Spellings Of gelu
270
+
271
+ `Cumo::NMath` answers both definitions of the GELU activation, under names of
272
+ their own rather than a keyword, since a unary NMath function takes none.
273
+
274
+ | method | formula | matches |
275
+ | --- | --- | --- |
276
+ | `gelu` | `0.5 * x * (1 + erf(x / sqrt(2)))` | PyTorch's default, `approximate='none'` |
277
+ | `gelu_tanh` | `0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x**3)))` | PyTorch's `approximate='tanh'`, and what GPT-2 was trained with |
278
+
279
+ They differ by at most 4.1e-4, which is small enough to mistake one for the
280
+ other and far too large to swap under a set of trained weights, so pick the one
281
+ the weights were trained with.
282
+
283
+ Both follow PyTorch at the edges rather than the limit: a large negative `x`
284
+ runs out of significant digits and answers zero, and `-Float::INFINITY` answers
285
+ `NaN`.
286
+
225
287
  ### Half Precision
226
288
 
227
289
  `Cumo::HFloat`, also reachable as `Cumo::Float16`, holds IEEE binary16: one sign bit, five of exponent and ten of mantissa.
@@ -262,6 +324,7 @@ That distinction is what makes half usable in a transformer's layer normalizatio
262
324
  Squaring overflows at 256, since 256 squared is already past the top, and a residual stream with one outlier feature reaches thousands.
263
325
  Writing the normalization out as `((x - mean) ** 2).mean` builds those squares as a half array and the answer is `Infinity`, while `x.var(axis: 1)` keeps them in its single-precision accumulator and answers 10208.0 against a single-precision 10209.
264
326
  `var` saturates only if the variance itself is out of range, which is a far higher bar than any one deviation being over 256, but it is still a bar.
327
+ An input over 65504 is a different matter: it becomes an infinite element on the way in, and `var` and `stddev` answer `NaN` for a row holding one.
265
328
  The quantity to check against 65504 is the largest variance a layer produces, not the largest activation in it.
266
329
  A single outlier of size d among n values contributes only d squared over n to the variance, so a row 768 wide divides it by 768: an activation of 3000 squares to nine million but raises the variance of its row by about twelve thousand.
267
330
  An activation that looks safe therefore says nothing about whether `var` overflows, in either direction.
@@ -342,6 +405,83 @@ Keeping the running statistics in single precision is what the arithmetic wants
342
405
  Gradients are the other place half runs out of room.
343
406
  Values below `Cumo::HFloat::MIN` of 6.1e-05 fall into the subnormals and then to zero, which is what loss scaling in a training loop exists to prevent.
344
407
 
408
+ ### Brain Float
409
+
410
+ `Cumo::BFloat`, also reachable as `Cumo::BFloat16`, holds bfloat16: one sign bit, eight of exponent and seven of mantissa.
411
+ It is the other sixteen-bit float, and it differs from `Cumo::HFloat` in where those bits went.
412
+ A bfloat16 has the exponent of a `Cumo::SFloat` and three fewer mantissa bits than a binary16, so it reaches everything a single-precision value reaches and resolves less of it:
413
+
414
+ ```ruby
415
+ Cumo::BFloat[1.0e38] #=> 9.969209968386869e+37
416
+ Cumo::HFloat[1.0e38] #=> Infinity
417
+
418
+ Cumo::BFloat[257.0] #=> 256.0
419
+ Cumo::HFloat[257.0] #=> 257.0
420
+ ```
421
+
422
+ Integers are exact only to 256, against 2048 for a binary16, and the largest finite value is 3.3895314e+38 rather than 65504.
423
+ That trade is why weights are published in it: a tensor that came out of training keeps its magnitudes, and the bits it loses are ones a trained weight does not carry.
424
+
425
+ The two do not contain each other, so an expression mixing them promotes to `Cumo::SFloat`, which does:
426
+
427
+ ```ruby
428
+ (Cumo::BFloat[1.0] + Cumo::HFloat[1.0]).class #=> Cumo::SFloat
429
+ ```
430
+
431
+ Everything else promotes as `Cumo::SFloat` does, so an integer array or a Ruby Float mixed in stays bfloat16 while anything wider takes over.
432
+
433
+ `layer_norm`, `softmax` and both spellings of `gelu` take it, and so do the reductions, `sort`, `median`, `cumsum`, `rand` and `dot`.
434
+ The cuDNN methods take it too: `conv`, `conv_transpose`, `conv_grad_w`, `max_pool`, `avg_pool` and the three batch norm entries.
435
+
436
+ Reductions accumulate in single precision and round once at the end, exactly as they do for half, so a sum passes 256 without stopping there:
437
+
438
+ ```ruby
439
+ Cumo::BFloat.new(40_000).fill(1.0).sum #=> 39936.0
440
+ ```
441
+
442
+ That 39936 is 40000 rounded to the nearest bfloat16, whose values are 256 apart up there; a bfloat16 accumulator would have answered 256.
443
+ `sum`, `mean`, `var`, `stddev`, `rms`, `mulsum`, `dot`, `gemm` and `cumsum` all widen this way, and `prod` and `cumprod` do not, for the same reasons given under Half Precision.
444
+
445
+ The wider exponent moves where the saturation described there bites.
446
+ A variance is far from 3.4e+38, so `var` does not saturate on any input a network produces, and squaring is safe to 1.8e+19 rather than to 256.
447
+ What replaces it is the mantissa: a bfloat16 resolves about three decimal digits, so a normalization computes its statistics well enough while the values it writes back carry that resolution and no more.
448
+
449
+ #### What bfloat16 is faster at
450
+
451
+ `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:
452
+
453
+ ```
454
+ BFloat HFloat SFloat
455
+ 1024x1024 0.044 ms 48.9 TF 0.044 ms 48.5 TF 0.144 ms 14.9 TF
456
+ 2048x2048 0.298 ms 57.6 TF 0.303 ms 56.7 TF 1.404 ms 12.2 TF
457
+ 4096x4096 2.839 ms 48.4 TF 2.898 ms 47.4 TF 9.540 ms 14.4 TF
458
+ ```
459
+
460
+ The odd-column penalty is the same as half's and for the same reason, a row starting on a two-byte boundary.
461
+ 1024x1024 times 1024x1024, with one dimension made odd at a time:
462
+
463
+ ```
464
+ all even M odd K odd N odd
465
+ BFloat 48.9 TF 48.7 TF 23.5 TF 23.0 TF
466
+ HFloat 48.5 TF - 24.0 TF 23.1 TF
467
+ ```
468
+
469
+ So the choice between the two sixteen-bit types is about range and resolution, not speed.
470
+ Take bfloat16 where the magnitudes came from somewhere else and binary16 where three more mantissa bits are worth having.
471
+
472
+ #### The accelerated paths want Ampere
473
+
474
+ cuBLAS supports `CUDA_R_16BF`, which is what `gemm` and `dot` reach, from compute capability 8.0.
475
+ 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.
476
+ A `dot` on a pre-Ampere card is the one to expect trouble from.
477
+ **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.
478
+
479
+ cuDNN reaches bfloat16 as `CUDNN_DATA_BFLOAT16`, and its own bfloat16 kernels want Ampere for the same reason cuBLAS does.
480
+ A convolution is given `CUDNN_DATA_FLOAT` to accumulate in, so it passes 256 the way a reduction does.
481
+ The batch norm parameters are `Cumo::SFloat`, the same as they are for `Cumo::HFloat`.
482
+ The workspace ceiling matters here as much as it does for half: left at the default 8MB, a bfloat16 convolution is no faster than a single-precision one.
483
+ See [Raise the cuDNN workspace ceiling](#raise-the-cudnn-workspace-ceiling).
484
+
345
485
  ### Select a GPU device ID
346
486
 
347
487
  Set the `CUDA_VISIBLE_DEVICES=id` environment variable, or
data/bench/cumo_probe.rb CHANGED
@@ -312,7 +312,7 @@ op(:scalar, 'scalar_left') { |c| c.scalar - c.sc.inplace }
312
312
 
313
313
  # NMath
314
314
  %w[sqrt exp exp2 exp10 expm1 log log2 log10 log1p sin cos tan asin acos atan
315
- sinh cosh tanh asinh acosh atanh cbrt erf erfc sinc].each do |f|
315
+ sinh cosh tanh asinh acosh atanh cbrt erf erfc sinc gelu gelu_tanh].each do |f|
316
316
  op(:math, f, :float) { |c| XM::NMath.send(f, c.usc.inplace) }
317
317
  end
318
318
  op(:math, 'atan2', :float) { |c| XM::NMath.atan2(c.sc.inplace, c.b) }
data/cumo.gemspec CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
  spec.summary = %q{Cumo is CUDA aware numerical library whose interface is highly compatible with Ruby Numo}
15
15
  spec.description = %q{Cumo is CUDA aware numerical library whose interface is highly compatible with Ruby Numo.}
16
16
  spec.homepage = "https://github.com/sonots/cumo"
17
- spec.license = "BSD-3-Clause"
17
+ spec.license = "MIT"
18
18
 
19
19
  spec.required_ruby_version = ">= 3.0.0"
20
20
 
@@ -53,6 +53,24 @@ BAD:
53
53
  env, cudnn_max_workspace_size);
54
54
  }
55
55
 
56
+ VALUE
57
+ cumo_cuda_cudnn_release_conv_held(VALUE held)
58
+ {
59
+ cumo_cuda_cudnn_conv_held_t *h = (cumo_cuda_cudnn_conv_held_t*)held;
60
+
61
+ if (h->x_desc) { cudnnDestroyTensorDescriptor(h->x_desc); h->x_desc = 0; }
62
+ if (h->y_desc) { cudnnDestroyTensorDescriptor(h->y_desc); h->y_desc = 0; }
63
+ if (h->b_desc) { cudnnDestroyTensorDescriptor(h->b_desc); h->b_desc = 0; }
64
+ if (h->w_desc) { cudnnDestroyFilterDescriptor(h->w_desc); h->w_desc = 0; }
65
+ if (h->conv_desc) { cudnnDestroyConvolutionDescriptor(h->conv_desc); h->conv_desc = 0; }
66
+ // cuDNN is still reading the workspace, so the wait is the caller's to
67
+ // report. A free that raises from here would replace the exception being
68
+ // carried out, so it goes back the way an unwinding path has to.
69
+ cumo_cuda_runtime_return_scratch(h->workspace, h->workspace != NULL, &h->wait_status);
70
+ h->workspace = NULL;
71
+ return Qnil;
72
+ }
73
+
56
74
  // Lazily initialize cudnn handle, and cache it
57
75
  cudnnHandle_t
58
76
  cumo_cuda_cudnn_handle()
@@ -67,17 +67,11 @@ cumo_cuda_cudnn_GetConvTransposeOutDim(
67
67
  }
68
68
 
69
69
  cudnnStatus_t
70
- cumo_cuda_cudnn_CreateTensorDescriptor(
70
+ cumo_cuda_cudnn_CreateTensorDescriptorFromShape(
71
71
  cudnnTensorDescriptor_t *desc,
72
- VALUE a, cudnnDataType_t cudnn_dtype) {
72
+ int ndim, size_t *shape, cudnnDataType_t cudnn_dtype) {
73
73
  cudnnStatus_t status = CUDNN_STATUS_SUCCESS;
74
- cumo_narray_t *na;
75
- CumoGetNArray(a, na);
76
- int ndim = (int)(na->ndim);
77
- size_t *shape = na->shape;
78
74
 
79
- // only the shape is read: the descriptor is NCHW and the caller passes the
80
- // contiguous copy, so a non-contiguous a still describes what is read
81
75
  status = cudnnCreateTensorDescriptor(desc);
82
76
  if (status != CUDNN_STATUS_SUCCESS) return status;
83
77
 
@@ -120,6 +114,47 @@ cumo_cuda_cudnn_CreateTensorDescriptor(
120
114
  return status;
121
115
  }
122
116
 
117
+ // Only the shape is read: the descriptor is NCHW and the caller passes the
118
+ // contiguous copy, so a non-contiguous a still describes what is read.
119
+ cudnnStatus_t
120
+ cumo_cuda_cudnn_CreateTensorDescriptor(
121
+ cudnnTensorDescriptor_t *desc,
122
+ VALUE a, cudnnDataType_t cudnn_dtype) {
123
+ cumo_narray_t *na;
124
+ CumoGetNArray(a, na);
125
+ return cumo_cuda_cudnn_CreateTensorDescriptorFromShape(desc, (int)(na->ndim), na->shape, cudnn_dtype);
126
+ }
127
+
128
+ // The bias is one value a channel, and cuDNN broadcasts it from a descriptor
129
+ // shaped like the output. Shared because conv and conv_transpose add it alike.
130
+ cudnnStatus_t
131
+ cumo_cuda_cudnn_AddBias(cumo_cuda_cudnn_conv_run_t *r, cudnnDataType_t dtype, const void *one) {
132
+ size_t bias_shape[CUMO_NA_MAX_DIMENSION];
133
+ cumo_narray_t *nb;
134
+ cudnnStatus_t status;
135
+
136
+ if (!RTEST(r->b_cont)) return CUDNN_STATUS_SUCCESS;
137
+
138
+ CumoGetNArray(r->b_cont, nb);
139
+ bias_shape[0] = 1;
140
+ bias_shape[1] = nb->size;
141
+ for (size_t i = 0; i < r->ndim; ++i) {
142
+ bias_shape[i + 2] = 1;
143
+ }
144
+ status = cumo_cuda_cudnn_CreateTensorDescriptorFromShape(
145
+ &r->held.b_desc, (int)(r->ndim + 2), bias_shape, dtype);
146
+ if (status != CUDNN_STATUS_SUCCESS) return status;
147
+
148
+ return cudnnAddTensor(
149
+ r->handle,
150
+ one,
151
+ r->held.b_desc,
152
+ (void*)r->b_cont_ptr,
153
+ one,
154
+ r->held.y_desc,
155
+ (void*)r->y_ptr);
156
+ }
157
+
123
158
  cudnnStatus_t
124
159
  cumo_cuda_cudnn_CreateFilterDescriptor(
125
160
  cudnnFilterDescriptor_t *desc,
@@ -322,6 +357,17 @@ struct AlgoCacheEntry {
322
357
  size_t memory;
323
358
  cudnnMathType_t math_type;
324
359
  };
360
+
361
+ // Nothing may reach the C caller: it has no handler, so an escaping exception
362
+ // is std::terminate. The cache only saves a later search, so one that cannot
363
+ // grow leaves the answer this call already has.
364
+ template <typename Map>
365
+ void StoreAlgoCache(Map& map, const AlgoCacheKey& key, const typename Map::mapped_type& entry) {
366
+ try {
367
+ map[key] = entry;
368
+ } catch (...) {
369
+ }
370
+ }
325
371
  }
326
372
 
327
373
  using FwdAlgoCacheMap = std::unordered_map<AlgoCacheKey, AlgoCacheEntry<cudnnConvolutionFwdAlgo_t>, AlgoCacheKeyHash>;
@@ -416,7 +462,7 @@ cumo_cuda_cudnn_FindConvolutionForwardAlgorithm(
416
462
  perf_result,
417
463
  (void*)workspace,
418
464
  max_workspace_size);
419
- cumo_cuda_runtime_free(workspace);
465
+ cumo_cuda_runtime_return_scratch(workspace, 1, NULL);
420
466
  if (status != CUDNN_STATUS_SUCCESS) return status;
421
467
  // A search that answers success with nothing to report would leave
422
468
  // perf_result untouched, and its algo and math type are used below.
@@ -424,7 +470,7 @@ cumo_cuda_cudnn_FindConvolutionForwardAlgorithm(
424
470
  if (perf_result->status != CUDNN_STATUS_SUCCESS) return perf_result->status;
425
471
 
426
472
  // TODO: thread-safe
427
- algo_cache_map[key] = {perf_result->algo, perf_result->memory, perf_result->mathType};
473
+ StoreAlgoCache(algo_cache_map, key, {perf_result->algo, perf_result->memory, perf_result->mathType});
428
474
  return status;
429
475
  }
430
476
 
@@ -487,7 +533,7 @@ cumo_cuda_cudnn_FindConvolutionBackwardDataAlgorithm(
487
533
  perf_result,
488
534
  (void*)workspace,
489
535
  max_workspace_size);
490
- cumo_cuda_runtime_free(workspace);
536
+ cumo_cuda_runtime_return_scratch(workspace, 1, NULL);
491
537
  if (status != CUDNN_STATUS_SUCCESS) return status;
492
538
  // A search that answers success with nothing to report would leave
493
539
  // perf_result untouched, and its algo and math type are used below.
@@ -495,7 +541,7 @@ cumo_cuda_cudnn_FindConvolutionBackwardDataAlgorithm(
495
541
  if (perf_result->status != CUDNN_STATUS_SUCCESS) return perf_result->status;
496
542
 
497
543
  // TODO: thread-safe
498
- algo_cache_map[key] = {perf_result->algo, perf_result->memory, perf_result->mathType};
544
+ StoreAlgoCache(algo_cache_map, key, {perf_result->algo, perf_result->memory, perf_result->mathType});
499
545
  return status;
500
546
  }
501
547
 
@@ -558,7 +604,7 @@ cumo_cuda_cudnn_FindConvolutionBackwardFilterAlgorithm(
558
604
  perf_result,
559
605
  (void*)workspace,
560
606
  max_workspace_size);
561
- cumo_cuda_runtime_free(workspace);
607
+ cumo_cuda_runtime_return_scratch(workspace, 1, NULL);
562
608
  if (status != CUDNN_STATUS_SUCCESS) return status;
563
609
  // A search that answers success with nothing to report would leave
564
610
  // perf_result untouched, and its algo and math type are used below.
@@ -566,7 +612,7 @@ cumo_cuda_cudnn_FindConvolutionBackwardFilterAlgorithm(
566
612
  if (perf_result->status != CUDNN_STATUS_SUCCESS) return perf_result->status;
567
613
 
568
614
  // TODO: thread-safe
569
- algo_cache_map[key] = {perf_result->algo, perf_result->memory, perf_result->mathType};
615
+ StoreAlgoCache(algo_cache_map, key, {perf_result->algo, perf_result->memory, perf_result->mathType});
570
616
  return status;
571
617
  }
572
618
 
@@ -449,15 +449,17 @@ Init_cumo_cuda_driver()
449
449
 
450
450
  // A driver API call needs a current context, and the runtime API only
451
451
  // creates its primary one once an array operation happens, so this covers
452
- // the gap in between. Losing it is not fatal -- everything but a driver
453
- // call made before any array operation still works -- so a device that
454
- // refuses a context is left for that call to report. cuDeviceGet leaves
455
- // cuDevice untouched when it fails, hence the guard.
452
+ // the gap in between. It has to be the primary context and not one of our
453
+ // own: cudaSetDevice binds the primary one, so anything built in a
454
+ // different context stops working the moment a caller sets the device,
455
+ // including the events a cuBLAS handle keeps inside itself.
456
+ // Losing it is not fatal -- everything but a driver call made before any
457
+ // array operation still works -- so a device that refuses a context is
458
+ // left for that call to report. cuDeviceGet leaves cuDevice untouched
459
+ // when it fails, hence the guard.
456
460
  if (cuDeviceGet(&cuDevice, 0) == CUDA_SUCCESS) {
457
- #if defined(CUDA_VERSION) && CUDA_VERSION >= 13000
458
- cuCtxCreate(&context, NULL, 0, cuDevice);
459
- #else
460
- cuCtxCreate(&context, 0, cuDevice);
461
- #endif
461
+ if (cuDevicePrimaryCtxRetain(&context, cuDevice) == CUDA_SUCCESS) {
462
+ cuCtxSetCurrent(context);
463
+ }
462
464
  }
463
465
  }
@@ -2,6 +2,8 @@
2
2
  #include <assert.h>
3
3
  #include <cuda_runtime.h>
4
4
  #include "cumo/cuda/runtime.h"
5
+ #include "cumo/cuda/memory_pool.h"
6
+ #include "cumo/template_kernel.h"
5
7
 
6
8
  VALUE cumo_cuda_eRuntimeError;
7
9
  VALUE cumo_cuda_mRuntime;
@@ -20,6 +22,60 @@ cumo_cuda_runtime_check_kernel_launch(void)
20
22
  check_status(cudaGetLastError());
21
23
  }
22
24
 
25
+ // A pointer given twice would be freed twice, and the second free would reach
26
+ // past the pool to a block it still owns, so each one goes back once.
27
+ static void
28
+ release_held(char *p[5])
29
+ {
30
+ int i, j;
31
+ for (i = 0; i < 5; ++i) {
32
+ if (p[i] == NULL) { continue; }
33
+ for (j = 0; j < i; ++j) {
34
+ if (p[j] == p[i]) { break; }
35
+ }
36
+ if (j == i) { cumo_cuda_runtime_free_no_raise(p[i]); }
37
+ }
38
+ }
39
+
40
+ // What a launcher holding scratch has to call instead of the plain check, for a
41
+ // status it already took out of the slot: a library call in the window takes it
42
+ // first otherwise, and whoever reads it first is the only one who sees it.
43
+ // Raising is a longjmp, so the frees written below the check never run, and
44
+ // unwinding frees through the form that cannot raise so one failure cannot hide
45
+ // another.
46
+ void
47
+ cumo_cuda_runtime_check_taken_status_holding(int status, char *p0, char *p1, char *p2, char *p3, char *p4)
48
+ {
49
+ if ((cudaError_t)status != cudaSuccess) {
50
+ char *held[5];
51
+ held[0] = p0; held[1] = p1; held[2] = p2; held[3] = p3; held[4] = p4;
52
+ release_held(held);
53
+ }
54
+ check_status((cudaError_t)status);
55
+ }
56
+
57
+ // The slot is read once here rather than peeked and read again: the frees below
58
+ // can put a status of their own in it, and the launch is the one to report.
59
+ void
60
+ cumo_cuda_runtime_check_kernel_launch_holding(char *p0, char *p1, char *p2, char *p3, char *p4)
61
+ {
62
+ cumo_cuda_runtime_check_taken_status_holding((int)cudaGetLastError(), p0, p1, p2, p3, p4);
63
+ }
64
+
65
+ // How scratch goes back from a handler that may be unwinding: a free that
66
+ // raises there replaces the exception being carried, and leaves every free
67
+ // below it unrun. wait_for_stream asks for the wait a caller needs when the
68
+ // buffer is still being read, since the pool hands a freed chunk straight out.
69
+ void
70
+ cumo_cuda_runtime_return_scratch(char *ptr, int wait_for_stream, cudaError_t *status)
71
+ {
72
+ if (wait_for_stream) {
73
+ cudaError_t wait = cudaStreamSynchronize(0);
74
+ if (status != NULL && *status == cudaSuccess) { *status = wait; }
75
+ }
76
+ if (ptr != NULL) { cumo_cuda_runtime_free_no_raise(ptr); }
77
+ }
78
+
23
79
  int*
24
80
  cumo_cuda_runtime_error_flag_ptr(void)
25
81
  {
data/ext/cumo/cumo.c CHANGED
@@ -21,6 +21,7 @@ void Init_cumo_uint16();
21
21
  void Init_cumo_uint32();
22
22
  void Init_cumo_uint64();
23
23
  void Init_cumo_hfloat();
24
+ void Init_cumo_bfloat();
24
25
  void Init_cumo_sfloat();
25
26
  void Init_cumo_scomplex();
26
27
  void Init_cumo_dfloat();
@@ -31,6 +32,7 @@ void Init_cumo_dfloat_upcast();
31
32
  void Init_cumo_scomplex_upcast();
32
33
  void Init_cumo_sfloat_upcast();
33
34
  void Init_cumo_hfloat_upcast();
35
+ void Init_cumo_bfloat_upcast();
34
36
  void Init_cumo_int64_upcast();
35
37
  void Init_cumo_uint64_upcast();
36
38
  void Init_cumo_int32_upcast();
@@ -167,6 +169,7 @@ Init_cumo()
167
169
  Init_cumo_scomplex();
168
170
  Init_cumo_sfloat();
169
171
  Init_cumo_hfloat();
172
+ Init_cumo_bfloat();
170
173
 
171
174
  Init_cumo_int64();
172
175
  Init_cumo_uint64();
@@ -186,6 +189,7 @@ Init_cumo()
186
189
  Init_cumo_scomplex_upcast();
187
190
  Init_cumo_sfloat_upcast();
188
191
  Init_cumo_hfloat_upcast();
192
+ Init_cumo_bfloat_upcast();
189
193
  Init_cumo_int64_upcast();
190
194
  Init_cumo_uint64_upcast();
191
195
  Init_cumo_int32_upcast();
data/ext/cumo/extconf.rb CHANGED
@@ -65,6 +65,7 @@ narray/types/uint16
65
65
  narray/types/uint32
66
66
  narray/types/uint64
67
67
  narray/types/hfloat
68
+ narray/types/bfloat
68
69
  narray/types/sfloat
69
70
  narray/types/dfloat
70
71
  narray/types/scomplex
@@ -80,6 +81,7 @@ narray/types/uint16_kernel
80
81
  narray/types/uint32_kernel
81
82
  narray/types/uint64_kernel
82
83
  narray/types/hfloat_kernel
84
+ narray/types/bfloat_kernel
83
85
  narray/types/sfloat_kernel
84
86
  narray/types/dfloat_kernel
85
87
  narray/types/scomplex_kernel
@@ -442,7 +442,7 @@ static inline void cumo_bit_count_reduce(cumo_na_bit_reduction_arg_t arg, int in
442
442
  cumo_bit_detail::bit_count_partial_kernel<<<grid_size, block_size, shared_mem_size>>>(
443
443
  arg, p.ad, p.wa, invert, partial, p.n_split, p.chunk,
444
444
  (int)p.split_out_block_size, (int)p.split_reduce_block_size, p.unit_total_size);
445
- cumo_cuda_runtime_check_kernel_launch();
445
+ cumo_check_launch_holding(partial);
446
446
 
447
447
  cumo_na_reduction_arg_t combine;
448
448
  combine.in.ptr = reinterpret_cast<char*>(partial);
@@ -452,7 +452,7 @@ static inline void cumo_bit_count_reduce(cumo_na_bit_reduction_arg_t arg, int in
452
452
  combine.in_indexer.total_size = partial_total_size;
453
453
  combine.out = arg.out;
454
454
  combine.out_indexer = arg.out_indexer;
455
- cumo_reduce<uint64_t, uint64_t>(combine, cumo_bit_detail::BitCountImpl{});
455
+ cumo_reduce<uint64_t, uint64_t>(combine, cumo_bit_detail::BitCountImpl{}, reinterpret_cast<char*>(partial));
456
456
 
457
457
  cumo_cuda_runtime_free(reinterpret_cast<char*>(partial));
458
458
  }
@@ -483,12 +483,12 @@ static inline void cumo_bit_stat_reduce(cumo_na_bit_reduction_arg_t arg, int sta
483
483
  cumo_bit_detail::bit_count_partial_kernel<<<grid_size, block_size, shared_mem_size>>>(
484
484
  arg, p.ad, p.wa, 0, partial, p.n_split, p.chunk,
485
485
  (int)p.split_out_block_size, (int)p.split_reduce_block_size, p.unit_total_size);
486
- cumo_cuda_runtime_check_kernel_launch();
486
+ cumo_check_launch_holding(partial);
487
487
 
488
488
  int64_t combine_grid = (p.out_total_size + block_size - 1) / block_size;
489
489
  if (combine_grid > cumo_detail::max_grid_size) combine_grid = cumo_detail::max_grid_size;
490
490
  cumo_bit_detail::bit_stat_combine_kernel<<<combine_grid, block_size>>>(arg, p.ad, stat, partial, p.n_split);
491
- cumo_cuda_runtime_check_kernel_launch();
491
+ cumo_check_launch_holding(partial);
492
492
 
493
493
  cumo_cuda_runtime_free(reinterpret_cast<char*>(partial));
494
494
  }
@@ -519,12 +519,12 @@ static inline void cumo_bit_pred_reduce(cumo_na_bit_pred_reduction_arg_t arg, in
519
519
  cumo_bit_detail::bit_count_partial_kernel<<<grid_size, block_size, shared_mem_size>>>(
520
520
  arg, p.ad, p.wa, 0, partial, p.n_split, p.chunk,
521
521
  (int)p.split_out_block_size, (int)p.split_reduce_block_size, p.unit_total_size);
522
- cumo_cuda_runtime_check_kernel_launch();
522
+ cumo_check_launch_holding(partial);
523
523
 
524
524
  int64_t combine_grid = (p.out_total_size + block_size - 1) / block_size;
525
525
  if (combine_grid > cumo_detail::max_grid_size) combine_grid = cumo_detail::max_grid_size;
526
526
  cumo_bit_detail::bit_pred_combine_kernel<<<combine_grid, block_size>>>(arg, p.ad, all, partial, p.n_split);
527
- cumo_cuda_runtime_check_kernel_launch();
527
+ cumo_check_launch_holding(partial);
528
528
 
529
529
  cumo_cuda_runtime_free(reinterpret_cast<char*>(partial));
530
530
  }
@@ -97,9 +97,42 @@ cumo_cuda_cudnn_check_param_type(VALUE param, VALUE type, const char* name)
97
97
  void
98
98
  cumo_cuda_cudnn_check_status(cudnnStatus_t status);
99
99
 
100
+ // Raises, so the methods that clean up through a label take it before they
101
+ // hold a descriptor: a raise runs no label.
100
102
  cudnnHandle_t
101
103
  cumo_cuda_cudnn_handle();
102
104
 
105
+ // What a convolution holds while it works. A raise runs no error label, so the
106
+ // work goes under rb_ensure and the handler gives these back instead.
107
+ typedef struct {
108
+ cudnnTensorDescriptor_t x_desc;
109
+ cudnnTensorDescriptor_t y_desc;
110
+ cudnnTensorDescriptor_t b_desc;
111
+ cudnnFilterDescriptor_t w_desc;
112
+ cudnnConvolutionDescriptor_t conv_desc;
113
+ char *workspace;
114
+ cudaError_t wait_status;
115
+ } cumo_cuda_cudnn_conv_held_t;
116
+
117
+ // Takes the held as its argument so it can be handed to rb_ensure as it is.
118
+ VALUE
119
+ cumo_cuda_cudnn_release_conv_held(VALUE held);
120
+
121
+ // What conv and conv_transpose carry into the run the ensure above guards.
122
+ typedef struct {
123
+ cumo_cuda_cudnn_conv_held_t held;
124
+ cudnnHandle_t handle;
125
+ VALUE x_cont, w_cont, y, b_cont;
126
+ char *x_cont_ptr, *w_cont_ptr, *y_ptr, *b_cont_ptr;
127
+ size_t ndim;
128
+ int *int_stride, *int_pad;
129
+ cudnnStatus_t status;
130
+ } cumo_cuda_cudnn_conv_run_t;
131
+
132
+ // Adds nothing when the run carries no bias, so callers need no guard.
133
+ cudnnStatus_t
134
+ cumo_cuda_cudnn_AddBias(cumo_cuda_cudnn_conv_run_t *r, cudnnDataType_t dtype, const void *one);
135
+
103
136
  // VALUE is Ruby Array
104
137
  static inline void
105
138
  cumo_cuda_cudnn_get_int_ary(int* int_ary, VALUE ary, size_t ndim, int default_value)
@@ -178,6 +211,13 @@ cumo_cuda_cudnn_CreateTensorDescriptor(
178
211
  VALUE a,
179
212
  cudnnDataType_t cudnn_dtype);
180
213
 
214
+ cudnnStatus_t
215
+ cumo_cuda_cudnn_CreateTensorDescriptorFromShape(
216
+ cudnnTensorDescriptor_t *desc,
217
+ int ndim,
218
+ size_t *shape,
219
+ cudnnDataType_t cudnn_dtype);
220
+
181
221
  cudnnStatus_t
182
222
  cumo_cuda_cudnn_CreateFilterDescriptor(
183
223
  cudnnFilterDescriptor_t *desc,
@@ -28,6 +28,8 @@ cumo_cuda_runtime_check_status(cudaError_t status)
28
28
  }
29
29
  }
30
30
 
31
+ void cumo_cuda_runtime_return_scratch(char *ptr, int wait_for_stream, cudaError_t *status);
32
+
31
33
  static inline void
32
34
  cumo_cuda_runtime_device_synchronize(void)
33
35
  {
@@ -66,6 +66,15 @@ VALUE cumo_na_ndloop4(cumo_ndfunc_t *nf, void *ptr, VALUE args);
66
66
 
67
67
  VALUE cumo_na_ndloop_cast_narray_to_rarray(cumo_ndfunc_t *nf, VALUE nary, VALUE fmt);
68
68
  VALUE cumo_na_ndloop_store_rarray(cumo_ndfunc_t *nf, VALUE nary, VALUE rary);
69
+
70
+ // One device buffer for a whole walk, not one per row. A row that gives its
71
+ // own back has to wait for the kernel still reading it, and that wait costs
72
+ // more than everything else the row does. cumo_na_ndloop_store_rarray owns
73
+ // one and hands it over as the loop's opt_ptr.
74
+ // Opaque: an iterator asks for a buffer and reads nothing else.
75
+ typedef struct cumo_na_stage_pool cumo_na_stage_pool_t;
76
+
77
+ char *cumo_na_stage_pool_get(cumo_na_stage_pool_t *pool, size_t bytes);
69
78
  VALUE cumo_na_ndloop_store_rarray2(cumo_ndfunc_t *nf, VALUE nary, VALUE rary, VALUE opt);
70
79
  VALUE cumo_na_ndloop_inspect(VALUE nary, cumo_na_text_func_t func, VALUE opt);
71
80
  VALUE cumo_na_ndloop_with_index(cumo_ndfunc_t *nf, int argc, ...);