cumo 0.5.11 → 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 (174) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -1
  3. data/README.md +133 -0
  4. data/bench/cumo_shape_probe.rb +264 -0
  5. data/bench/cumo_sync_probe.rb +71 -8
  6. data/ext/cumo/cuda/cublas.c +0 -40
  7. data/ext/cumo/cuda/cudnn.c +48 -0
  8. data/ext/cumo/cuda/cudnn_impl.cpp +100 -63
  9. data/ext/cumo/cuda/memory_pool_impl_test.cpp +4 -1
  10. data/ext/cumo/cuda/nvrtc.c +1 -1
  11. data/ext/cumo/cuda/runtime.c +3 -4
  12. data/ext/cumo/cumo.c +34 -0
  13. data/ext/cumo/depend.erb +7 -0
  14. data/ext/cumo/extconf.rb +2 -0
  15. data/ext/cumo/include/cumo/bit_reduce_kernel.h +119 -13
  16. data/ext/cumo/include/cumo/check.h +51 -0
  17. data/ext/cumo/include/cumo/cuda/cudnn.h +47 -43
  18. data/ext/cumo/include/cumo/cuda/cumo_thrust.hpp +1 -0
  19. data/ext/cumo/include/cumo/cuda/runtime.h +28 -0
  20. data/ext/cumo/include/cumo/indexer.h +29 -8
  21. data/ext/cumo/include/cumo/intern.h +4 -1
  22. data/ext/cumo/include/cumo/narray.h +69 -3
  23. data/ext/cumo/include/cumo/ndloop.h +1 -0
  24. data/ext/cumo/include/cumo/reduce_kernel.h +133 -50
  25. data/ext/cumo/include/cumo/template.h +35 -10
  26. data/ext/cumo/include/cumo/template_kernel.h +96 -0
  27. data/ext/cumo/include/cumo/types/bit.h +3 -0
  28. data/ext/cumo/include/cumo/types/bit_kernel.h +3 -0
  29. data/ext/cumo/include/cumo/types/complex_macro.h +2 -0
  30. data/ext/cumo/include/cumo/types/complex_macro_kernel.h +2 -0
  31. data/ext/cumo/include/cumo/types/float_macro.h +17 -2
  32. data/ext/cumo/include/cumo/types/float_macro_kernel.h +17 -2
  33. data/ext/cumo/include/cumo/types/half_def.h +129 -0
  34. data/ext/cumo/include/cumo/types/half_def_kernel.h +28 -0
  35. data/ext/cumo/include/cumo/types/half_macro.h +188 -0
  36. data/ext/cumo/include/cumo/types/half_macro_kernel.h +198 -0
  37. data/ext/cumo/include/cumo/types/hfloat.h +22 -0
  38. data/ext/cumo/include/cumo/types/hfloat_kernel.h +16 -0
  39. data/ext/cumo/include/cumo/types/int_macro.h +20 -0
  40. data/ext/cumo/include/cumo/types/int_macro_kernel.h +20 -0
  41. data/ext/cumo/include/cumo/types/real_accum.h +2 -0
  42. data/ext/cumo/include/cumo/types/real_accum_kernel.h +2 -0
  43. data/ext/cumo/include/cumo/types/robj_macro.h +11 -3
  44. data/ext/cumo/include/cumo/types/xint_macro.h +3 -0
  45. data/ext/cumo/include/cumo/types/xint_macro_kernel.h +2 -0
  46. data/ext/cumo/include/cumo.h +2 -2
  47. data/ext/cumo/narray/data.c +70 -199
  48. data/ext/cumo/narray/data_kernel.cu +113 -0
  49. data/ext/cumo/narray/gen/def/bit.rb +2 -0
  50. data/ext/cumo/narray/gen/def/dcomplex.rb +2 -0
  51. data/ext/cumo/narray/gen/def/dfloat.rb +8 -0
  52. data/ext/cumo/narray/gen/def/hfloat.rb +47 -0
  53. data/ext/cumo/narray/gen/def/int16.rb +2 -0
  54. data/ext/cumo/narray/gen/def/int32.rb +2 -0
  55. data/ext/cumo/narray/gen/def/int64.rb +2 -0
  56. data/ext/cumo/narray/gen/def/int8.rb +2 -0
  57. data/ext/cumo/narray/gen/def/robject.rb +2 -0
  58. data/ext/cumo/narray/gen/def/scomplex.rb +2 -0
  59. data/ext/cumo/narray/gen/def/sfloat.rb +8 -0
  60. data/ext/cumo/narray/gen/def/uint16.rb +3 -1
  61. data/ext/cumo/narray/gen/def/uint32.rb +3 -1
  62. data/ext/cumo/narray/gen/def/uint64.rb +2 -0
  63. data/ext/cumo/narray/gen/def/uint8.rb +3 -1
  64. data/ext/cumo/narray/gen/erbpp2.rb +6 -0
  65. data/ext/cumo/narray/gen/narray_def.rb +5 -0
  66. data/ext/cumo/narray/gen/spec.rb +11 -0
  67. data/ext/cumo/narray/gen/tmpl/accum.c +1 -1
  68. data/ext/cumo/narray/gen/tmpl/accum_arg.c +1 -1
  69. data/ext/cumo/narray/gen/tmpl/accum_arg_kernel.cu +5 -5
  70. data/ext/cumo/narray/gen/tmpl/accum_binary_kernel.cu +14 -1
  71. data/ext/cumo/narray/gen/tmpl/accum_index.c +1 -1
  72. data/ext/cumo/narray/gen/tmpl/accum_index_kernel.cu +5 -5
  73. data/ext/cumo/narray/gen/tmpl/alloc_func.c +4 -7
  74. data/ext/cumo/narray/gen/tmpl/allocate.c +1 -2
  75. data/ext/cumo/narray/gen/tmpl/aref_cpu.c +1 -1
  76. data/ext/cumo/narray/gen/tmpl/batch_norm.c +27 -36
  77. data/ext/cumo/narray/gen/tmpl/batch_norm_backward.c +26 -35
  78. data/ext/cumo/narray/gen/tmpl/binary.c +3 -3
  79. data/ext/cumo/narray/gen/tmpl/binary2.c +1 -1
  80. data/ext/cumo/narray/gen/tmpl/binary_kernel.cu +69 -2
  81. data/ext/cumo/narray/gen/tmpl/binary_s.c +3 -3
  82. data/ext/cumo/narray/gen/tmpl/clip.c +9 -9
  83. data/ext/cumo/narray/gen/tmpl/cond_binary.c +3 -3
  84. data/ext/cumo/narray/gen/tmpl/cond_unary.c +2 -2
  85. data/ext/cumo/narray/gen/tmpl/conv.c +19 -25
  86. data/ext/cumo/narray/gen/tmpl/conv_grad_w.c +21 -27
  87. data/ext/cumo/narray/gen/tmpl/conv_transpose.c +21 -27
  88. data/ext/cumo/narray/gen/tmpl/cum.c +24 -1
  89. data/ext/cumo/narray/gen/tmpl/cum_kernel.cu +29 -4
  90. data/ext/cumo/narray/gen/tmpl/each.c +2 -3
  91. data/ext/cumo/narray/gen/tmpl/each_with_index.c +5 -21
  92. data/ext/cumo/narray/gen/tmpl/ewcomp.c +1 -1
  93. data/ext/cumo/narray/gen/tmpl/extract_cpu.c +1 -1
  94. data/ext/cumo/narray/gen/tmpl/extract_data.c +1 -1
  95. data/ext/cumo/narray/gen/tmpl/fill.c +2 -2
  96. data/ext/cumo/narray/gen/tmpl/fixed_batch_norm.c +18 -27
  97. data/ext/cumo/narray/gen/tmpl/float_accum_kernel.cu +42 -37
  98. data/ext/cumo/narray/gen/tmpl/format.c +1 -1
  99. data/ext/cumo/narray/gen/tmpl/format_to_a.c +1 -1
  100. data/ext/cumo/narray/gen/tmpl/frexp.c +1 -1
  101. data/ext/cumo/narray/gen/tmpl/gemm.c +78 -27
  102. data/ext/cumo/narray/gen/tmpl/init_class.c +0 -7
  103. data/ext/cumo/narray/gen/tmpl/init_upcast.c +6 -0
  104. data/ext/cumo/narray/gen/tmpl/inspect.c +1 -1
  105. data/ext/cumo/narray/gen/tmpl/lib.c +11 -1
  106. data/ext/cumo/narray/gen/tmpl/map_with_index.c +7 -14
  107. data/ext/cumo/narray/gen/tmpl/median.c +9 -47
  108. data/ext/cumo/narray/gen/tmpl/minmax.c +1 -1
  109. data/ext/cumo/narray/gen/tmpl/poly.c +2 -2
  110. data/ext/cumo/narray/gen/tmpl/pooling_backward.c +6 -18
  111. data/ext/cumo/narray/gen/tmpl/pooling_forward.c +6 -18
  112. data/ext/cumo/narray/gen/tmpl/pow.c +6 -6
  113. data/ext/cumo/narray/gen/tmpl/qsort.c +0 -1
  114. data/ext/cumo/narray/gen/tmpl/rand.c +20 -2
  115. data/ext/cumo/narray/gen/tmpl/rand_kernel.cu +13 -0
  116. data/ext/cumo/narray/gen/tmpl/rand_norm.c +4 -4
  117. data/ext/cumo/narray/gen/tmpl/rand_norm_kernel.cu +7 -5
  118. data/ext/cumo/narray/gen/tmpl/real_accum_kernel.cu +155 -96
  119. data/ext/cumo/narray/gen/tmpl/set2.c +1 -1
  120. data/ext/cumo/narray/gen/tmpl/sort.c +1 -1
  121. data/ext/cumo/narray/gen/tmpl/sort_index.c +6 -3
  122. data/ext/cumo/narray/gen/tmpl/store_array.c +21 -22
  123. data/ext/cumo/narray/gen/tmpl/store_bit.c +2 -2
  124. data/ext/cumo/narray/gen/tmpl/store_from.c +2 -2
  125. data/ext/cumo/narray/gen/tmpl/store_from_kernel.cu +37 -2
  126. data/ext/cumo/narray/gen/tmpl/to_a.c +1 -1
  127. data/ext/cumo/narray/gen/tmpl/unary.c +21 -4
  128. data/ext/cumo/narray/gen/tmpl/unary2.c +2 -2
  129. data/ext/cumo/narray/gen/tmpl/unary_ret2.c +1 -1
  130. data/ext/cumo/narray/gen/tmpl/unary_s.c +2 -2
  131. data/ext/cumo/narray/gen/tmpl_bit/allocate.c +2 -3
  132. data/ext/cumo/narray/gen/tmpl_bit/aref_cpu.c +1 -1
  133. data/ext/cumo/narray/gen/tmpl_bit/binary.c +1 -1
  134. data/ext/cumo/narray/gen/tmpl_bit/bit_count_cpu.c +1 -1
  135. data/ext/cumo/narray/gen/tmpl_bit/bit_reduce.c +4 -1
  136. data/ext/cumo/narray/gen/tmpl_bit/bit_stat.c +36 -0
  137. data/ext/cumo/narray/gen/tmpl_bit/bit_stat_kernel.cu +4 -0
  138. data/ext/cumo/narray/gen/tmpl_bit/each.c +2 -2
  139. data/ext/cumo/narray/gen/tmpl_bit/each_with_index.c +5 -20
  140. data/ext/cumo/narray/gen/tmpl_bit/extract.c +13 -27
  141. data/ext/cumo/narray/gen/tmpl_bit/extract_cpu.c +1 -1
  142. data/ext/cumo/narray/gen/tmpl_bit/fill.c +1 -1
  143. data/ext/cumo/narray/gen/tmpl_bit/format.c +1 -1
  144. data/ext/cumo/narray/gen/tmpl_bit/format_to_a.c +1 -1
  145. data/ext/cumo/narray/gen/tmpl_bit/inspect.c +1 -1
  146. data/ext/cumo/narray/gen/tmpl_bit/mask.c +38 -17
  147. data/ext/cumo/narray/gen/tmpl_bit/mask_kernel.cu +4 -4
  148. data/ext/cumo/narray/gen/tmpl_bit/store_array.c +10 -11
  149. data/ext/cumo/narray/gen/tmpl_bit/store_bit.c +1 -1
  150. data/ext/cumo/narray/gen/tmpl_bit/store_from.c +2 -2
  151. data/ext/cumo/narray/gen/tmpl_bit/to_a.c +1 -1
  152. data/ext/cumo/narray/gen/tmpl_bit/unary.c +1 -1
  153. data/ext/cumo/narray/gen/tmpl_bit/where.c +66 -13
  154. data/ext/cumo/narray/gen/tmpl_bit/where2.c +57 -20
  155. data/ext/cumo/narray/gen/tmpl_bit/where_kernel.cu +6 -6
  156. data/ext/cumo/narray/index.c +83 -26
  157. data/ext/cumo/narray/index_kernel.cu +15 -0
  158. data/ext/cumo/narray/math.c +2 -0
  159. data/ext/cumo/narray/narray.c +421 -178
  160. data/ext/cumo/narray/ndloop.c +158 -26
  161. data/ext/cumo/narray/sort_kernel.cu +39 -18
  162. data/ext/cumo/narray/struct.c +14 -17
  163. data/lib/cumo/cuda/cudnn.rb +1 -1
  164. data/lib/cumo/narray/extra.rb +130 -26
  165. data/test/bit_test.rb +290 -0
  166. data/test/cudnn_test.rb +208 -34
  167. data/test/cumo_test.rb +1 -0
  168. data/test/extra_test.rb +75 -7
  169. data/test/hfloat_test.rb +717 -0
  170. data/test/math_test.rb +4 -1
  171. data/test/narray_alt_coverage_test.rb +7 -0
  172. data/test/narray_test.rb +1865 -10
  173. data/test/test_helper.rb +73 -0
  174. metadata +15 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b345c4e036212d334600041165fe79e533ef0e1dd450e22b1288f5e6d6214ffa
4
- data.tar.gz: af60f78cdd2f39a96e8a183eff02a26c26d8977560ba148643c55df5961c2bf2
3
+ metadata.gz: 97c7f575209616cb9fa484e5dd4096bac11170aef18e99f1f6f733e599daa9af
4
+ data.tar.gz: 5efabd7aface13fac85dcef81fef54dd9bb22d3748666177ff221741a8419486
5
5
  SHA512:
6
- metadata.gz: 361042e17c2f498b8e4f5ddc76bbd119688a6a3a3ac176307973fd51c75b332d4d831a82c0fc0e8a3b28be98bfbe50d507ec67d517c609c2342caf57d94d9c41
7
- data.tar.gz: 53da42c447fe31812654e1ab0566f5e553f781daaf75b97bb1c36630b8dc3ed195704002990bc4a2a9f4fdd6e26e92c2d5905ff64da0e0f058b01dae20ae3dec
6
+ metadata.gz: 314d2b224cb753761618b6bc833af4715d8d1cb2e1fd8de05b0ad65483ec940c31167997abe7cc76e2f056b12b442885731d8e6b4d4b47ec174c19c70c2326e9
7
+ data.tar.gz: 5a4909d952dbe926b52ccaf04010b3ffcf08617a2af710ff49a70764ff7e626ffddcef04ebcb4ca6b5aab8c7b14170b9b3328768a203dd7961d2fbab5c973002
data/CHANGELOG.md CHANGED
@@ -1,3 +1,78 @@
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
+
43
+ # 0.6.0 (2026/09/06)
44
+
45
+ Breaking changes:
46
+
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)
48
+ * A view whose base has since been given a smaller shape raises `RuntimeError` instead of reading past what the base holds (PR #363)
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)
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)
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)
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)
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)
54
+
55
+ Fixes:
56
+
57
+ * Fix `RObject#divmod` reading the pair an element's `divmod` answered without looking at it: a bare Integer or a String took the process down, and a shorter Array handed back the words past its end as the quotient and the remainder (PR #359)
58
+ * Fix an out-of-bounds read when `initialize` is given a new shape through `send`, which left the buffer sized for the old one: 4096 elements were read out of 8, and the same on an `Cumo::RObject` crashed at the next collection (PR #361)
59
+ * Fix a memory leak when an Array subscript is rejected, the host buffer it is staged in being freed only after the loop that fills it, and the memory a subscript piles up before the collector takes it back (PR #357, PR #358)
60
+ * Fix `marshal_load` writing past a buffer sized for the shape it replaces, and the double free that came of releasing a buffer noted down before the shape's own `to_int` had run (PR #355, PR #356)
61
+ * Fix reading past a Ruby array or string that a conversion shrank underneath the walk: a subscript, a shape, an axis list, a marshal array and the string behind `from_binary` were each measured once and walked afterwards, and `to_int` is free to empty any of them (PR #353)
62
+ * Fix `Cumo::Bit#swap_byte` walking one byte per bit, eight times past the end of the packed buffer; `hton`, `to_network` and `to_swapped` reach the same loop (PR #352)
63
+
64
+ Changes:
65
+
66
+ * Add `argsort`, which answers positions along an axis rather than into the flattened array as `sort_index` does (PR #342)
67
+ * Add `mean`, `var`, `stddev` and `rms` to the integer types and to `Cumo::Bit`, which had only the float ones (PR #344, PR #345)
68
+ * Walk an elementwise function along the memory of the operand it writes rather than the shape it was given; on a transposed 1024x1024 SFloat view `a.inplace + 1.0` goes from 1180.6us to 78.3us (PR #351)
69
+ * Reduce along the contiguous axis with a contiguous group of threads, keep a reduction's operands out of local memory, and give a short strided row more elements per thread; `[1024, 4, 1024].sum(axis: 1)` goes from 142.0us to 23.4us and the same `mulsum` from 675.3us to 30.9us (PR #350, PR #347)
70
+ * Split a flat index in 32 bits where the array fits in them, the 64-bit division being a software routine on the GPU; a 4M SFloat `a[true, 1..-2] * 2.0` goes from 60.6us to 38.7us (PR #348)
71
+ * Reverse the bytes of an array in a kernel instead of on the host; a 4M SFloat `swap_byte` goes from 5617.0us to 13.0us (PR #335)
72
+ * Add `bench/cumo_shape_probe.rb`, which reads each shape of an operation against the same bytes moved along the best path it has (PR #349)
73
+ * Make every object depend on the headers it is built from, a header-only change having left stale objects behind (PR #346)
74
+ * Answer the four cuDNN TODOs left in the tree, and say what the memory pool's `TestRemoveFromFreeList` holds rather than what it waits for (PR #340, PR #339, PR #338, PR #337)
75
+
1
76
  # 0.5.11 (2026/08/29)
2
77
 
3
78
  Breaking changes:
@@ -232,7 +307,7 @@ Fixes:
232
307
  * Backport: improve discrimination between int32/64
233
308
  * Backport: missing declaration of id_ge
234
309
  * Backport: support casting any object that responds to 'to_a'
235
- * 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`
236
311
  * Backport: if int32_max is Bignum, it should be protected from GC
237
312
  * fix: handle compiler flags included in RbConfig::CONFIG properly (#157)
238
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.
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # ---------------------------------------------------------------------------
5
+ # Cumo shape sweep
6
+ #
7
+ # cumo_probe.rb runs every kernel family at one shape and reads each member
8
+ # against the fastest of its group. What that cannot see is a family whose
9
+ # speed depends on the shape it is handed: a reduction along a short row, an
10
+ # operand that is broadcast or strided, a copy that transposes. This sweep
11
+ # holds the bytes fixed, varies the shape and the layout, and reads every case
12
+ # against the same bytes moved down the best path there is for them: the flat
13
+ # reduction, the add of two contiguous arrays, the contiguous copy.
14
+ #
15
+ # GPU=1 ruby cumo_shape_probe.rb # the default sweep
16
+ # GPU=1 GROUP=reduce ruby cumo_shape_probe.rb # reduce, elementwise or copy
17
+ # GPU=1 ALL=1 ruby cumo_shape_probe.rb # every row, not just the flagged
18
+ # GPU=1 FLAG=3 ruby cumo_shape_probe.rb # how far off the reference is called out
19
+ # GPU=1 DTYPE=DFloat ELEMENTS=67108864 ruby cumo_shape_probe.rb
20
+ # GPU=1 JSON=before.json ruby cumo_shape_probe.rb # keep this run's rows ...
21
+ # GPU=1 COMPARE=before.json ruby cumo_shape_probe.rb # ... and read a later build against them
22
+ # ruby cumo_shape_probe.rb # the same sweep on Numo
23
+ #
24
+ # Every case queues LAUNCHES calls and synchronizes once, and the run opens
25
+ # with a few seconds of load. On a laptop the memory clock climbs in steps,
26
+ # and a best-of-N that synchronizes between calls can settle on a step below
27
+ # the top and read a third low without anything being wrong.
28
+ #
29
+ # The sweep runs in one process. The cases work in place or reduce, so the
30
+ # only results allocated are the reductions', a thousandth of their input.
31
+ #
32
+ # The default 4M elements of SFloat is 16 MB, inside the L2 of this class of
33
+ # GPU, which is where the activations of a model live. ELEMENTS=67108864 moves
34
+ # the sweep to DRAM, where a cost paid per element hides behind the bandwidth.
35
+ # ---------------------------------------------------------------------------
36
+
37
+ require 'json'
38
+
39
+ GPU = !%w[0 false].include?(ENV['GPU'].to_s.downcase) && !ENV['GPU'].to_s.empty?
40
+
41
+ if GPU
42
+ require 'cumo/narray'
43
+ XM = Cumo
44
+ else
45
+ require 'numo/narray'
46
+ XM = Numo
47
+ end
48
+
49
+ GROUP = ENV['GROUP']
50
+ FLAG = (ENV['FLAG'] || 2.0).to_f
51
+ SHOW_ALL = !ENV['ALL'].to_s.empty?
52
+ LAUNCHES = (ENV['LAUNCHES'] || 10).to_i
53
+ ROUNDS = (ENV['ROUNDS'] || 4).to_i
54
+ ELEMENTS = (ENV['ELEMENTS'] || (1 << 22)).to_i
55
+ DTYPE = XM.const_get(ENV['DTYPE'] || 'SFloat')
56
+ JSON_OUT = ENV['JSON']
57
+ COMPARE = ENV['COMPARE']
58
+
59
+ def sync
60
+ XM::CUDA::Runtime.cudaDeviceSynchronize if GPU
61
+ end
62
+
63
+ def now
64
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
65
+ end
66
+
67
+ def elmsz
68
+ DTYPE::ELEMENT_BYTE_SIZE
69
+ end
70
+
71
+ def warm(seconds)
72
+ a = DTYPE.new(1 << 22).fill(0)
73
+ t = now
74
+ a.inplace + 1 while now - t < seconds
75
+ sync
76
+ end
77
+
78
+ # Seconds for one call, best of ROUNDS batches of LAUNCHES calls.
79
+ def time_one
80
+ 2.times { yield }
81
+ sync
82
+ best = Float::INFINITY
83
+ ROUNDS.times do
84
+ sync
85
+ t = now
86
+ LAUNCHES.times { yield }
87
+ sync
88
+ best = [best, (now - t) / LAUNCHES].min
89
+ end
90
+ best
91
+ end
92
+
93
+ Row = Struct.new(:group, :name, :shape, :us, :gbs, :ref, keyword_init: true) do
94
+ def key
95
+ "#{group}/#{name}"
96
+ end
97
+ end
98
+
99
+ ROWS = []
100
+
101
+ # bytes is what the case moves, and the reference row is the one this case is
102
+ # read against.
103
+ def probe(group, name, shape, bytes, ref: nil, &blk)
104
+ return if GROUP && !group.start_with?(GROUP)
105
+
106
+ s = time_one(&blk)
107
+ ROWS << Row.new(group: group, name: name, shape: shape.inspect, us: s * 1e6, gbs: bytes / s / 1e9, ref: ref || name)
108
+ end
109
+
110
+ def rand_array(*shape)
111
+ a = DTYPE.new(*shape)
112
+ DTYPE.name.include?('Int') ? a.rand(100) : a.rand
113
+ end
114
+
115
+ # --- reductions ---------------------------------------------------------------
116
+ #
117
+ # The same ELEMENTS reduced along the last axis, the first axis and a middle
118
+ # axis, over rows of every length, all read against the flat reduction of the
119
+ # same array. Each output of every case is a reduction over len elements.
120
+ def sweep_reduce
121
+ n = ELEMENTS
122
+ es = elmsz
123
+ a = rand_array(n)
124
+ b = rand_array(n)
125
+ methods = %i[sum max mean argmax mulsum].select { |m| a.respond_to?(m) }
126
+ methods.each do |m|
127
+ bytes = (m == :mulsum ? 2 : 1) * n * es
128
+ probe('reduce', "#{m} flat", [n], bytes) { m == :mulsum ? a.mulsum(b) : a.send(m) }
129
+ end
130
+ [4, 16, 64, 256, 1024, 4096].each do |len|
131
+ next if len > n
132
+
133
+ rows = n / len
134
+ r1 = 1 << (Math.log2(rows).to_i / 2)
135
+ r2 = rows / r1
136
+ last = a.reshape(rows, len)
137
+ last_b = b.reshape(rows, len)
138
+ first = a.reshape(len, rows)
139
+ first_b = b.reshape(len, rows)
140
+ mid = a.reshape(r1, len, r2)
141
+ mid_b = b.reshape(r1, len, r2)
142
+ methods.each do |m|
143
+ bytes = (m == :mulsum ? 2 : 1) * n * es
144
+ ref = "#{m} flat"
145
+ call = lambda do |x, y, axis|
146
+ m == :mulsum ? x.mulsum(y, axis: axis) : x.send(m, axis: axis)
147
+ end
148
+ probe('reduce', "#{m} len #{len} last", [rows, len], bytes, ref: ref) { call.call(last, last_b, 1) }
149
+ probe('reduce', "#{m} len #{len} first", [len, rows], bytes, ref: ref) { call.call(first, first_b, 0) }
150
+ probe('reduce', "#{m} len #{len} middle", [r1, len, r2], bytes, ref: ref) { call.call(mid, mid_b, 1) }
151
+ end
152
+ end
153
+ end
154
+
155
+ # --- elementwise --------------------------------------------------------------
156
+ #
157
+ # One [R, C] array added to in place, its operand contiguous, a scalar, a
158
+ # broadcast row or column, a slice, a stepped view or a transpose, plus the
159
+ # same broadcast through three and four dimensions. The reference is the add
160
+ # of two contiguous arrays, which the loop walks as one run.
161
+ def sweep_elementwise
162
+ n = ELEMENTS
163
+ es = elmsz
164
+ r = 1024
165
+ c = n / r
166
+ a = rand_array(r, c)
167
+ b = rand_array(r, c)
168
+ row = rand_array(c)
169
+ col = rand_array(r, 1)
170
+ probe('elementwise', 'a + b contiguous', [r, c], 3 * n * es) { a.inplace + b }
171
+ probe('elementwise', 'a * scalar', [r, c], 2 * n * es, ref: 'a + b contiguous') { a.inplace * 2 }
172
+ probe('elementwise', 'a + row broadcast', [r, c], 2 * n * es, ref: 'a + b contiguous') { a.inplace + row }
173
+ probe('elementwise', 'a - column broadcast', [r, c], 2 * n * es, ref: 'a + b contiguous') { a.inplace - col }
174
+ a3 = a.reshape(16, r / 16, c)
175
+ b3 = rand_array(1, r / 16, 1)
176
+ probe('elementwise', 'a + broadcast 3d', [16, r / 16, c], 2 * n * es, ref: 'a + b contiguous') { a3.inplace + b3 }
177
+ a4 = a.reshape(4, 4, r / 16, c)
178
+ b4 = rand_array(1, 4, 1, c)
179
+ probe('elementwise', 'a + broadcast 4d', [4, 4, r / 16, c], 2 * n * es, ref: 'a + b contiguous') { a4.inplace + b4 }
180
+ half = 0...(c / 2)
181
+ probe('elementwise', 'a + b column slice', [r, c / 2], 3 * n * es / 2, ref: 'a + b contiguous') { a[true, half].inplace + b[true, half] }
182
+ every_other = (0..-1) % 2
183
+ probe('elementwise', 'a * scalar stepped rows', [r / 2, c], n * es, ref: 'a + b contiguous') { a[every_other, true].inplace * 2 }
184
+ probe('elementwise', 'a + b transposed views', [c, r], 3 * n * es, ref: 'a + b contiguous') { a.transpose.inplace + b.transpose }
185
+ probe('elementwise', 'a.gt(b) to Bit', [r, c], 2 * n * es, ref: 'a + b contiguous') { a.gt(b) }
186
+ probe('elementwise', 'a.gt(row) to Bit', [r, c], n * es, ref: 'a + b contiguous') { a.gt(row) }
187
+ end
188
+
189
+ # --- copies -------------------------------------------------------------------
190
+ #
191
+ # A store from a view of every kind into contiguous memory, read against the
192
+ # contiguous copy of the same bytes.
193
+ def sweep_copy
194
+ n = ELEMENTS
195
+ es = elmsz
196
+ r = 1024
197
+ c = n / r
198
+ a = rand_array(r, c)
199
+ dst = DTYPE.new(r, c).fill(0)
200
+ dst_t = DTYPE.new(c, r).fill(0)
201
+ probe('copy', 'contiguous', [r, c], 2 * n * es) { dst.store(a) }
202
+ half = 0...(c / 2)
203
+ probe('copy', 'column slice', [r, c / 2], n * es, ref: 'contiguous') { dst[true, half].store(a[true, half]) }
204
+ every_other = (0..-1) % 2
205
+ probe('copy', 'stepped rows', [r / 2, c], n * es, ref: 'contiguous') { dst[every_other, true].store(a[every_other, true]) }
206
+ probe('copy', 'reversed rows', [r, c], 2 * n * es, ref: 'contiguous') { dst.store(a.reverse(0)) }
207
+ probe('copy', 'transposed', [c, r], 2 * n * es, ref: 'contiguous') { dst_t.store(a.transpose) }
208
+ idx = XM::Int32.new(r / 2).seq * 2
209
+ probe('copy', 'index-backed rows', [r / 2, c], n * es, ref: 'contiguous') { dst[0...(r / 2), true].store(a[idx, true]) }
210
+ end
211
+
212
+ # --- run ----------------------------------------------------------------------
213
+
214
+ version = GPU ? Cumo::NArray::VERSION : Numo::NArray::VERSION
215
+ puts "backend : #{XM} #{version}"
216
+ puts "dtype : #{DTYPE}, #{ELEMENTS} elements (#{(ELEMENTS * elmsz / 1e6).round(1)} MB), #{LAUNCHES} launches per sync, best of #{ROUNDS}"
217
+ warm(3)
218
+ sweep_reduce
219
+ sweep_elementwise
220
+ sweep_copy
221
+
222
+ refs = ROWS.to_h { |row| [row.key, row] }
223
+ flagged = []
224
+ puts
225
+ puts format(' %-11s %-26s %-18s %9s %8s %8s', 'group', 'case', 'shape', 'us', 'GB/s', 'vs ref')
226
+ ROWS.each do |row|
227
+ ref = refs["#{row.group}/#{row.ref}"]
228
+ ratio = ref ? ref.gbs / row.gbs : 1.0
229
+ slow = ratio > FLAG
230
+ flagged << row if slow
231
+ next unless slow || SHOW_ALL || row.ref == row.name
232
+
233
+ puts format(' %-11s %-26s %-18s %9.1f %8.1f %7.1fx%s', row.group, row.name, row.shape, row.us, row.gbs, ratio, slow ? ' <--' : '')
234
+ end
235
+ puts
236
+ if flagged.empty?
237
+ puts format(' nothing was more than %.1fx off its reference', FLAG)
238
+ else
239
+ puts format(' %d of %d cases more than %.1fx off their reference', flagged.size, ROWS.size, FLAG)
240
+ end
241
+
242
+ if COMPARE
243
+ before = JSON.parse(File.read(COMPARE))
244
+ puts
245
+ puts " against #{File.basename(COMPARE)} (#{before['meta']['backend']} #{before['meta']['version']}, saved #{before['meta']['saved']})"
246
+ puts format(' %-11s %-26s %9s %9s %8s', 'group', 'case', 'before us', 'after us', 'speedup')
247
+ ROWS.each do |row|
248
+ was = before['rows'][row.key]
249
+ next unless was
250
+
251
+ speedup = was['us'] / row.us
252
+ next unless SHOW_ALL || speedup > 1.25 || speedup < 0.8
253
+
254
+ puts format(' %-11s %-26s %9.1f %9.1f %7.2fx', row.group, row.name, was['us'], row.us, speedup)
255
+ end
256
+ end
257
+
258
+ if JSON_OUT
259
+ out = { 'meta' => { 'backend' => XM.to_s, 'version' => version, 'dtype' => DTYPE.to_s, 'elements' => ELEMENTS,
260
+ 'saved' => Time.now.strftime('%Y-%m-%d %H:%M') },
261
+ 'rows' => ROWS.to_h { |row| [row.key, { 'shape' => row.shape, 'us' => row.us, 'gbs' => row.gbs }] } }
262
+ File.write(JSON_OUT, JSON.pretty_generate(out))
263
+ puts " wrote #{ROWS.size} rows to #{JSON_OUT}"
264
+ end
@@ -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.'