cumo 0.6.0 → 0.7.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -2
  3. data/README.md +133 -0
  4. data/bench/cumo_sync_probe.rb +71 -8
  5. data/ext/cumo/cuda/cublas.c +0 -40
  6. data/ext/cumo/cuda/cudnn.c +48 -0
  7. data/ext/cumo/cuda/cudnn_impl.cpp +91 -62
  8. data/ext/cumo/cuda/runtime.c +3 -4
  9. data/ext/cumo/cumo.c +34 -0
  10. data/ext/cumo/extconf.rb +2 -0
  11. data/ext/cumo/include/cumo/cuda/cudnn.h +21 -2
  12. data/ext/cumo/include/cumo/cuda/cumo_thrust.hpp +1 -0
  13. data/ext/cumo/include/cumo/cuda/runtime.h +28 -0
  14. data/ext/cumo/include/cumo/intern.h +4 -1
  15. data/ext/cumo/include/cumo/narray.h +59 -3
  16. data/ext/cumo/include/cumo/template.h +35 -10
  17. data/ext/cumo/include/cumo/template_kernel.h +96 -0
  18. data/ext/cumo/include/cumo/types/bit.h +3 -0
  19. data/ext/cumo/include/cumo/types/bit_kernel.h +3 -0
  20. data/ext/cumo/include/cumo/types/complex_macro.h +2 -0
  21. data/ext/cumo/include/cumo/types/complex_macro_kernel.h +2 -0
  22. data/ext/cumo/include/cumo/types/float_macro.h +2 -0
  23. data/ext/cumo/include/cumo/types/float_macro_kernel.h +2 -0
  24. data/ext/cumo/include/cumo/types/half_def.h +129 -0
  25. data/ext/cumo/include/cumo/types/half_def_kernel.h +28 -0
  26. data/ext/cumo/include/cumo/types/half_macro.h +188 -0
  27. data/ext/cumo/include/cumo/types/half_macro_kernel.h +198 -0
  28. data/ext/cumo/include/cumo/types/hfloat.h +22 -0
  29. data/ext/cumo/include/cumo/types/hfloat_kernel.h +16 -0
  30. data/ext/cumo/include/cumo/types/real_accum.h +2 -0
  31. data/ext/cumo/include/cumo/types/real_accum_kernel.h +2 -0
  32. data/ext/cumo/include/cumo/types/robj_macro.h +3 -0
  33. data/ext/cumo/include/cumo/types/xint_macro.h +3 -0
  34. data/ext/cumo/include/cumo/types/xint_macro_kernel.h +2 -0
  35. data/ext/cumo/include/cumo.h +2 -2
  36. data/ext/cumo/narray/data.c +49 -174
  37. data/ext/cumo/narray/gen/def/bit.rb +2 -0
  38. data/ext/cumo/narray/gen/def/dcomplex.rb +2 -0
  39. data/ext/cumo/narray/gen/def/dfloat.rb +8 -0
  40. data/ext/cumo/narray/gen/def/hfloat.rb +47 -0
  41. data/ext/cumo/narray/gen/def/int16.rb +2 -0
  42. data/ext/cumo/narray/gen/def/int32.rb +2 -0
  43. data/ext/cumo/narray/gen/def/int64.rb +2 -0
  44. data/ext/cumo/narray/gen/def/int8.rb +2 -0
  45. data/ext/cumo/narray/gen/def/robject.rb +2 -0
  46. data/ext/cumo/narray/gen/def/scomplex.rb +2 -0
  47. data/ext/cumo/narray/gen/def/sfloat.rb +8 -0
  48. data/ext/cumo/narray/gen/def/uint16.rb +3 -1
  49. data/ext/cumo/narray/gen/def/uint32.rb +3 -1
  50. data/ext/cumo/narray/gen/def/uint64.rb +2 -0
  51. data/ext/cumo/narray/gen/def/uint8.rb +3 -1
  52. data/ext/cumo/narray/gen/erbpp2.rb +6 -0
  53. data/ext/cumo/narray/gen/spec.rb +1 -0
  54. data/ext/cumo/narray/gen/tmpl/accum.c +1 -1
  55. data/ext/cumo/narray/gen/tmpl/accum_arg.c +1 -1
  56. data/ext/cumo/narray/gen/tmpl/accum_arg_kernel.cu +5 -5
  57. data/ext/cumo/narray/gen/tmpl/accum_binary_kernel.cu +14 -1
  58. data/ext/cumo/narray/gen/tmpl/accum_index.c +1 -1
  59. data/ext/cumo/narray/gen/tmpl/accum_index_kernel.cu +5 -5
  60. data/ext/cumo/narray/gen/tmpl/alloc_func.c +4 -7
  61. data/ext/cumo/narray/gen/tmpl/allocate.c +1 -2
  62. data/ext/cumo/narray/gen/tmpl/aref_cpu.c +1 -1
  63. data/ext/cumo/narray/gen/tmpl/batch_norm.c +11 -23
  64. data/ext/cumo/narray/gen/tmpl/batch_norm_backward.c +12 -24
  65. data/ext/cumo/narray/gen/tmpl/binary_kernel.cu +69 -2
  66. data/ext/cumo/narray/gen/tmpl/clip.c +3 -3
  67. data/ext/cumo/narray/gen/tmpl/cond_unary.c +1 -1
  68. data/ext/cumo/narray/gen/tmpl/conv.c +11 -17
  69. data/ext/cumo/narray/gen/tmpl/conv_grad_w.c +11 -17
  70. data/ext/cumo/narray/gen/tmpl/conv_transpose.c +11 -17
  71. data/ext/cumo/narray/gen/tmpl/cum.c +24 -1
  72. data/ext/cumo/narray/gen/tmpl/cum_kernel.cu +29 -4
  73. data/ext/cumo/narray/gen/tmpl/each.c +2 -3
  74. data/ext/cumo/narray/gen/tmpl/each_with_index.c +5 -21
  75. data/ext/cumo/narray/gen/tmpl/extract_cpu.c +1 -1
  76. data/ext/cumo/narray/gen/tmpl/extract_data.c +1 -1
  77. data/ext/cumo/narray/gen/tmpl/fill.c +1 -1
  78. data/ext/cumo/narray/gen/tmpl/fixed_batch_norm.c +9 -21
  79. data/ext/cumo/narray/gen/tmpl/float_accum_kernel.cu +42 -37
  80. data/ext/cumo/narray/gen/tmpl/format.c +1 -1
  81. data/ext/cumo/narray/gen/tmpl/format_to_a.c +1 -1
  82. data/ext/cumo/narray/gen/tmpl/gemm.c +78 -27
  83. data/ext/cumo/narray/gen/tmpl/init_class.c +0 -7
  84. data/ext/cumo/narray/gen/tmpl/init_upcast.c +6 -0
  85. data/ext/cumo/narray/gen/tmpl/inspect.c +1 -1
  86. data/ext/cumo/narray/gen/tmpl/lib.c +11 -1
  87. data/ext/cumo/narray/gen/tmpl/map_with_index.c +7 -14
  88. data/ext/cumo/narray/gen/tmpl/median.c +9 -47
  89. data/ext/cumo/narray/gen/tmpl/minmax.c +1 -1
  90. data/ext/cumo/narray/gen/tmpl/poly.c +1 -1
  91. data/ext/cumo/narray/gen/tmpl/pooling_backward.c +2 -14
  92. data/ext/cumo/narray/gen/tmpl/pooling_forward.c +2 -14
  93. data/ext/cumo/narray/gen/tmpl/qsort.c +0 -1
  94. data/ext/cumo/narray/gen/tmpl/rand.c +20 -2
  95. data/ext/cumo/narray/gen/tmpl/rand_kernel.cu +13 -0
  96. data/ext/cumo/narray/gen/tmpl/rand_norm.c +4 -4
  97. data/ext/cumo/narray/gen/tmpl/rand_norm_kernel.cu +7 -5
  98. data/ext/cumo/narray/gen/tmpl/real_accum_kernel.cu +83 -97
  99. data/ext/cumo/narray/gen/tmpl/sort.c +1 -1
  100. data/ext/cumo/narray/gen/tmpl/sort_index.c +6 -3
  101. data/ext/cumo/narray/gen/tmpl/store_array.c +21 -22
  102. data/ext/cumo/narray/gen/tmpl/store_bit.c +1 -1
  103. data/ext/cumo/narray/gen/tmpl/store_from.c +1 -1
  104. data/ext/cumo/narray/gen/tmpl/store_from_kernel.cu +37 -2
  105. data/ext/cumo/narray/gen/tmpl/to_a.c +1 -1
  106. data/ext/cumo/narray/gen/tmpl/unary.c +20 -3
  107. data/ext/cumo/narray/gen/tmpl/unary2.c +1 -1
  108. data/ext/cumo/narray/gen/tmpl/unary_s.c +1 -1
  109. data/ext/cumo/narray/gen/tmpl_bit/allocate.c +2 -3
  110. data/ext/cumo/narray/gen/tmpl_bit/aref_cpu.c +1 -1
  111. data/ext/cumo/narray/gen/tmpl_bit/bit_count_cpu.c +1 -1
  112. data/ext/cumo/narray/gen/tmpl_bit/each.c +2 -2
  113. data/ext/cumo/narray/gen/tmpl_bit/each_with_index.c +5 -20
  114. data/ext/cumo/narray/gen/tmpl_bit/extract_cpu.c +1 -1
  115. data/ext/cumo/narray/gen/tmpl_bit/format.c +1 -1
  116. data/ext/cumo/narray/gen/tmpl_bit/format_to_a.c +1 -1
  117. data/ext/cumo/narray/gen/tmpl_bit/inspect.c +1 -1
  118. data/ext/cumo/narray/gen/tmpl_bit/mask.c +38 -17
  119. data/ext/cumo/narray/gen/tmpl_bit/mask_kernel.cu +4 -4
  120. data/ext/cumo/narray/gen/tmpl_bit/store_array.c +10 -11
  121. data/ext/cumo/narray/gen/tmpl_bit/store_from.c +1 -1
  122. data/ext/cumo/narray/gen/tmpl_bit/to_a.c +1 -1
  123. data/ext/cumo/narray/gen/tmpl_bit/where.c +66 -13
  124. data/ext/cumo/narray/gen/tmpl_bit/where2.c +57 -20
  125. data/ext/cumo/narray/gen/tmpl_bit/where_kernel.cu +6 -6
  126. data/ext/cumo/narray/index.c +63 -18
  127. data/ext/cumo/narray/index_kernel.cu +15 -0
  128. data/ext/cumo/narray/math.c +2 -0
  129. data/ext/cumo/narray/narray.c +289 -183
  130. data/ext/cumo/narray/ndloop.c +91 -26
  131. data/ext/cumo/narray/sort_kernel.cu +39 -18
  132. data/ext/cumo/narray/struct.c +14 -17
  133. data/lib/cumo/cuda/cudnn.rb +1 -1
  134. data/lib/cumo/narray/extra.rb +11 -2
  135. data/test/bit_test.rb +147 -0
  136. data/test/cudnn_test.rb +132 -46
  137. data/test/extra_test.rb +1 -1
  138. data/test/hfloat_test.rb +717 -0
  139. data/test/math_test.rb +4 -1
  140. data/test/narray_alt_coverage_test.rb +7 -0
  141. data/test/narray_test.rb +1018 -11
  142. data/test/test_helper.rb +73 -0
  143. metadata +11 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d6b3e0decf69d07199a7930dc6a23d1a05a90ed044cf007203c78c28a8e9ea1
4
- data.tar.gz: dec0ca1f195b302919d30085fcc139911299ba075193b67cab9965907210092f
3
+ metadata.gz: 97c7f575209616cb9fa484e5dd4096bac11170aef18e99f1f6f733e599daa9af
4
+ data.tar.gz: 5efabd7aface13fac85dcef81fef54dd9bb22d3748666177ff221741a8419486
5
5
  SHA512:
6
- metadata.gz: e4f8d6204fca59288efad1ab5dec228d474de02ee3465f89afe7fc7440b456b0d46b78c92fb1e48e92063a2b3c385dedb8955aed3ddfa4411c13beb2f32a5417
7
- data.tar.gz: a397b427534b829b13f8bd4a4d6f8616d918bde834dc5c69a5611671ec942796d8615bd74d2564a2c3cd6cdaf9e572f4599e0704071f21a3c40394d0236b148d
6
+ metadata.gz: 314d2b224cb753761618b6bc833af4715d8d1cb2e1fd8de05b0ad65483ec940c31167997abe7cc76e2f056b12b442885731d8e6b4d4b47ec174c19c70c2326e9
7
+ data.tar.gz: 5a4909d952dbe926b52ccaf04010b3ffcf08617a2af710ff49a70764ff7e626ffddcef04ebcb4ca6b5aab8c7b14170b9b3328768a203dd7961d2fbab5c973002
data/CHANGELOG.md CHANGED
@@ -1,3 +1,45 @@
1
+ # 0.7.0 (2026/09/13)
2
+
3
+ Breaking changes:
4
+
5
+ * `cumo_narray_data_t` no longer carries `owned` and `cumo_narray_t` no longer carries `elmsz`. Both ship in `narray.h`, so an extension compiled against 0.6.0 has to be rebuilt. `NArray#free` also refuses a frozen array, as the other release paths already did (PR #421)
6
+ * `gemm` raises `RangeError` for the sizes cuBLAS takes as `int`, where they wrapped and the call read outside its operands (PR #399)
7
+ * Taking a pointer refuses an `allocate` that leaves the array without a buffer, or hands back one already holding data, and `to_binary` measures the array by its type rather than by the `ELEMENT_BYTE_SIZE` its class names (PR #393, PR #392)
8
+ * `store_binary` refuses a non-contiguous view, and a `Cumo::Bit` view that does not begin and end on a byte. It copies the string it is given, so `NArray#free` answers true after a frozen one where it answered false (PR #390, PR #389)
9
+ * `Cumo::Int8.upcast(Cumo::UInt8)` answers `Cumo::Int16` where it answered `Cumo::Int8`, and `Cumo::Bit.upcast(Cumo::RObject)` answers rather than `nil`, the tables having been built before the classes they name existed (PR #378)
10
+
11
+ Fixes:
12
+
13
+ * Fix a view constructor that raises partway leaving `stridx` unreadable for the free hook, and leaking the index array it had just built (PR #407)
14
+ * Fix `Cumo::Bit#each`, `map` and `map_with_index` reading what the block queued before the device had run it, 7 of the 18 dtype and method pairs having answered differently from numo (PR #401)
15
+ * Fix `gemm` reading and writing far outside its operands when a matrix holds more than 2**31 elements, the batch strides reaching cuBLAS as `int` where cuBLAS takes `long long int` (PR #399)
16
+ * Fix `median(nan: true)` answering with a number when a NaN is present, the host quicksort it used leaving NaN unordered (PR #398)
17
+ * Fix `where`, `where2` and a masked `aref` writing past the index array they sized, `allocate` being free to resize it between the count and the walk (PR #397)
18
+ * Fix `Cumo::Bit#each_with_index` writing in front of the counter array on a zero-dimensional array, which ordinary code reaches since indexing a Bit answers a zero-dimensional Bit (PR #396)
19
+ * Fix the segfaults an `allocate` that takes no buffer, reshapes the array, or claims its own element size leaves behind, and `to_binary` handing back a 16KB String from a 32 byte allocation (PR #393, PR #392)
20
+ * Fix `store_binary` writing at the start of the base rather than through the view, and pointing the array at a frozen String's bytes, which a compaction moves out from under it (PR #390, PR #389)
21
+ * Fix storing a Ruby Array of sub-narrays: the rows came back as the first one repeated, a row short in an outer axis copied past its own end, and a row with no elements stored the loop's flag as a value, 8.5us a row down to 1.6 (PR #384, PR #383, PR #382)
22
+ * Fix flattening a `Cumo::Bit` view that walks an axis backwards reading near address zero, which `to_a`, `where`, `where2` and `mask` all reach (PR #380)
23
+ * Fix a length-1 axis costing a view its contiguity, and `reshape!` on one carrying a negative stride walking out of the allocation; of 288 views measured, 92 more answer `contiguous?` and none fewer (PR #379, PR #376)
24
+ * Fix `:new` in front of an index-backed dimension reading and writing outside the array, only a trailing new axis having been recognized as one (PR #375)
25
+ * Fix `min`, `max`, `minmax` and `ptp` answering a finite number for an array of infinities, the nan-aware forms seeding with the largest finite value the dtype holds (PR #372)
26
+
27
+ Changes:
28
+
29
+ * Add `Cumo::HFloat`, the 16-bit float dtype the GPU has had since Maxwell: the four operations, the comparisons, the reductions, `Cumo::NMath`, `sort`, `rand`, `dot` through cuBLAS with a float compute type, and the cuDNN methods, which now reach the tensor cores. A 1024x1024 `dot` fell to the mulsum fallback and was 285x slower before it, and `cumsum` stays undefined for half until the scan carries a wider accumulator (issue #107, PR #371, PR #370, PR #368, PR #367, PR #366, PR #365)
30
+ * Copy a transposed 2-d view, and run an elementwise op with a transposed operand, through a 32x32 shared memory tile; on 16M SFloat the copy goes from 177 to 380 GB/s and the op from 247 to 396 GB/s (PR #416, PR #415)
31
+ * Point a derived view at the index arrays it would have copied, one device allocation and one copy per dimension, and let `reverse` and `diagonal` borrow the dimensions they only pass through; on a 2**20 index `transpose` goes from 394us to 0.3us, `reverse(1)` from 327us to 0.2us and `diagonal` from 329us to 0.1us (PR #412, PR #410)
32
+ * Let any device synchronize settle a view's index rather than only the one the index code makes, and record the epoch in every constructor that fills an index; storing sub-narrays into an index-backed view goes from 11.7-18.0us to 5.9-8.9us, and a reversed, expanded or diagonal index view stops waiting once per read (PR #414, PR #388)
33
+ * Wait for a sub-narray index only when the row binds one, rather than whenever any argument uses one; a store into an index-backed destination goes from 6.3us to 1.7us a row (PR #387)
34
+ * Reverse a view's index array with a kernel instead of a host loop that stopped the whole device first; a 2**20 index turns around in 480us rather than 1600us (PR #406)
35
+ * Wait for the device once per row of a host sort instead of once per recursion, a 20000 element row having cost 4003 waits; over 200000 elements `sort(nan: true)` goes from 18.95 to 13.03 ms, `median` from 18.15 to 12.41 and `sort_index` from 20.99 to 14.39 (PR #394)
36
+ * Warn when a view is built holding an index array that nothing owns, which leaks it when that view is the last reference and dangles when it is not (PR #413)
37
+ * Say once what the duplicated templates and kernels had each grown a copy of: the transpose tile walk, the `with_index` block argument and its 0-dimensional clamp, the Bit pointer rebase rule, the min and max rules, the minmax pair, and the byte size a packed bit array takes (PR #422, PR #418, PR #409, PR #408, PR #381, PR #377, PR #374, PR #373)
38
+ * Carry the sub-narray reach per dimension and through the row's own shape rather than through the value channel the store templates read (PR #386, PR #385)
39
+ * Separate the cuDNN scalar, compute and parameter types from the tensor's dtype, so a dtype whose compute type differs is named in `gen/def` rather than in eight templates (PR #369)
40
+ * Delete the `dot` and cuBLAS option code no build compiles, the branch that called a view with no strides contiguous, the second spelling of whether a view's offset reaches a char pointer, and the wait in front of a blocking copy that already queues behind the fill it waited for (PR #417, PR #411, PR #404, PR #403)
41
+ * Count the waits a call makes in the sync probe and probe the block walks, run `Cumo::HFloat` through the structural, math and alt-coverage suites, skip the boundary tests on a machine too small for them, and name the message the gemm row count tests are there for (PR #420, PR #419, PR #405, PR #402, PR #400)
42
+
1
43
  # 0.6.0 (2026/09/06)
2
44
 
3
45
  Breaking changes:
@@ -5,7 +47,7 @@ Breaking changes:
5
47
  * `divmod` and `%` floor the quotient and give the remainder the divisor's sign, as Ruby does, on every signed type: `Cumo::Int32[-7].divmod(3)` answers `[-3, 2]` where it answered `[-2, -1]`, and `-7 % 3` answers `2` rather than `-1`. Dividing a float by zero answers a quotient of `NaN` where it answered `Infinity`. Unsigned types are unchanged (PR #364)
6
48
  * A view whose base has since been given a smaller shape raises `RuntimeError` instead of reading past what the base holds (PR #363)
7
49
  * `marshal_load` and `initialize` leave an array unallocated when they take a new shape, so it reads as unallocated until something is stored in it, and both refuse a view. `initialize` also refuses a frozen array, as `marshal_load` already did (PR #361, PR #360, PR #355)
8
- * A range subscript longer than 2**31 answers the length it names instead of an empty view, and one longer than 2**32 no longer answers with the low 32 bits of it (PR #354)
50
+ * A range subscript longer than `2**31` answers the length it names instead of an empty view, and one longer than `2**32` no longer answers with the low 32 bits of it (PR #354)
9
51
  * `cov` rejects a 3-dimensional receiver or `y`, 2-dimensional or non-integer `fweights`, and any `ddof` but 0 or 1, where it answered from them; it warns and answers `nan` when the degrees of freedom come out at zero or below (PR #343)
10
52
  * `Cumo::Bit#extract` answers with a zero-dimensional `Cumo::Bit`, as every other dtype does, rather than a Ruby Integer read back through a synchronize (PR #341)
11
53
  * `batch_norm` and its backward reject an axis that names nothing in `x`, repeats a dimension, or is out of order, where they answered as though a different axis had been given (PR #336)
@@ -265,7 +307,7 @@ Fixes:
265
307
  * Backport: improve discrimination between int32/64
266
308
  * Backport: missing declaration of id_ge
267
309
  * Backport: support casting any object that responds to 'to_a'
268
- * Backport: fix the range of int32 as -2**31 .. 2**31-1
310
+ * Backport: fix the range of int32 as `-2**31 .. 2**31-1`
269
311
  * Backport: if int32_max is Bignum, it should be protected from GC
270
312
  * fix: handle compiler flags included in RbConfig::CONFIG properly (#157)
271
313
  * Backport: Added more percentile tests
data/README.md CHANGED
@@ -222,6 +222,126 @@ 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
+ ### Half Precision
226
+
227
+ `Cumo::HFloat`, also reachable as `Cumo::Float16`, holds IEEE binary16: one sign bit, five of exponent and ten of mantissa.
228
+ It exists for the two things half is good at, moving half as many bytes and reaching the tensor cores, and it promotes exactly as `Cumo::SFloat` does, so an integer array or a Ruby Float mixed into an expression stays half while anything wider takes over.
229
+
230
+ What it cannot hold is the thing to plan around.
231
+ Integers are exact only to 2048, and the largest finite value is 65504:
232
+
233
+ ```ruby
234
+ Cumo::HFloat[2049.0] #=> 2048.0
235
+ Cumo::HFloat[50257.0] #=> 50272.0
236
+ Cumo::HFloat[100000] #=> Infinity
237
+ ```
238
+
239
+ There is no exception on the way past the top; the value saturates, as it does in every other float type.
240
+ Anything that carries an index rather than a measurement has to be built in a wider type and cast afterwards.
241
+
242
+ Reductions do not inherit that limit, because they accumulate in single precision and round once at the end.
243
+ A sum of forty thousand ones is forty thousand, not the 2048 a half accumulator would stop at:
244
+
245
+ ```ruby
246
+ Cumo::HFloat.new(40_000).fill(1.0).sum #=> 40000.0
247
+ ```
248
+
249
+ `sum`, `mean`, `var`, `stddev`, `rms`, `mulsum`, `dot`, `gemm` and `cumsum` all widen this way.
250
+ `prod` and `cumprod` do not: a product leaves half's range long before it loses precision, so widening the accumulator would only hide the overflow.
251
+
252
+ The widening protects the accumulation, not the answer, which is still stored as half.
253
+ A variance above 65504 therefore saturates even though nothing overflowed while it was being computed, and the standard deviation of the same array is fine because the square root brings it back into range:
254
+
255
+ ```ruby
256
+ a = Cumo::HFloat[2000.0, -2000.0, 1000.0, -1000.0]
257
+ a.var #=> Infinity
258
+ a.stddev #=> 1826.0
259
+ ```
260
+
261
+ That distinction is what makes half usable in a transformer's layer normalization, and getting it wrong is the first thing to go wrong there.
262
+ Squaring overflows at 256, since 256 squared is already past the top, and a residual stream with one outlier feature reaches thousands.
263
+ 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
+ `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.
265
+ The quantity to check against 65504 is the largest variance a layer produces, not the largest activation in it.
266
+ 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
+ An activation that looks safe therefore says nothing about whether `var` overflows, in either direction.
268
+
269
+ A `dot` whose answer does not fit still saturates, since the result is stored back as half.
270
+ The accumulator itself is single precision, so scaling it down on the way out recovers the value:
271
+
272
+ ```ruby
273
+ a = Cumo::HFloat.ones(1, 1024)
274
+ b = Cumo::HFloat.new(1024, 1).fill(100.0)
275
+ a.dot(b) #=> Infinity
276
+ a.gemm(b, alpha: 0.001) #=> 102.375, the true 102400 scaled down
277
+ ```
278
+
279
+ #### What half is faster at
280
+
281
+ `gemm` reaches the tensor cores. Square matrices on an RTX 5070 Ti Laptop, median of three runs each:
282
+
283
+ ```
284
+ HFloat SFloat DFloat
285
+ 1024x1024 0.043 ms 49.7 TF 0.161 ms 13.3 TF 5.370 ms 0.40 TF
286
+ 2048x2048 0.362 ms 47.4 TF 1.360 ms 12.6 TF 43.46 ms 0.40 TF
287
+ 4096x4096 3.335 ms 41.2 TF 10.40 ms 13.2 TF 328.1 ms 0.42 TF
288
+ ```
289
+
290
+ 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.
291
+ It is the column count of either operand that matters, not the row count.
292
+ 1024x1024 times 1024x1024, with one dimension made odd at a time:
293
+
294
+ ```
295
+ all even M odd K odd N odd
296
+ HFloat 51.8 TF 49.2 TF 23.3 TF 23.3 TF
297
+ SFloat 11.9 TF - 11.6 TF 10.8 TF
298
+ ```
299
+
300
+ 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.
301
+
302
+ 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.
303
+ 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.
304
+ Pad the inner dimensions of a real matrix product; leave a gemv alone.
305
+
306
+ `conv` is a different story, and worth reading before reaching for half in a network.
307
+ cuDNN chooses its algorithm from the ones that fit in a scratch buffer, and the half algorithms that use the tensor cores ask for more than the default 8MB ceiling allows.
308
+ Left at the default, a half convolution is no faster than a single-precision one.
309
+ N=32, C=K=64, 56x56, 3x3:
310
+
311
+ ```
312
+ HFloat SFloat
313
+ CUMO_CUDNN_MAX_WORKSPACE_SIZE unset 1.13 ms 1.03 ms
314
+ CUMO_CUDNN_MAX_WORKSPACE_SIZE=268435456 0.56 ms 0.68 ms
315
+ ```
316
+
317
+ Tensor cores also want the channel counts to be multiples of eight, which the first layer of a network never satisfies.
318
+ That layer is still faster in half, but for the other reason:
319
+
320
+ ```
321
+ C=3, K=64, 56x56, 3x3 0.085 ms 0.170 ms
322
+ C=K=64, 56x56, 1x1 0.037 ms 0.120 ms
323
+ ```
324
+
325
+ Neither of those reaches a tensor core; they move half the bytes.
326
+
327
+ #### Batch normalization takes single-precision parameters
328
+
329
+ cuDNN derives the descriptor for the batch norm parameters from `x`, and widens it to float when `x` is half.
330
+ `gamma`, `beta`, `running_mean`, `running_var`, `mean` and `inv_std` are therefore `Cumo::SFloat` where `x` is `Cumo::HFloat`, and `batch_norm_backward` answers `gx` in half with `ggamma` and `gbeta` in single:
331
+
332
+ ```ruby
333
+ x = Cumo::HFloat.new(2, 4, 3, 3).seq
334
+ gamma = Cumo::SFloat.ones(4)
335
+ beta = Cumo::SFloat.zeros(4)
336
+ x.batch_norm(gamma, beta, axis: [0, 2, 3]) #=> Cumo::HFloat
337
+ ```
338
+
339
+ Passing half parameters raises `TypeError: gamma must be Cumo::SFloat, not Cumo::HFloat`.
340
+ Keeping the running statistics in single precision is what the arithmetic wants in any case: a momentum update is a long chain of small corrections, and eleven bits of mantissa lose them.
341
+
342
+ Gradients are the other place half runs out of room.
343
+ 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
+
225
345
  ### Select a GPU device ID
226
346
 
227
347
  Set the `CUDA_VISIBLE_DEVICES=id` environment variable, or
@@ -363,6 +483,19 @@ export CUMO_SHOW_WARNING=ON
363
483
  export CUMO_SHOW_WARNING_ONCE=OFF
364
484
  ```
365
485
 
486
+ ### Raise the cuDNN workspace ceiling
487
+
488
+ cuDNN picks a convolution algorithm by benchmarking the ones that fit in a scratch buffer, and the ceiling on that buffer is 8MB.
489
+ The fastest half precision algorithms, the ones that reach the tensor cores, ask for more than that and are left out of the search.
490
+ To let them in:
491
+
492
+ ```
493
+ export CUMO_CUDNN_MAX_WORKSPACE_SIZE=67108864
494
+ ```
495
+
496
+ The value is in bytes and only bounds the search; each convolution reserves what its chosen algorithm actually needs.
497
+ `Cumo::CUDA::CUDNN.max_workspace_size` reads back the value in force.
498
+
366
499
  ## Contributing
367
500
 
368
501
  Bug reports and pull requests are welcome on GitHub at https://github.com/sonots/cumo.
@@ -90,8 +90,8 @@ end
90
90
 
91
91
  PROBES = []
92
92
 
93
- def probe(group, name, &setup)
94
- PROBES << { group: group, name: name, setup: setup }
93
+ def probe(group, name, counts_only: false, &setup)
94
+ PROBES << { group: group, name: name, setup: setup, counts_only: counts_only }
95
95
  end
96
96
 
97
97
  # Reading data back is meant to synchronize. The question is whether it says so.
@@ -283,6 +283,47 @@ probe('compute', 'index aref a[idx, true]') do
283
283
  end
284
284
 
285
285
  # Host to device
286
+ # A block walks the array on the host and can queue work on it, so the walk has
287
+ # to wait between elements. It only has to wait when the block queued something,
288
+ # which is what the idle count says: nothing for a block that stays off the
289
+ # device, once per touched element for one that does not.
290
+ BLOCK_N = 32
291
+
292
+ probe('block', 'each (block off the device)', counts_only: true) do
293
+ a = Cumo::SFloat.new(BLOCK_N).seq
294
+ -> { a.each { |x| x } }
295
+ end
296
+
297
+ probe('block', 'each (block writes)', counts_only: true) do
298
+ a = Cumo::SFloat.new(BLOCK_N).seq
299
+ -> { i = 0; a.each { |_x| a[i] = 1; i += 1 } }
300
+ end
301
+
302
+ probe('block', 'each_with_index (block writes)', counts_only: true) do
303
+ a = Cumo::SFloat.new(BLOCK_N).seq
304
+ -> { a.each_with_index { |_x, i| a[i] = 1 } }
305
+ end
306
+
307
+ probe('block', 'map (block writes)', counts_only: true) do
308
+ a = Cumo::SFloat.new(BLOCK_N).seq
309
+ -> { i = 0; a.map { |x| a[i] = 1; i += 1; x } }
310
+ end
311
+
312
+ probe('block', 'map_with_index (block writes)', counts_only: true) do
313
+ a = Cumo::SFloat.new(BLOCK_N).seq
314
+ -> { a.map_with_index { |x, i| a[i] = 1; x } }
315
+ end
316
+
317
+ probe('block', 'Bit each (block writes)', counts_only: true) do
318
+ a = Cumo::Bit.new(BLOCK_N).fill(1)
319
+ -> { i = 0; a.each { |_x| a[i] = 0; i += 1 } }
320
+ end
321
+
322
+ probe('block', 'Bit each_with_index (block writes)', counts_only: true) do
323
+ a = Cumo::Bit.new(BLOCK_N).fill(1)
324
+ -> { a.each_with_index { |_x, i| a[i] = 0 } }
325
+ end
326
+
286
327
  probe('h2d', 'from_binary') do
287
328
  bin = Array.new(BIG_N / 4, 1.0).pack('f*')
288
329
  -> { Cumo::SFloat.from_binary(bin, [BIG_N / 4]) }
@@ -347,8 +388,8 @@ puts
347
388
 
348
389
  results = []
349
390
 
350
- puts format(' %-26s %10s %12s %-9s %s', 'probe', 'idle', 'behind queue', 'verdict', 'warning')
351
- puts " #{'-' * 84}"
391
+ puts format(' %-34s %10s %6s %12s %-9s %s', 'probe', 'idle', 'waits', 'behind queue', 'verdict', 'warning')
392
+ puts " #{'-' * 99}"
352
393
 
353
394
  current_group = nil
354
395
  PROBES.each do |p|
@@ -363,7 +404,22 @@ PROBES.each do |p|
363
404
  begin
364
405
  call = p[:setup].call
365
406
  if call.nil?
366
- puts format(' %-26s %10s %12s %-9s %s', p[:name], '-', '-', 'skip', '(no such API)')
407
+ puts format(' %-34s %10s %6s %12s %-9s %s', p[:name], '-', '-', '-', 'skip', '(no such API)')
408
+ next
409
+ end
410
+
411
+ # (1b) how many times it waits with nothing queued. With the warning left
412
+ # on every occurrence this is the wait count, not a yes or no.
413
+ sync
414
+ idle_text = capture_stdio { call.call }
415
+ idle_warns = idle_text.to_s.lines.grep(/warn|sync|Sync|WARN/i).size
416
+
417
+ if p[:counts_only]
418
+ # Timing these would measure the warning writes, not the call.
419
+ puts format(' %-34s %10s %6d %12s %-9s %s',
420
+ p[:name], '-', idle_warns, '-', 'waits', '-')
421
+ results << { name: p[:name], group: p[:group], sync: idle_warns.positive?,
422
+ warned: idle_warns.positive? }
367
423
  next
368
424
  end
369
425
 
@@ -412,12 +468,12 @@ PROBES.each do |p|
412
468
  format('x%d %s', warns.size, warns.first.to_s.strip[0, 40])
413
469
  end
414
470
 
415
- puts format(' %-26s %8.1f us %10.2f ms %-9s %s',
416
- p[:name], idle * 1e6, blocked * 1e3,
471
+ puts format(' %-34s %8.1f us %6d %10.2f ms %-9s %s',
472
+ p[:name], idle * 1e6, idle_warns, blocked * 1e3,
417
473
  is_sync ? 'SYNC' : 'async', note)
418
474
  rescue StandardError, NotImplementedError => e
419
475
  msg = e.message.to_s.split("\n").first.to_s[0, 40]
420
- puts format(' %-26s %10s %12s %-9s %s', p[:name], '-', '-', 'ERROR', "#{e.class}: #{msg}")
476
+ puts format(' %-34s %10s %6s %12s %-9s %s', p[:name], '-', '-', '-', 'ERROR', "#{e.class}: #{msg}")
421
477
  end
422
478
  end
423
479
 
@@ -440,3 +496,10 @@ puts ' The first line is what this is for: those calls stall the pipeline and'
440
496
  puts ' CUMO_SHOW_WARNING will not tell you. Either add the warning or, where'
441
497
  puts ' the synchronization is avoidable, remove it.'
442
498
  puts ' A SYNC in the meta group is waste with nothing to show for it.'
499
+ puts
500
+ puts ' The block group counts waits rather than timing them: a walk that hands'
501
+ puts ' every element to a Ruby block has to wait between elements, since the'
502
+ puts ' block can queue work on the array being walked. A block that stays off'
503
+ puts ' the device should cost none, and one that writes should cost about one'
504
+ puts ' per element. A count of one there means the walk waits once for the whole'
505
+ puts ' row, which is what made Bit#each and map answer differently from numo.'
@@ -76,46 +76,6 @@ cumo_cuda_cublas_option_value(VALUE value, VALUE default_value)
76
76
  return value;
77
77
  }
78
78
 
79
- #if 0
80
- cublasOperation_t
81
- cumo_cuda_cublas_option_trans(VALUE trans)
82
- {
83
- int opt;
84
- char *ptr;
85
-
86
- switch(TYPE(trans)) {
87
- case T_NIL:
88
- case T_UNDEF:
89
- case T_FALSE:
90
- return CUBLAS_OP_N;
91
- case T_TRUE:
92
- return CUBLAS_OP_T;
93
- case T_FIXNUM:
94
- opt = FIX2INT(trans);
95
- if (opt >= CUBLAS_OP_N && opt <= CUBLAS_OP_C) {
96
- return opt;
97
- }
98
- break;
99
- case T_SYMBOL:
100
- trans = rb_sym2str(trans);
101
- case T_STRING:
102
- ptr = RSTRING_PTR(trans);
103
- if (RSTRING_LEN(trans) > 0) {
104
- switch(ptr[0]){
105
- case 'N': case 'n':
106
- return CUBLAS_OP_N;
107
- case 'T': case 't':
108
- return CUBLAS_OP_T;
109
- case 'C': case 'c':
110
- return CUBLAS_OP_C;
111
- }
112
- }
113
- break;
114
- }
115
- rb_raise(rb_eArgError, "invalid value for cublasOperation_t");
116
- return 0;
117
- }
118
- #endif
119
79
 
120
80
  void
121
81
  Init_cumo_cuda_cublas(void)
@@ -1,6 +1,9 @@
1
1
  #include "cumo/cuda/cudnn.h"
2
2
 
3
3
  #include <assert.h>
4
+ #include <stdlib.h>
5
+ #include <stdint.h>
6
+ #include <errno.h>
4
7
  #include <ruby.h>
5
8
  #include "cumo/narray.h"
6
9
  #include "cumo/template.h"
@@ -21,6 +24,35 @@ cumo_cuda_cudnn_check_status(cudnnStatus_t status)
21
24
  }
22
25
  }
23
26
 
27
+ static size_t cudnn_max_workspace_size = CUMO_CUDA_CUDNN_DEFAULT_MAX_WORKSPACE_SIZE;
28
+
29
+ size_t
30
+ cumo_cuda_cudnn_max_workspace_size()
31
+ {
32
+ return cudnn_max_workspace_size;
33
+ }
34
+
35
+ static void
36
+ init_max_workspace_size(void)
37
+ {
38
+ const char* env = getenv("CUMO_CUDNN_MAX_WORKSPACE_SIZE");
39
+ char* end = NULL;
40
+ unsigned long long v;
41
+
42
+ if (env == NULL || *env == '\0') return;
43
+ // strtoull takes a leading minus and wraps it, so reject the sign itself
44
+ if (*env == '-' || *env == '+') goto BAD;
45
+ errno = 0;
46
+ v = strtoull(env, &end, 10);
47
+ if (errno != 0 || end == env || *end != '\0' || v == 0 || v > (unsigned long long)SIZE_MAX) goto BAD;
48
+ cudnn_max_workspace_size = (size_t)v;
49
+ return;
50
+
51
+ BAD:
52
+ rb_warn("CUMO_CUDNN_MAX_WORKSPACE_SIZE=%s is not a positive byte count, using %"PRIuSIZE,
53
+ env, cudnn_max_workspace_size);
54
+ }
55
+
24
56
  // Lazily initialize cudnn handle, and cache it
25
57
  cudnnHandle_t
26
58
  cumo_cuda_cudnn_handle()
@@ -51,6 +83,20 @@ cumo_cuda_cudnn_handle()
51
83
 
52
84
  @return [Boolean] Returns true if cuDNN is available
53
85
  */
86
+ #ifdef CUDNN_FOUND
87
+ /*
88
+ Returns the ceiling cuDNN may use when it searches for a convolution
89
+ algorithm, set by CUMO_CUDNN_MAX_WORKSPACE_SIZE.
90
+
91
+ @return [Integer] bytes
92
+ */
93
+ static VALUE
94
+ rb_cudnn_max_workspace_size(VALUE self)
95
+ {
96
+ return SIZET2NUM(cumo_cuda_cudnn_max_workspace_size());
97
+ }
98
+ #endif // CUDNN_FOUND
99
+
54
100
  static VALUE
55
101
  rb_cudnn_available_p(VALUE self)
56
102
  {
@@ -76,6 +122,8 @@ Init_cumo_cuda_cudnn(void)
76
122
 
77
123
  rb_define_singleton_method(mCUDNN, "available?", rb_cudnn_available_p, 0);
78
124
  #ifdef CUDNN_FOUND
125
+ init_max_workspace_size();
126
+ rb_define_singleton_method(mCUDNN, "max_workspace_size", rb_cudnn_max_workspace_size, 0);
79
127
  rb_define_const(mCUDNN, "CUDNN_POOLING_MAX", INT2NUM(CUDNN_POOLING_MAX));
80
128
  rb_define_const(mCUDNN, "CUDNN_POOLING_MAX_DETERMINISTIC", INT2NUM(CUDNN_POOLING_MAX_DETERMINISTIC));
81
129
  rb_define_const(mCUDNN, "CUDNN_POOLING_AVERAGE_COUNT_INCLUDE_PADDING", INT2NUM(CUDNN_POOLING_AVERAGE_COUNT_INCLUDE_PADDING));