cumo 0.5.11 → 0.6.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/bench/cumo_shape_probe.rb +264 -0
  4. data/ext/cumo/cuda/cudnn_impl.cpp +9 -1
  5. data/ext/cumo/cuda/memory_pool_impl_test.cpp +4 -1
  6. data/ext/cumo/cuda/nvrtc.c +1 -1
  7. data/ext/cumo/depend.erb +7 -0
  8. data/ext/cumo/include/cumo/bit_reduce_kernel.h +119 -13
  9. data/ext/cumo/include/cumo/check.h +51 -0
  10. data/ext/cumo/include/cumo/cuda/cudnn.h +27 -42
  11. data/ext/cumo/include/cumo/indexer.h +29 -8
  12. data/ext/cumo/include/cumo/narray.h +10 -0
  13. data/ext/cumo/include/cumo/ndloop.h +1 -0
  14. data/ext/cumo/include/cumo/reduce_kernel.h +133 -50
  15. data/ext/cumo/include/cumo/types/float_macro.h +15 -2
  16. data/ext/cumo/include/cumo/types/float_macro_kernel.h +15 -2
  17. data/ext/cumo/include/cumo/types/int_macro.h +20 -0
  18. data/ext/cumo/include/cumo/types/int_macro_kernel.h +20 -0
  19. data/ext/cumo/include/cumo/types/robj_macro.h +8 -3
  20. data/ext/cumo/include/cumo.h +2 -2
  21. data/ext/cumo/narray/data.c +21 -25
  22. data/ext/cumo/narray/data_kernel.cu +113 -0
  23. data/ext/cumo/narray/gen/narray_def.rb +5 -0
  24. data/ext/cumo/narray/gen/spec.rb +10 -0
  25. data/ext/cumo/narray/gen/tmpl/batch_norm.c +25 -22
  26. data/ext/cumo/narray/gen/tmpl/batch_norm_backward.c +20 -17
  27. data/ext/cumo/narray/gen/tmpl/binary.c +3 -3
  28. data/ext/cumo/narray/gen/tmpl/binary2.c +1 -1
  29. data/ext/cumo/narray/gen/tmpl/binary_s.c +3 -3
  30. data/ext/cumo/narray/gen/tmpl/clip.c +6 -6
  31. data/ext/cumo/narray/gen/tmpl/cond_binary.c +3 -3
  32. data/ext/cumo/narray/gen/tmpl/cond_unary.c +1 -1
  33. data/ext/cumo/narray/gen/tmpl/conv.c +8 -8
  34. data/ext/cumo/narray/gen/tmpl/conv_grad_w.c +10 -10
  35. data/ext/cumo/narray/gen/tmpl/conv_transpose.c +10 -10
  36. data/ext/cumo/narray/gen/tmpl/ewcomp.c +1 -1
  37. data/ext/cumo/narray/gen/tmpl/fill.c +1 -1
  38. data/ext/cumo/narray/gen/tmpl/fixed_batch_norm.c +16 -13
  39. data/ext/cumo/narray/gen/tmpl/frexp.c +1 -1
  40. data/ext/cumo/narray/gen/tmpl/poly.c +1 -1
  41. data/ext/cumo/narray/gen/tmpl/pooling_backward.c +4 -4
  42. data/ext/cumo/narray/gen/tmpl/pooling_forward.c +4 -4
  43. data/ext/cumo/narray/gen/tmpl/pow.c +6 -6
  44. data/ext/cumo/narray/gen/tmpl/real_accum_kernel.cu +73 -0
  45. data/ext/cumo/narray/gen/tmpl/set2.c +1 -1
  46. data/ext/cumo/narray/gen/tmpl/store_bit.c +1 -1
  47. data/ext/cumo/narray/gen/tmpl/store_from.c +1 -1
  48. data/ext/cumo/narray/gen/tmpl/unary.c +1 -1
  49. data/ext/cumo/narray/gen/tmpl/unary2.c +1 -1
  50. data/ext/cumo/narray/gen/tmpl/unary_ret2.c +1 -1
  51. data/ext/cumo/narray/gen/tmpl/unary_s.c +1 -1
  52. data/ext/cumo/narray/gen/tmpl_bit/binary.c +1 -1
  53. data/ext/cumo/narray/gen/tmpl_bit/bit_reduce.c +4 -1
  54. data/ext/cumo/narray/gen/tmpl_bit/bit_stat.c +36 -0
  55. data/ext/cumo/narray/gen/tmpl_bit/bit_stat_kernel.cu +4 -0
  56. data/ext/cumo/narray/gen/tmpl_bit/extract.c +13 -27
  57. data/ext/cumo/narray/gen/tmpl_bit/fill.c +1 -1
  58. data/ext/cumo/narray/gen/tmpl_bit/store_bit.c +1 -1
  59. data/ext/cumo/narray/gen/tmpl_bit/store_from.c +1 -1
  60. data/ext/cumo/narray/gen/tmpl_bit/unary.c +1 -1
  61. data/ext/cumo/narray/index.c +20 -8
  62. data/ext/cumo/narray/narray.c +155 -18
  63. data/ext/cumo/narray/ndloop.c +67 -0
  64. data/lib/cumo/narray/extra.rb +119 -24
  65. data/test/bit_test.rb +143 -0
  66. data/test/cudnn_test.rb +88 -0
  67. data/test/cumo_test.rb +1 -0
  68. data/test/extra_test.rb +74 -6
  69. data/test/narray_test.rb +851 -3
  70. metadata +5 -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: 0d6b3e0decf69d07199a7930dc6a23d1a05a90ed044cf007203c78c28a8e9ea1
4
+ data.tar.gz: dec0ca1f195b302919d30085fcc139911299ba075193b67cab9965907210092f
5
5
  SHA512:
6
- metadata.gz: 361042e17c2f498b8e4f5ddc76bbd119688a6a3a3ac176307973fd51c75b332d4d831a82c0fc0e8a3b28be98bfbe50d507ec67d517c609c2342caf57d94d9c41
7
- data.tar.gz: 53da42c447fe31812654e1ab0566f5e553f781daaf75b97bb1c36630b8dc3ed195704002990bc4a2a9f4fdd6e26e92c2d5905ff64da0e0f058b01dae20ae3dec
6
+ metadata.gz: e4f8d6204fca59288efad1ab5dec228d474de02ee3465f89afe7fc7440b456b0d46b78c92fb1e48e92063a2b3c385dedb8955aed3ddfa4411c13beb2f32a5417
7
+ data.tar.gz: a397b427534b829b13f8bd4a4d6f8616d918bde834dc5c69a5611671ec942796d8615bd74d2564a2c3cd6cdaf9e572f4599e0704071f21a3c40394d0236b148d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ # 0.6.0 (2026/09/06)
2
+
3
+ Breaking changes:
4
+
5
+ * `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
+ * A view whose base has since been given a smaller shape raises `RuntimeError` instead of reading past what the base holds (PR #363)
7
+ * `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)
9
+ * `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
+ * `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
+ * `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)
12
+
13
+ Fixes:
14
+
15
+ * 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)
16
+ * 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)
17
+ * 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)
18
+ * 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)
19
+ * 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)
20
+ * 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)
21
+
22
+ Changes:
23
+
24
+ * Add `argsort`, which answers positions along an axis rather than into the flattened array as `sort_index` does (PR #342)
25
+ * Add `mean`, `var`, `stddev` and `rms` to the integer types and to `Cumo::Bit`, which had only the float ones (PR #344, PR #345)
26
+ * 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)
27
+ * 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)
28
+ * 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)
29
+ * 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)
30
+ * 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)
31
+ * Make every object depend on the headers it is built from, a header-only change having left stale objects behind (PR #346)
32
+ * 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)
33
+
1
34
  # 0.5.11 (2026/08/29)
2
35
 
3
36
  Breaking changes:
@@ -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
@@ -552,7 +552,15 @@ cumo_cuda_cudnn_GetBatchNormMode(size_t ndim, int* axis) {
552
552
  }
553
553
  if ((ndim == 3 && axis[0] == 0 && axis[1] == 2 && axis[2] == 3) ||
554
554
  (ndim == 4 && axis[0] == 0 && axis[1] == 2 && axis[2] == 3 && axis[3] == 4)) { // (1, channels, (1, )1, 1)
555
- // TODO: Consider CUDNN_BATCHNORM_SPATIAL_PERSISTENT if we can afford to check for overflow, with or without blocking.
555
+ // CUDNN_BATCHNORM_SPATIAL_PERSISTENT is not worth taking. The overflow
556
+ // check it asks for is affordable -- cudnnQueryRuntimeError costs 0.1us
557
+ // either way -- but the mode is 1.3 to 1.8x faster only where the
558
+ // spatial extent is large, and 1.6x slower on 32x256x14x14 backward.
559
+ // Worse, on 32x64x56x56 it raises CUDNN_STATUS_RUNTIME_FP_OVERFLOW for x
560
+ // in [-2, 2] with gamma 1 and beta 0, answering bit for bit what SPATIAL
561
+ // does; honouring that means running SPATIAL again on top. The mode, and
562
+ // the batch normalization API it belongs to, are deprecated as of cuDNN
563
+ // 9.0.0 besides.
556
564
  return CUDNN_BATCHNORM_SPATIAL;
557
565
  }
558
566
  rb_raise(rb_eRuntimeError, "Invalid axis for BatchNorm using cuDNN. Expected 1, 3 or 4 dimensions.");
@@ -337,7 +337,10 @@ public:
337
337
  assert(pool_->GetFreeBytes() == kRoundSize * 8);
338
338
  }
339
339
 
340
- // TODO(sonots): Fix after implementing compaction
340
+ // RemoveFromFreeList leaves an emptied bin in the arena -- it erases the
341
+ // chunk and returns, where Malloc drops one through CompactIndex. The
342
+ // assertions below spell that out, so they describe the arena as it is
343
+ // rather than wait on a compaction this path is not getting.
341
344
  void TestRemoveFromFreeList() {
342
345
  Arena& arena = pool_->GetArena(stream_ptr_);
343
346
  ArenaIndexMap& arena_index_map = pool_->GetArenaIndexMap(stream_ptr_);
@@ -63,7 +63,7 @@ push_strings(VALUE strings, VALUE ary, int n)
63
63
  int i;
64
64
 
65
65
  for (i = 0; i < n; i++) {
66
- VALUE str = rb_str_to_str(RARRAY_AREF(ary, i));
66
+ VALUE str = rb_str_to_str(rb_ary_entry(ary, i));
67
67
  StringValueCStr(str);
68
68
  rb_ary_push(strings, str);
69
69
  }
data/ext/cumo/depend.erb CHANGED
@@ -49,6 +49,13 @@ list_type_rb.each do |type_rb|
49
49
 
50
50
  src : <%= list_type_cu.join(" ") %> <%= list_type_c.join(" ") %>
51
51
 
52
+ # mkmf writes no rule that makes an object depend on a header, so editing one
53
+ # and rebuilding was a no-op that kept the previous cumo.so. Rebuilding all of
54
+ # them is a minute and a half, so the list does not have to be per-object.
55
+ CUMO_HDRS = <%= Dir.glob("#{__dir__}/**/*.{h,hpp}").sort.join(" ") %>
56
+
57
+ $(OBJS): $(CUMO_HDRS)
58
+
52
59
  build-ctest : <%= __dir__ %>/cuda/memory_pool_impl_test.exe
53
60
 
54
61
  run-ctest : <%= __dir__ %>/cuda/memory_pool_impl_test.exe
@@ -7,6 +7,11 @@
7
7
  #include "cumo/indexer.h"
8
8
  #include "cumo/reduce_kernel.h"
9
9
 
10
+ #define CUMO_BIT_STAT_MEAN 0
11
+ #define CUMO_BIT_STAT_VAR 1
12
+ #define CUMO_BIT_STAT_STDDEV 2
13
+ #define CUMO_BIT_STAT_RMS 3
14
+
10
15
  namespace cumo_bit_detail {
11
16
 
12
17
  // A Bit reduction addresses its operands the way a numeric one does -- the
@@ -169,7 +174,7 @@ __device__ static inline uint64_t bit_count_axis(
169
174
  }
170
175
 
171
176
  __global__ static void bit_count_reduction_kernel(
172
- cumo_na_bit_reduction_arg_t arg, cumo_detail::cumo_reduce_addr_t ad, cumo_bit_word_addr_t wa, int invert,
177
+ CUMO_GRID_CONSTANT cumo_na_bit_reduction_arg_t arg, CUMO_GRID_CONSTANT cumo_detail::cumo_reduce_addr_t ad, CUMO_GRID_CONSTANT cumo_bit_word_addr_t wa, int invert,
173
178
  int out_block_size, int reduce_block_size, int64_t unit_total_size) {
174
179
  extern __shared__ __align__(8) char sdata_raw[];
175
180
  uint64_t* sdata = reinterpret_cast<uint64_t*>(sdata_raw);
@@ -179,8 +184,8 @@ __global__ static void bit_count_reduction_kernel(
179
184
  int64_t out_total_size = arg.out_indexer.total_size;
180
185
  int64_t reduce_total_size = arg.in_indexer.total_size / out_total_size;
181
186
 
182
- int64_t reduce_offset = tid / out_block_size;
183
- int64_t out_offset = tid % out_block_size;
187
+ int64_t reduce_offset, out_offset;
188
+ cumo_detail::reduce_thread_split(ad, tid, out_block_size, reduce_block_size, &reduce_offset, &out_offset);
184
189
  int64_t out_base = blockIdx.x * out_block_size;
185
190
  int64_t out_stride = gridDim.x * out_block_size;
186
191
 
@@ -191,7 +196,7 @@ __global__ static void bit_count_reduction_kernel(
191
196
  uint64_t accum = bit_count_axis(arg, ad, wa, invert, in_out_off, i_in, reduce_total_size,
192
197
  0, unit_total_size, reduce_offset, reduce_block_size);
193
198
 
194
- accum = cumo_detail::reduce_in_block(accum, sdata, tid, out_block_size, impl);
199
+ accum = cumo_detail::reduce_in_block(accum, sdata, tid, out_block_size, reduce_block_size, !ad.out_inner, impl);
195
200
  if (reduce_offset == 0) {
196
201
  *reinterpret_cast<uint64_t*>(arg.out.ptr + bit_out_offset(arg, ad, i_out)) = accum;
197
202
  }
@@ -202,7 +207,7 @@ __global__ static void bit_count_reduction_kernel(
202
207
  // grid a handful of blocks however long the reduce axis is.
203
208
  template <typename TArg>
204
209
  __global__ static void bit_count_partial_kernel(
205
- TArg arg, cumo_detail::cumo_reduce_addr_t ad, cumo_bit_word_addr_t wa, int invert,
210
+ CUMO_GRID_CONSTANT TArg arg, CUMO_GRID_CONSTANT cumo_detail::cumo_reduce_addr_t ad, CUMO_GRID_CONSTANT cumo_bit_word_addr_t wa, int invert,
206
211
  uint64_t* partial, int64_t n_split, int64_t chunk,
207
212
  int out_block_size, int reduce_block_size, int64_t unit_total_size) {
208
213
  extern __shared__ __align__(8) char sdata_raw[];
@@ -214,8 +219,8 @@ __global__ static void bit_count_partial_kernel(
214
219
  int64_t reduce_total_size = arg.in_indexer.total_size / out_total_size;
215
220
  int64_t partial_total_size = out_total_size * n_split;
216
221
 
217
- int64_t reduce_offset = tid / out_block_size;
218
- int64_t out_offset = tid % out_block_size;
222
+ int64_t reduce_offset, out_offset;
223
+ cumo_detail::reduce_thread_split(ad, tid, out_block_size, reduce_block_size, &reduce_offset, &out_offset);
219
224
  int64_t out_base = blockIdx.x * out_block_size;
220
225
  int64_t out_stride = gridDim.x * out_block_size;
221
226
 
@@ -231,7 +236,7 @@ __global__ static void bit_count_partial_kernel(
231
236
  uint64_t accum = bit_count_axis(arg, ad, wa, invert, in_out_off, i_in, reduce_total_size,
232
237
  begin, end, reduce_offset, reduce_block_size);
233
238
 
234
- accum = cumo_detail::reduce_in_block(accum, sdata, tid, out_block_size, impl);
239
+ accum = cumo_detail::reduce_in_block(accum, sdata, tid, out_block_size, reduce_block_size, !ad.out_inner, impl);
235
240
  if (reduce_offset == 0) {
236
241
  partial[i_out * n_split + i_split] = accum;
237
242
  }
@@ -287,6 +292,71 @@ static inline bit_reduce_plan make_bit_reduce_plan(const TArg& arg) {
287
292
  return p;
288
293
  }
289
294
 
295
+ // mean, var, stddev and rms of a bit array are functions of how many bits are
296
+ // set and how long the axis is, because every element is 0 or 1. They reduce
297
+ // the way count does and differ only in what the store computes.
298
+ __device__ static inline double bit_stat_of_count(uint64_t count, int64_t reduce_total_size, int stat) {
299
+ double k = (double)count;
300
+ double n = (double)reduce_total_size;
301
+ double mean = k / n;
302
+ switch (stat) {
303
+ case CUMO_BIT_STAT_MEAN: return mean;
304
+ case CUMO_BIT_STAT_RMS: return sqrt(mean);
305
+ // x squared is x for a bit, so the squared deviations sum to k - k * mean.
306
+ case CUMO_BIT_STAT_VAR: return (k - k * mean) / (n - 1);
307
+ default: return sqrt((k - k * mean) / (n - 1));
308
+ }
309
+ }
310
+
311
+ __global__ static void bit_stat_reduction_kernel(
312
+ CUMO_GRID_CONSTANT cumo_na_bit_reduction_arg_t arg, CUMO_GRID_CONSTANT cumo_detail::cumo_reduce_addr_t ad, CUMO_GRID_CONSTANT cumo_bit_word_addr_t wa, int stat,
313
+ int out_block_size, int reduce_block_size, int64_t unit_total_size) {
314
+ extern __shared__ __align__(8) char sdata_raw[];
315
+ uint64_t* sdata = reinterpret_cast<uint64_t*>(sdata_raw);
316
+ unsigned int tid = threadIdx.x;
317
+ BitCountImpl impl;
318
+
319
+ int64_t out_total_size = arg.out_indexer.total_size;
320
+ int64_t reduce_total_size = arg.in_indexer.total_size / out_total_size;
321
+
322
+ int64_t reduce_offset, out_offset;
323
+ cumo_detail::reduce_thread_split(ad, tid, out_block_size, reduce_block_size, &reduce_offset, &out_offset);
324
+ int64_t out_base = blockIdx.x * out_block_size;
325
+ int64_t out_stride = gridDim.x * out_block_size;
326
+
327
+ for (int64_t i_out = out_base + out_offset; i_out < out_total_size; i_out += out_stride) {
328
+ ssize_t in_out_off = bit_in_out_offset(arg, ad, i_out);
329
+ int64_t i_in = i_out * reduce_total_size + reduce_offset;
330
+
331
+ uint64_t accum = bit_count_axis(arg, ad, wa, 0, in_out_off, i_in, reduce_total_size,
332
+ 0, unit_total_size, reduce_offset, reduce_block_size);
333
+
334
+ accum = cumo_detail::reduce_in_block(accum, sdata, tid, out_block_size, reduce_block_size, !ad.out_inner, impl);
335
+ if (reduce_offset == 0) {
336
+ *reinterpret_cast<double*>(arg.out.ptr + bit_out_offset(arg, ad, i_out)) =
337
+ bit_stat_of_count(accum, reduce_total_size, stat);
338
+ }
339
+ }
340
+ }
341
+
342
+ // Second pass of a split statistic, laid out like bit_pred_combine_kernel.
343
+ __global__ static void bit_stat_combine_kernel(
344
+ CUMO_GRID_CONSTANT cumo_na_bit_reduction_arg_t arg, CUMO_GRID_CONSTANT cumo_detail::cumo_reduce_addr_t ad, int stat,
345
+ const uint64_t* partial, int64_t n_split) {
346
+ int64_t out_total_size = arg.out_indexer.total_size;
347
+ int64_t reduce_total_size = arg.in_indexer.total_size / out_total_size;
348
+
349
+ for (int64_t i_out = blockIdx.x * blockDim.x + threadIdx.x; i_out < out_total_size;
350
+ i_out += (int64_t)blockDim.x * gridDim.x) {
351
+ uint64_t count = 0;
352
+ for (int64_t i_split = 0; i_split < n_split; ++i_split) {
353
+ count += partial[i_out * n_split + i_split];
354
+ }
355
+ *reinterpret_cast<double*>(arg.out.ptr + bit_out_offset(arg, ad, i_out)) =
356
+ bit_stat_of_count(count, reduce_total_size, stat);
357
+ }
358
+ }
359
+
290
360
  // all? and any? are the count of set bits against the length of the axis and
291
361
  // against zero, so they reduce the same way and differ only here.
292
362
  __device__ static inline CUMO_BIT_DIGIT bit_pred_of_count(uint64_t count, int64_t reduce_total_size, int all) {
@@ -294,7 +364,7 @@ __device__ static inline CUMO_BIT_DIGIT bit_pred_of_count(uint64_t count, int64_
294
364
  }
295
365
 
296
366
  __global__ static void bit_pred_reduction_kernel(
297
- cumo_na_bit_pred_reduction_arg_t arg, cumo_detail::cumo_reduce_addr_t ad, cumo_bit_word_addr_t wa, int all,
367
+ CUMO_GRID_CONSTANT cumo_na_bit_pred_reduction_arg_t arg, CUMO_GRID_CONSTANT cumo_detail::cumo_reduce_addr_t ad, CUMO_GRID_CONSTANT cumo_bit_word_addr_t wa, int all,
298
368
  int out_block_size, int reduce_block_size, int64_t unit_total_size) {
299
369
  extern __shared__ __align__(8) char sdata_raw[];
300
370
  uint64_t* sdata = reinterpret_cast<uint64_t*>(sdata_raw);
@@ -304,8 +374,8 @@ __global__ static void bit_pred_reduction_kernel(
304
374
  int64_t out_total_size = arg.out_indexer.total_size;
305
375
  int64_t reduce_total_size = arg.in_indexer.total_size / out_total_size;
306
376
 
307
- int64_t reduce_offset = tid / out_block_size;
308
- int64_t out_offset = tid % out_block_size;
377
+ int64_t reduce_offset, out_offset;
378
+ cumo_detail::reduce_thread_split(ad, tid, out_block_size, reduce_block_size, &reduce_offset, &out_offset);
309
379
  int64_t out_base = blockIdx.x * out_block_size;
310
380
  int64_t out_stride = gridDim.x * out_block_size;
311
381
 
@@ -316,7 +386,7 @@ __global__ static void bit_pred_reduction_kernel(
316
386
  uint64_t accum = bit_count_axis(arg, ad, wa, 0, in_out_off, i_in, reduce_total_size,
317
387
  0, unit_total_size, reduce_offset, reduce_block_size);
318
388
 
319
- accum = cumo_detail::reduce_in_block(accum, sdata, tid, out_block_size, impl);
389
+ accum = cumo_detail::reduce_in_block(accum, sdata, tid, out_block_size, reduce_block_size, !ad.out_inner, impl);
320
390
  if (reduce_offset == 0) {
321
391
  size_t pos = (size_t)((ssize_t)arg.out.pos + bit_out_offset(arg, ad, i_out));
322
392
  CUMO_STORE_BIT(arg.out.ptr, pos, bit_pred_of_count(accum, reduce_total_size, all));
@@ -327,7 +397,7 @@ __global__ static void bit_pred_reduction_kernel(
327
397
  // Second pass of a split all? or any?. There are few outputs whenever the axis
328
398
  // was worth splitting, so one thread per output walks its chunks.
329
399
  __global__ static void bit_pred_combine_kernel(
330
- cumo_na_bit_pred_reduction_arg_t arg, cumo_detail::cumo_reduce_addr_t ad, int all,
400
+ CUMO_GRID_CONSTANT cumo_na_bit_pred_reduction_arg_t arg, CUMO_GRID_CONSTANT cumo_detail::cumo_reduce_addr_t ad, int all,
331
401
  const uint64_t* partial, int64_t n_split) {
332
402
  int64_t out_total_size = arg.out_indexer.total_size;
333
403
  int64_t reduce_total_size = arg.in_indexer.total_size / out_total_size;
@@ -387,6 +457,42 @@ static inline void cumo_bit_count_reduce(cumo_na_bit_reduction_arg_t arg, int in
387
457
  cumo_cuda_runtime_free(reinterpret_cast<char*>(partial));
388
458
  }
389
459
 
460
+ // The same for the four statistics, whose result is a DFloat of the same width
461
+ // as the count it is computed from.
462
+ static inline void cumo_bit_stat_reduce(cumo_na_bit_reduction_arg_t arg, int stat) {
463
+ if (arg.out_indexer.total_size == 0) {
464
+ return;
465
+ }
466
+
467
+ cumo_bit_detail::bit_reduce_plan p = cumo_bit_detail::make_bit_reduce_plan(arg);
468
+ int64_t block_size = cumo_detail::max_block_size;
469
+ int64_t shared_mem_size = sizeof(uint64_t) * block_size;
470
+
471
+ if (p.n_split < 2) {
472
+ int64_t grid_size = std::min(cumo_detail::max_grid_size, p.out_block_num);
473
+ cumo_bit_detail::bit_stat_reduction_kernel<<<grid_size, block_size, shared_mem_size>>>(
474
+ arg, p.ad, p.wa, stat, (int)p.out_block_size, (int)p.reduce_block_size, p.unit_total_size);
475
+ cumo_cuda_runtime_check_kernel_launch();
476
+ return;
477
+ }
478
+
479
+ int64_t partial_total_size = p.out_total_size * p.n_split;
480
+ uint64_t* partial = reinterpret_cast<uint64_t*>(cumo_cuda_runtime_malloc(sizeof(uint64_t) * partial_total_size));
481
+
482
+ int64_t grid_size = std::min(cumo_detail::max_grid_size, p.partial_block_num);
483
+ cumo_bit_detail::bit_count_partial_kernel<<<grid_size, block_size, shared_mem_size>>>(
484
+ arg, p.ad, p.wa, 0, partial, p.n_split, p.chunk,
485
+ (int)p.split_out_block_size, (int)p.split_reduce_block_size, p.unit_total_size);
486
+ cumo_cuda_runtime_check_kernel_launch();
487
+
488
+ int64_t combine_grid = (p.out_total_size + block_size - 1) / block_size;
489
+ if (combine_grid > cumo_detail::max_grid_size) combine_grid = cumo_detail::max_grid_size;
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();
492
+
493
+ cumo_cuda_runtime_free(reinterpret_cast<char*>(partial));
494
+ }
495
+
390
496
  // The same for all? (all non-zero) and any? (any non-zero), whose result is a
391
497
  // Bit rather than a count.
392
498
  static inline void cumo_bit_pred_reduce(cumo_na_bit_pred_reduction_arg_t arg, int all) {
@@ -0,0 +1,51 @@
1
+ #ifndef CUMO_CHECK_H
2
+ #define CUMO_CHECK_H
3
+
4
+ #include <ruby.h>
5
+ #include "cumo/narray.h"
6
+
7
+ #if defined(__cplusplus)
8
+ extern "C" {
9
+ #if 0
10
+ } /* satisfy cc-mode */
11
+ #endif
12
+ #endif
13
+
14
+ #define CUMO_CHECK_NARRAY_TYPE(x,t) \
15
+ if (rb_obj_class(x)!=(t)) { \
16
+ rb_raise(rb_eTypeError,"invalid NArray type (class)"); \
17
+ }
18
+
19
+ #define CUMO_CHECK_SIZE_EQ(sz1,sz2) \
20
+ if ((sz1) != (sz2)) { \
21
+ rb_raise(cumo_na_eShapeError, \
22
+ "size mismatch: %d != %d", \
23
+ (int)(sz1), (int)(sz2)); \
24
+ }
25
+
26
+ #define CUMO_CHECK_DIM_EQ(nd1,nd2) \
27
+ if ((nd1) != (nd2)) { \
28
+ rb_raise(cumo_na_eShapeError, \
29
+ "dimension mismatch: %d != %d", \
30
+ (int)(nd1), (int)(nd2)); \
31
+ }
32
+
33
+ static inline VALUE
34
+ cumo_option_value(VALUE value, VALUE default_value)
35
+ {
36
+ switch(TYPE(value)) {
37
+ case T_NIL:
38
+ case T_UNDEF:
39
+ return default_value;
40
+ }
41
+ return value;
42
+ }
43
+
44
+ #if defined(__cplusplus)
45
+ #if 0
46
+ { /* satisfy cc-mode */
47
+ #endif
48
+ } /* extern "C" { */
49
+ #endif
50
+
51
+ #endif /* CUMO_CHECK_H */