cumo 0.5.7 → 0.5.8

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/bench/bench.rb +235 -0
  4. data/ext/cumo/cuda/cublas.c +4 -2
  5. data/ext/cumo/cuda/cudnn.c +4 -2
  6. data/ext/cumo/cuda/driver.c +12 -4
  7. data/ext/cumo/cuda/memory_pool.cpp +104 -30
  8. data/ext/cumo/cuda/runtime.c +29 -0
  9. data/ext/cumo/include/cumo/cuda/cudnn.h +1 -1
  10. data/ext/cumo/include/cumo/cuda/cumo_thrust.hpp +8 -3
  11. data/ext/cumo/include/cumo/cuda/memory_pool.h +5 -0
  12. data/ext/cumo/include/cumo/cuda/runtime.h +12 -1
  13. data/ext/cumo/include/cumo/intern.h +5 -0
  14. data/ext/cumo/include/cumo/ndloop.h +1 -1
  15. data/ext/cumo/include/cumo/reduce_kernel.h +2 -0
  16. data/ext/cumo/include/cumo/template.h +18 -0
  17. data/ext/cumo/include/cumo/template_kernel.h +8 -1
  18. data/ext/cumo/include/cumo/types/complex_kernel.h +2 -2
  19. data/ext/cumo/include/cumo.h +2 -2
  20. data/ext/cumo/narray/array.c +1 -1
  21. data/ext/cumo/narray/data.c +32 -9
  22. data/ext/cumo/narray/data_kernel.cu +4 -0
  23. data/ext/cumo/narray/gen/spec.rb +1 -1
  24. data/ext/cumo/narray/gen/tmpl/accum.c +1 -0
  25. data/ext/cumo/narray/gen/tmpl/accum_arg.c +1 -0
  26. data/ext/cumo/narray/gen/tmpl/accum_binary_kernel.cu +13 -13
  27. data/ext/cumo/narray/gen/tmpl/accum_index.c +31 -4
  28. data/ext/cumo/narray/gen/tmpl/alloc_func.c +16 -6
  29. data/ext/cumo/narray/gen/tmpl/allocate.c +9 -0
  30. data/ext/cumo/narray/gen/tmpl/aref.c +6 -0
  31. data/ext/cumo/narray/gen/tmpl/aset.c +1 -1
  32. data/ext/cumo/narray/gen/tmpl/binary.c +11 -5
  33. data/ext/cumo/narray/gen/tmpl/binary2.c +24 -12
  34. data/ext/cumo/narray/gen/tmpl/binary2_kernel.cu +17 -3
  35. data/ext/cumo/narray/gen/tmpl/binary_kernel.cu +17 -4
  36. data/ext/cumo/narray/gen/tmpl/binary_s_kernel.cu +1 -0
  37. data/ext/cumo/narray/gen/tmpl/bincount.c +52 -10
  38. data/ext/cumo/narray/gen/tmpl/clip.c +70 -33
  39. data/ext/cumo/narray/gen/tmpl/clip_kernel.cu +58 -0
  40. data/ext/cumo/narray/gen/tmpl/complex_accum_kernel.cu +12 -4
  41. data/ext/cumo/narray/gen/tmpl/cond_binary_kernel.cu +1 -0
  42. data/ext/cumo/narray/gen/tmpl/ewcomp_kernel.cu +1 -0
  43. data/ext/cumo/narray/gen/tmpl/eye_kernel.cu +1 -0
  44. data/ext/cumo/narray/gen/tmpl/fill_kernel.cu +2 -0
  45. data/ext/cumo/narray/gen/tmpl/float_accum_kernel.cu +8 -0
  46. data/ext/cumo/narray/gen/tmpl/format.c +1 -1
  47. data/ext/cumo/narray/gen/tmpl/format_to_a.c +1 -1
  48. data/ext/cumo/narray/gen/tmpl/frexp.c +1 -0
  49. data/ext/cumo/narray/gen/tmpl/gemm.c +8 -2
  50. data/ext/cumo/narray/gen/tmpl/logseq.c +2 -1
  51. data/ext/cumo/narray/gen/tmpl/logseq_kernel.cu +2 -0
  52. data/ext/cumo/narray/gen/tmpl/median.c +1 -0
  53. data/ext/cumo/narray/gen/tmpl/minmax.c +9 -2
  54. data/ext/cumo/narray/gen/tmpl/new_dim0.c +8 -0
  55. data/ext/cumo/narray/gen/tmpl/new_dim0_kernel.cu +3 -0
  56. data/ext/cumo/narray/gen/tmpl/poly.c +9 -8
  57. data/ext/cumo/narray/gen/tmpl/pow_kernel.cu +2 -0
  58. data/ext/cumo/narray/gen/tmpl/rand.c +43 -14
  59. data/ext/cumo/narray/gen/tmpl/rand_kernel.cu +97 -0
  60. data/ext/cumo/narray/gen/tmpl/rand_norm.c +28 -57
  61. data/ext/cumo/narray/gen/tmpl/rand_norm_kernel.cu +73 -0
  62. data/ext/cumo/narray/gen/tmpl/real_accum_kernel.cu +43 -13
  63. data/ext/cumo/narray/gen/tmpl/seq.c +1 -1
  64. data/ext/cumo/narray/gen/tmpl/seq_kernel.cu +2 -0
  65. data/ext/cumo/narray/gen/tmpl/set2.c +1 -0
  66. data/ext/cumo/narray/gen/tmpl/store_array.c +20 -29
  67. data/ext/cumo/narray/gen/tmpl/store_array_kernel.cu +4 -0
  68. data/ext/cumo/narray/gen/tmpl/store_bit_kernel.cu +4 -0
  69. data/ext/cumo/narray/gen/tmpl/store_from_kernel.cu +4 -0
  70. data/ext/cumo/narray/gen/tmpl/unary.c +49 -34
  71. data/ext/cumo/narray/gen/tmpl/unary_kernel.cu +37 -15
  72. data/ext/cumo/narray/gen/tmpl/unary_ret2.c +1 -0
  73. data/ext/cumo/narray/gen/tmpl/unary_s_kernel.cu +4 -0
  74. data/ext/cumo/narray/gen/tmpl_bit/allocate.c +1 -0
  75. data/ext/cumo/narray/gen/tmpl_bit/aref.c +6 -0
  76. data/ext/cumo/narray/gen/tmpl_bit/bit_count_kernel.cu +4 -0
  77. data/ext/cumo/narray/gen/tmpl_bit/format.c +1 -1
  78. data/ext/cumo/narray/gen/tmpl_bit/format_to_a.c +1 -1
  79. data/ext/cumo/narray/gen/tmpl_bit/store_array.c +9 -10
  80. data/ext/cumo/narray/index.c +3 -3
  81. data/ext/cumo/narray/index_kernel.cu +11 -0
  82. data/ext/cumo/narray/narray.c +84 -21
  83. data/ext/cumo/narray/ndloop.c +64 -47
  84. data/ext/cumo/narray/ndloop_kernel.cu +2 -0
  85. data/ext/cumo/narray/rand.c +34 -2
  86. data/lib/cumo/narray/extra.rb +13 -12
  87. data/test/bit_test.rb +16 -0
  88. data/test/cuda/driver_test.rb +16 -0
  89. data/test/cuda/memory_pool_test.rb +123 -0
  90. data/test/cumo_test.rb +35 -0
  91. data/test/extra_test.rb +1095 -0
  92. data/test/math_test.rb +638 -0
  93. data/test/narray_alt_coverage_test.rb +856 -0
  94. data/test/narray_test.rb +831 -0
  95. metadata +11 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ddc7b756681afdf9fccd45652c422d2f67accfd1cf238d1fd05f6827fb5029d
4
- data.tar.gz: 47848503fd8a388b7dfee79d2da0e627032fd4d8c8717645da70d8f09770f599
3
+ metadata.gz: 12027799f8203c1e08409de1fdec1b550e26511547bbaee2a938488ecd19e079
4
+ data.tar.gz: 399daea2a183febf70ceeb1e6492479fd3a34ae29e994265c4da9f89644c3c4d
5
5
  SHA512:
6
- metadata.gz: f59ddc27c8f96a5eceec7fb4ae6665d1fae6dd20ef039a83563c0e0f5a76780fd2ab9b5ebbe06e116a3f01cd59c7493eee06dc6c1166a0bb82ba546c43331f03
7
- data.tar.gz: 16543c06d74f79089799442abc30741ffb2a26556da953410023b19711fd25e15adb05548f9664c79bf8430a3be9c8f15c0813dccfae4b5e918ffc55a5fc68c0
6
+ metadata.gz: d12b0fa56578c8ffd309aa314ab40c98d3b983375d7ddd33b0909ac47092d52b48ea1b53173ea843701587a0fb50442a907b459bd558a8c92e4525eedd1531fd
7
+ data.tar.gz: ba1b2bac6afb811d790c3f8d90906f3e7f571cbbdb8dc59e3ff6141fa54aa5b8f8a8d6e597ff3045e001c9ae5ff0cab6d9e6679ee6b8f5bbdb1642f203ba19b1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,52 @@
1
+ # 0.5.8 (2026/08/17)
2
+
3
+ Breaking changes:
4
+
5
+ * Random numbers are generated on the GPU, so `srand` no longer reproduces `Numo::NArray`'s values for the same seed. Reproducibility within Cumo is unchanged and no longer depends on how the calls are split (PR #223)
6
+ * Integer division by zero raises `ZeroDivisionError` instead of returning whatever the hardware produced, matching `Numo::NArray` (PR #218)
7
+ * A numeric index into an unallocated NArray raises instead of returning a view nothing ever reads (PR #225)
8
+
9
+ Fixes:
10
+
11
+ * Fix `rb_raise` from inside a C++ catch leaking the exception object (PR #229)
12
+ * Fix a failed Array store leaking its staging buffer, as much as the destination holds per failure (PR #228)
13
+ * Fix the GC free hook raising when a device free fails, which surfaced at whatever line triggered the collection (PR #227)
14
+ * Fix `from_binary` and `store_binary` writing managed memory while a kernel was still reading it (PR #226)
15
+ * Fix integer `sum` and `prod` truncating every partial the reduction merged (PR #224)
16
+ * Fix `minmax` ignoring compatible mode and returning zero-dimensional NArrays (PR #220)
17
+ * Fix `nan` poisoning `max` and an all-`nan` `min` answering `DBL_MAX` (PR #219)
18
+ * Fix `reshape!` and `marshal_load` mutating the array before they validate their arguments (PR #217)
19
+ * Fix `ptp` answering 1 whatever the input (PR #216)
20
+ * Fix `mulsum` ignoring the accumulator and the operand strides, which faulted the GPU on 8-bit dtypes (PR #215)
21
+ * Fix `dot` handing `gemm` operands of a different dtype, an out-of-bounds read (PR #214)
22
+ * Fix view offsets being added in the wrong unit (PR #213)
23
+ * Fix interpreter abort on a zero-dimensional view (PR #212)
24
+ * Fix `Cumo::RObject#logseq` returning `Infinity` (PR #210)
25
+ * Fix `to_i`, `to_f` and `to_c` recursing forever on a one-element array (PR #208)
26
+ * Fix complex `log2` and `log10` discarding the logarithm on the GPU (PR #207)
27
+ * Fix segfault in `max_index` and `min_index` with `nan: true` (PR #206)
28
+ * Fix `poly` failing on every input from an uninitialized `ndfunc` dimension (PR #205)
29
+ * Fix out-of-bounds writes in `bincount` from an overflowing length and a stale scan (PR #204)
30
+ * Fix `bincount` raising `TypeError` for every input (PR #203)
31
+ * Fix host loops reading device memory without synchronizing, which silently corrupted `minmax`, the `nan: true` reductions, `kahan_sum`, `modf`, `frexp` and `set_imag` (PR #202)
32
+ * Fix out-of-bounds access from three unchecked `size_t` multiplications (PR #201)
33
+ * Fix segfaults from an unvalidated marshal payload (PR #200)
34
+ * Fix `store_array` reading a source Array that its own elements rewrite (PR #198)
35
+ * Fix CUDA initialisation statuses being ignored at `require` time (PR #196)
36
+ * Fix kernel launch errors being discarded, which let a rejected launch return an untouched buffer as success (PR #195)
37
+ * Fix wrong `Cumo::RObject` results from driving host memory with asynchronous CUDA work (PR #194)
38
+ * Fix device memory being invisible to the GC, which let a program churning temporaries run the GPU out of memory (PR #221)
39
+
40
+ Changes:
41
+
42
+ * Run `rand` and `rand_norm` on the GPU (PR #223)
43
+ * Run `clip` on the GPU (PR #222)
44
+ * Correct spelling in messages, docs and comments (PR #211)
45
+ * Add the math, extra and narray test suites `numo-narray-alt` has and cumo lacked (PR #207, PR #208, PR #209)
46
+ * Add a `store_array` regression test for a shrunk source Array (PR #199)
47
+ * Stop the ccache cache growing past the 10 GB limit in CI (PR #197)
48
+ * Add a benchmark script under `bench/` (commit 3127f84)
49
+
1
50
  # 0.5.7 (2026/08/13)
2
51
 
3
52
  Breaking changes:
data/bench/bench.rb ADDED
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # ---------------------------------------------------------------------------
5
+ # Numo / Cumo comparison benchmark
6
+ #
7
+ # ruby bench.rb # CPU: numo-narray
8
+ # GPU=1 ruby bench.rb # GPU: cumo
9
+ # SIZE=4096 STEPS=500 ITER=3 ruby bench.rb
10
+ # PGM=0 ruby bench.rb # skip the PGM output
11
+ #
12
+ # To compare against numo-narray-alt (yoshoku's fork, SIMD enabled), swap the
13
+ # gem; this file needs no change, since the namespace stays Numo:
14
+ # gem uninstall numo-narray && gem install numo-narray-alt
15
+ #
16
+ # Environment variables that matter on the Cumo side:
17
+ # CUMO_SHOW_WARNING=ON warn wherever a CPU/GPU synchronization happens
18
+ # CUDA_LAUNCH_BLOCKING=1 synchronize on every launch (for profiling; slow)
19
+ # CUMO_MEMORY_POOL=OFF turn the memory pool off to measure allocation too
20
+ # CUDA_VISIBLE_DEVICES=0 device selection
21
+ #
22
+ # Numo::Bit, masked assignment and fancy indexing are deliberately unused. The
23
+ # divergence test is written with clip and floor arithmetic alone, so Numo and
24
+ # Cumo take exactly the same code path.
25
+ # ---------------------------------------------------------------------------
26
+
27
+ require 'benchmark'
28
+
29
+ GPU = !%w[0 false].include?(ENV['GPU'].to_s.downcase) && !ENV['GPU'].to_s.empty?
30
+
31
+ if GPU
32
+ require 'cumo/narray'
33
+ XM = Cumo
34
+ else
35
+ require 'numo/narray'
36
+ XM = Numo
37
+ end
38
+
39
+ SIZE = (ENV['SIZE'] || 1024).to_i # side of the mandelbrot / diffusion grid
40
+ STEPS = (ENV['STEPS'] || 200).to_i # number of iterations
41
+ NBODY = (ENV['NBODY'] || 2048).to_i # number of n-body particles
42
+ MC = (ENV['MC'] || 10_000_000).to_i
43
+ ITER = (ENV['ITER'] || 3).to_i # repeats per measurement (3-5 to steady it)
44
+ PGM = ENV['PGM'] != '0'
45
+
46
+ # --- helpers ---------------------------------------------------------------
47
+
48
+ # Cumo launches kernels asynchronously, so every measurement boundary syncs.
49
+ def sync
50
+ XM::CUDA::Runtime.cudaDeviceSynchronize if GPU
51
+ end
52
+
53
+ # Cumo returns a zero-dimensional NArray from sum / min / max / count_true,
54
+ # where Numo returns a Numeric.
55
+ def scalar(v)
56
+ return v if v.is_a?(Numeric)
57
+ v.respond_to?(:extract_cpu) ? v.extract_cpu : v.extract
58
+ end
59
+
60
+ def free(*arrays)
61
+ return unless GPU
62
+
63
+ arrays.each { |a| a.free if a.respond_to?(:free) }
64
+ end
65
+
66
+ def report(label)
67
+ result = nil
68
+ total = Benchmark.realtime do
69
+ ITER.times do
70
+ result = yield
71
+ sync
72
+ end
73
+ end
74
+ puts format(' %-12s %9.3f s total %9.3f s/iter', label, total, total / ITER)
75
+ result
76
+ end
77
+
78
+ def write_pgm(path, arr)
79
+ return unless PGM
80
+
81
+ lo = scalar(arr.min).to_f
82
+ hi = scalar(arr.max).to_f
83
+ hi = lo + 1.0 if hi <= lo
84
+ img = XM::UInt8.cast((arr - lo) / (hi - lo) * 255.0)
85
+ h, w = img.shape
86
+ bytes =
87
+ begin
88
+ img.to_binary
89
+ rescue StandardError
90
+ # fallback for a Cumo without to_binary (device to host copy)
91
+ img.to_a.flatten.pack('C*')
92
+ end
93
+ File.open(path, 'wb') do |f|
94
+ f.write("P5\n#{w} #{h}\n255\n")
95
+ f.write(bytes)
96
+ end
97
+ free(img)
98
+ puts " -> #{path}"
99
+ end
100
+
101
+ # --- 1. mandelbrot set (pure elementwise arithmetic) ------------------------
102
+ #
103
+ # Build an indicator that is 1.0 once diverged and 0.0 while still alive using
104
+ # clip and floor alone, then accumulate the survival count. No branches and no
105
+ # masks, so it maps straight onto the GPU.
106
+ def mandelbrot(n, steps)
107
+ lim = 1.0e10 # bound zr and zi here so neither Inf nor NaN appears
108
+ cr = XM::SFloat.new(1, n).seq / n * 3.0 - 2.0 # real axis [-2.0, 1.0)
109
+ ci = XM::SFloat.new(n, 1).seq / n * 3.0 - 1.5 # imaginary axis [-1.5, 1.5)
110
+ zr = XM::SFloat.zeros(n, n)
111
+ zi = XM::SFloat.zeros(n, n)
112
+ cnt = XM::SFloat.zeros(n, n)
113
+ cr = zr + cr # materialize the broadcast
114
+ ci = zr + ci
115
+
116
+ steps.times do
117
+ zr2 = zr * zr
118
+ zi2 = zi * zi
119
+ escaped = ((zr2 + zi2).clip(0.0, 4.0) / 4.0).floor # 1.0 once |z|^2 >= 4
120
+ cnt += 1.0 - escaped
121
+ zi = (2.0 * zr * zi + ci).clip(-lim, lim)
122
+ zr = (zr2 - zi2 + cr).clip(-lim, lim)
123
+ end
124
+ free(cr, ci, zr, zi)
125
+ cnt
126
+ end
127
+
128
+ # --- 2. 2-D heat diffusion (stencil / slice arithmetic) ---------------------
129
+ def diffusion(n, steps)
130
+ u = XM::SFloat.zeros(n, n)
131
+ h = [n / 8, 1].max
132
+ u[h...(2 * h), h...(2 * h)] = 255.0
133
+ u[(5 * h)...(7 * h), (3 * h)...(4 * h)] = 255.0
134
+ k = 0.2
135
+
136
+ steps.times do
137
+ lap = u[0..-3, 1..-2] + u[2..-1, 1..-2] +
138
+ u[1..-2, 0..-3] + u[1..-2, 2..-1] - 4.0 * u[1..-2, 1..-2]
139
+ u[1..-2, 1..-2] = u[1..-2, 1..-2] + k * lap
140
+ end
141
+ u
142
+ end
143
+
144
+ # --- 3. n-body (2-D, O(N^2) through broadcasting) ---------------------------
145
+ def nbody(n, steps)
146
+ dt = 1.0e-3
147
+ eps = 1.0e-3
148
+ x = XM::SFloat.new(n).rand * 2.0 - 1.0
149
+ y = XM::SFloat.new(n).rand * 2.0 - 1.0
150
+ vx = XM::SFloat.zeros(n)
151
+ vy = XM::SFloat.zeros(n)
152
+
153
+ steps.times do
154
+ dx = x.reshape(1, n) - x.reshape(n, 1) # [n, n]
155
+ dy = y.reshape(1, n) - y.reshape(n, 1)
156
+ r2 = dx * dx + dy * dy + eps
157
+ inv3 = 1.0 / (r2 * XM::NMath.sqrt(r2))
158
+ vx += (dx * inv3).sum(axis: 1) * dt
159
+ vy += (dy * inv3).sum(axis: 1) * dt
160
+ free(dx, dy, r2, inv3)
161
+ x += vx * dt
162
+ y += vy * dt
163
+ end
164
+ free(y, vx, vy)
165
+ x
166
+ end
167
+
168
+ # --- 4. monte carlo pi (random numbers + reduction) -------------------------
169
+ # The sum is SFloat, so a large n costs pi its accuracy to rounding. Speed is
170
+ # what this measures.
171
+ def monte_carlo_pi(n)
172
+ x = XM::SFloat.new(n).rand
173
+ y = XM::SFloat.new(n).rand
174
+ r2 = x * x + y * y
175
+ inside = (1.0 - r2.clip(0.0, 1.0)).ceil # 1.0 where r2 < 1
176
+ hits = scalar(inside.sum).to_f
177
+ free(x, y, r2, inside)
178
+ 4.0 * hits / n
179
+ end
180
+
181
+ # --- 5. compatibility probe (eyeball the incompatibilities the README lists) -
182
+ def compat_probe
183
+ a = XM::SFloat[1.0, 2.0, 3.0]
184
+ bit = (a > 1.5)
185
+ count = bit.respond_to?(:count_true_cpu) ? bit.count_true_cpu : bit.count_true
186
+ puts " (a > 1.5).class : #{bit.class}"
187
+ puts " count_true : #{count.inspect}"
188
+ puts " a.sum.class : #{a.sum.class}" # Numo: Float / Cumo: zero-dimensional NArray
189
+ puts " a.max.class : #{a.max.class}"
190
+ puts " a[0].class : #{a[0].class}"
191
+ free(a)
192
+ end
193
+
194
+ # --- main ------------------------------------------------------------------
195
+
196
+ version = begin
197
+ XM::NArray::VERSION
198
+ rescue StandardError
199
+ 'unknown'
200
+ end
201
+
202
+ puts "ruby : #{RUBY_VERSION} (#{RUBY_PLATFORM})"
203
+ puts "backend : #{XM} #{version}"
204
+ puts "params : SIZE=#{SIZE} STEPS=#{STEPS} NBODY=#{NBODY} MC=#{MC} ITER=#{ITER}"
205
+ puts
206
+
207
+ # warm up (keep GPU initialization and kernel JIT out of the measurement)
208
+ warm = XM::SFloat.new(64, 64).seq
209
+ scalar((warm * warm + warm).sum)
210
+ sync
211
+ free(warm)
212
+ GC.start
213
+
214
+ puts 'compat:'
215
+ compat_probe
216
+ puts
217
+
218
+ puts 'bench:'
219
+ cnt = report('mandelbrot') { mandelbrot(SIZE, STEPS) }
220
+ write_pgm('mandelbrot.pgm', cnt)
221
+ free(cnt)
222
+ GC.start
223
+
224
+ u = report('diffusion') { diffusion(SIZE, STEPS) }
225
+ write_pgm('diffusion.pgm', u)
226
+ free(u)
227
+ GC.start
228
+
229
+ pos = report('nbody') { nbody(NBODY, 10) }
230
+ free(pos)
231
+ GC.start
232
+
233
+ pi = nil
234
+ report('monte_carlo') { pi = monte_carlo_pi(MC) }
235
+ puts format(' pi ~= %.6f', pi)
@@ -51,14 +51,16 @@ cumo_cuda_cublas_handle()
51
51
  if (handles == 0) {
52
52
  int i;
53
53
  int device_count = cumo_cuda_runtime_get_device_count();
54
- handles = malloc(sizeof(cublasHandle_t) * device_count);
54
+ handles = ALLOC_N(cublasHandle_t, device_count);
55
55
  for (i = 0; i < device_count; ++i) {
56
56
  handles[i] = 0;
57
57
  }
58
58
  }
59
59
  device = cumo_cuda_runtime_get_device();
60
60
  if (handles[device] == 0) {
61
- cublasCreate(&handles[device]);
61
+ // A discarded status leaves the handle NULL, and cuBLAS reports that as
62
+ // CUBLAS_STATUS_NOT_INITIALIZED at the next call instead of the reason.
63
+ cumo_cuda_cublas_check_status(cublasCreate(&handles[device]));
62
64
  }
63
65
  return handles[device];
64
66
  }
@@ -30,14 +30,16 @@ cumo_cuda_cudnn_handle()
30
30
  if (handles == 0) {
31
31
  int i;
32
32
  int device_count = cumo_cuda_runtime_get_device_count();
33
- handles = malloc(sizeof(cudnnHandle_t) * device_count);
33
+ handles = ALLOC_N(cudnnHandle_t, device_count);
34
34
  for (i = 0; i < device_count; ++i) {
35
35
  handles[i] = 0;
36
36
  }
37
37
  }
38
38
  device = cumo_cuda_runtime_get_device();
39
39
  if (handles[device] == 0) {
40
- cudnnCreate(&handles[device]);
40
+ // A discarded status leaves the handle NULL, and cuDNN reports that as
41
+ // CUDNN_STATUS_NOT_INITIALIZED at the next call instead of the reason.
42
+ cumo_cuda_cudnn_check_status(cudnnCreate(&handles[device]));
41
43
  }
42
44
  return handles[device];
43
45
  }
@@ -445,11 +445,19 @@ Init_cumo_cuda_driver()
445
445
  rb_define_const(mDriver, "CU_JIT_INPUT_OBJECT", INT2NUM(CU_JIT_INPUT_OBJECT));
446
446
  rb_define_const(mDriver, "CU_JIT_INPUT_PTX", INT2NUM(CU_JIT_INPUT_PTX));
447
447
 
448
- cuInit(0);
449
- cuDeviceGet(&cuDevice, 0);
448
+ check_status(cuInit(0));
449
+
450
+ // A driver API call needs a current context, and the runtime API only
451
+ // creates its primary one once an array operation happens, so this covers
452
+ // the gap in between. Losing it is not fatal -- everything but a driver
453
+ // call made before any array operation still works -- so a device that
454
+ // refuses a context is left for that call to report. cuDeviceGet leaves
455
+ // cuDevice untouched when it fails, hence the guard.
456
+ if (cuDeviceGet(&cuDevice, 0) == CUDA_SUCCESS) {
450
457
  #if defined(CUDA_VERSION) && CUDA_VERSION >= 13000
451
- cuCtxCreate(&context, NULL, 0, cuDevice);
458
+ cuCtxCreate(&context, NULL, 0, cuDevice);
452
459
  #else
453
- cuCtxCreate(&context, 0, cuDevice);
460
+ cuCtxCreate(&context, 0, cuDevice);
454
461
  #endif
462
+ }
455
463
  }
@@ -4,6 +4,7 @@
4
4
  #include "cumo/cuda/memory_pool.h"
5
5
  #include "cumo/cuda/runtime.h"
6
6
 
7
+ #include <cstdio>
7
8
  #include <cstdlib>
8
9
  #include <string>
9
10
 
@@ -19,36 +20,83 @@ static bool memory_pool_enabled;
19
20
 
20
21
  VALUE cumo_cuda_eOutOfMemoryError;
21
22
 
23
+ // How a call into the pool ended, so that the raise can wait until the handler
24
+ // has been left. rb_raise longjmps, and a longjmp out of an active handler
25
+ // skips __cxa_end_catch: the exception object is never destroyed and leaks for
26
+ // the life of the process.
27
+ enum pool_outcome {
28
+ POOL_OK,
29
+ POOL_CUDA_ERROR,
30
+ POOL_OUT_OF_MEMORY,
31
+ POOL_UNKNOWN_ERROR,
32
+ };
33
+
34
+ struct pool_error {
35
+ cudaError_t status;
36
+ char message[256];
37
+ };
38
+
39
+ static enum pool_outcome
40
+ pool_malloc(size_t size, char **ptr, struct pool_error *err)
41
+ {
42
+ try {
43
+ // TODO(sonots): Get current CUDA stream and pass it
44
+ *ptr = reinterpret_cast<char*>(pool.Malloc(size));
45
+ return POOL_OK;
46
+ } catch (const cumo::internal::CUDARuntimeError& e) {
47
+ err->status = e.status();
48
+ return POOL_CUDA_ERROR;
49
+ } catch (const cumo::internal::OutOfMemoryError& e) {
50
+ // A std::string here would in turn be leaked by the raise.
51
+ std::snprintf(err->message, sizeof(err->message), "%s", e.what());
52
+ return POOL_OUT_OF_MEMORY;
53
+ } catch (const std::exception& e) {
54
+ // Nothing may reach the C caller: it has no handler, so an escaping
55
+ // exception is std::terminate.
56
+ std::snprintf(err->message, sizeof(err->message), "%s", e.what());
57
+ return POOL_UNKNOWN_ERROR;
58
+ } catch (...) {
59
+ std::snprintf(err->message, sizeof(err->message), "unknown C++ exception");
60
+ return POOL_UNKNOWN_ERROR;
61
+ }
62
+ }
63
+
22
64
  char*
23
65
  cumo_cuda_runtime_malloc(size_t size)
24
66
  {
25
- if (memory_pool_enabled) {
26
- try {
27
- // TODO(sonots): Get current CUDA stream and pass it
28
- return reinterpret_cast<char*>(pool.Malloc(size));
29
- } catch (const cumo::internal::CUDARuntimeError& e) {
30
- cumo_cuda_runtime_check_status(e.status());
31
- } catch (const cumo::internal::OutOfMemoryError& e) {
32
- // retry after GC
33
- rb_funcall(rb_define_module("GC"), rb_intern("start"), 0);
34
- try {
35
- return reinterpret_cast<char*>(pool.Malloc(size));
36
- } catch (const cumo::internal::CUDARuntimeError& e) {
37
- cumo_cuda_runtime_check_status(e.status());
38
- } catch (const cumo::internal::OutOfMemoryError& e) {
39
- rb_raise(cumo_cuda_eOutOfMemoryError, "%s", e.what());
40
- }
41
- }
42
- } else {
43
- void *ptr = 0;
44
- cumo_cuda_runtime_check_status(cudaMallocManaged(&ptr, size, cudaMemAttachGlobal));
45
- return reinterpret_cast<char*>(ptr);
67
+ char *ptr = 0;
68
+ struct pool_error err;
69
+ enum pool_outcome outcome;
70
+
71
+ if (!memory_pool_enabled) {
72
+ void *raw = 0;
73
+ cumo_cuda_runtime_check_status(cudaMallocManaged(&raw, size, cudaMemAttachGlobal));
74
+ return reinterpret_cast<char*>(raw);
75
+ }
76
+
77
+ outcome = pool_malloc(size, &ptr, &err);
78
+ if (outcome == POOL_OUT_OF_MEMORY) {
79
+ // retry after GC
80
+ rb_funcall(rb_define_module("GC"), rb_intern("start"), 0);
81
+ outcome = pool_malloc(size, &ptr, &err);
82
+ }
83
+
84
+ switch (outcome) {
85
+ case POOL_OK:
86
+ return ptr;
87
+ case POOL_CUDA_ERROR:
88
+ cumo_cuda_runtime_check_status(err.status);
89
+ break;
90
+ case POOL_OUT_OF_MEMORY:
91
+ rb_raise(cumo_cuda_eOutOfMemoryError, "%s", err.message);
92
+ case POOL_UNKNOWN_ERROR:
93
+ rb_raise(rb_eRuntimeError, "%s", err.message);
46
94
  }
47
95
  return 0; // should not reach here
48
96
  }
49
97
 
50
- void
51
- cumo_cuda_runtime_free(char *ptr)
98
+ static cudaError_t
99
+ runtime_free(char *ptr)
52
100
  {
53
101
  // Always offer the pointer to the pool first, whatever memory_pool_enabled
54
102
  // says now: MemoryPool.enable/disable is public, so the state can differ
@@ -57,14 +105,35 @@ cumo_cuda_runtime_free(char *ptr)
57
105
  // which is not at the head of its buffer.
58
106
  try {
59
107
  if (pool.Free(reinterpret_cast<intptr_t>(ptr))) {
60
- return;
108
+ return cudaSuccess;
61
109
  }
62
110
  } catch (const cumo::internal::CUDARuntimeError& e) {
63
- cumo_cuda_runtime_check_status(e.status());
64
- return;
111
+ return e.status();
112
+ } catch (...) {
113
+ return cudaErrorUnknown;
65
114
  }
66
115
  // No pool owns it, so it came straight from cudaMallocManaged.
67
- cumo_cuda_runtime_check_status(cudaFree((void*)ptr));
116
+ return cudaFree((void*)ptr);
117
+ }
118
+
119
+ void
120
+ cumo_cuda_runtime_free(char *ptr)
121
+ {
122
+ cumo_cuda_runtime_check_status(runtime_free(ptr));
123
+ }
124
+
125
+ void
126
+ cumo_cuda_runtime_free_no_raise(char *ptr)
127
+ {
128
+ cudaError_t status = runtime_free(ptr);
129
+ // rb_raise from a GC free hook longjmps out of the sweep, so report the
130
+ // failure the way ~Memory does. cudaFree only fails once the context is
131
+ // unusable, and the next runtime call reports the same status anyway.
132
+ if (status == cudaSuccess || status == cudaErrorCudartUnloading) {
133
+ return;
134
+ }
135
+ std::fprintf(stderr, "cumo: failed to free device memory: %s (error=%d)\n",
136
+ cudaGetErrorString(status), static_cast<int>(status));
68
137
  }
69
138
 
70
139
  /*
@@ -110,17 +179,22 @@ rb_memory_pool_enabled_p(VALUE self)
110
179
  static VALUE
111
180
  rb_memory_pool_free_all_blocks(int argc, VALUE* argv, VALUE self)
112
181
  {
182
+ // TODO(sonots): FIX if we create a Stream object
183
+ cudaStream_t stream_ptr = (argc < 1) ? 0 : (cudaStream_t)NUM2SIZET(argv[0]);
184
+ cudaError_t status = cudaSuccess;
185
+
113
186
  try {
114
187
  if (argc < 1) {
115
188
  pool.FreeAllBlocks();
116
189
  } else {
117
- // TODO(sonots): FIX if we create a Stream object
118
- cudaStream_t stream_ptr = (cudaStream_t)NUM2SIZET(argv[0]);
119
190
  pool.FreeAllBlocks(stream_ptr);
120
191
  }
121
192
  } catch (const cumo::internal::CUDARuntimeError& e) {
122
- cumo_cuda_runtime_check_status(e.status());
193
+ status = e.status();
194
+ } catch (...) {
195
+ status = cudaErrorUnknown;
123
196
  }
197
+ cumo_cuda_runtime_check_status(status);
124
198
  return Qnil;
125
199
  }
126
200
 
@@ -10,6 +10,35 @@ VALUE cumo_cuda_mRuntime;
10
10
 
11
11
  #define check_status(status) (cumo_cuda_runtime_check_status((status)))
12
12
 
13
+ // Called right after a <<<>>> launch. cudaGetLastError() reports errors the
14
+ // launch itself was rejected for; a fault while the kernel runs is asynchronous
15
+ // and still surfaces at a later call.
16
+ void
17
+ cumo_cuda_runtime_check_kernel_launch(void)
18
+ {
19
+ check_status(cudaGetLastError());
20
+ }
21
+
22
+ int*
23
+ cumo_cuda_runtime_error_flag_new(void)
24
+ {
25
+ static int *flag = NULL;
26
+
27
+ if (flag == NULL) {
28
+ check_status(cudaHostAlloc((void**)&flag, sizeof(int),
29
+ cudaHostAllocMapped | cudaHostAllocPortable));
30
+ }
31
+ *flag = 0;
32
+ return flag;
33
+ }
34
+
35
+ bool
36
+ cumo_cuda_runtime_error_flag_get(int *flag)
37
+ {
38
+ check_status(cudaDeviceSynchronize());
39
+ return (*flag != 0);
40
+ }
41
+
13
42
  ///////////////////////////////////////////
14
43
  // Version Management
15
44
  ///////////////////////////////////////////
@@ -40,7 +40,7 @@ extern VALUE cumo_na_eShapeError;
40
40
  #define CUMO_CUDA_CUDNN_CHECK_DIM_EQ(nd1,nd2) \
41
41
  if ((nd1) != (nd2)) { \
42
42
  rb_raise(cumo_na_eShapeError, \
43
- "dimention mismatch: %d != %d", \
43
+ "dimension mismatch: %d != %d", \
44
44
  (int)(nd1), (int)(nd2)); \
45
45
  }
46
46
 
@@ -53,7 +53,12 @@ class cumo_thrust_strided_range
53
53
 
54
54
  // construct strided_range for the range [first,last)
55
55
  cumo_thrust_strided_range(Iterator first, Iterator last, difference_type stride)
56
- : first(first), last(last), stride(stride) {}
56
+ : first(first), stride(stride), count(((last - first) + (stride - 1)) / stride) {}
57
+
58
+ // A stride of 0 repeats one element and a negative stride walks backwards, so
59
+ // neither can be counted from (last - first); those callers pass the count.
60
+ cumo_thrust_strided_range(Iterator first, difference_type stride, difference_type count)
61
+ : first(first), stride(stride), count(count) {}
57
62
 
58
63
  iterator begin(void) const
59
64
  {
@@ -62,13 +67,13 @@ class cumo_thrust_strided_range
62
67
 
63
68
  iterator end(void) const
64
69
  {
65
- return begin() + ((last - first) + (stride - 1)) / stride;
70
+ return begin() + count;
66
71
  }
67
72
 
68
73
  protected:
69
74
  Iterator first;
70
- Iterator last;
71
75
  difference_type stride;
76
+ difference_type count;
72
77
  };
73
78
 
74
79
 
@@ -18,6 +18,11 @@ cumo_cuda_runtime_malloc(size_t size);
18
18
  void
19
19
  cumo_cuda_runtime_free(char *ptr);
20
20
 
21
+ // For GC free hooks, which cannot take a raise: reports a failed free on stderr
22
+ // instead of raising.
23
+ void
24
+ cumo_cuda_runtime_free_no_raise(char *ptr);
25
+
21
26
  #if defined(__cplusplus)
22
27
  #if 0
23
28
  { /* satisfy cc-mode */
@@ -45,9 +45,20 @@ cumo_cuda_runtime_is_device_memory(void* ptr)
45
45
  if (!ptr) { return false; }
46
46
  status = cudaPointerGetAttributes(&attrs, ptr);
47
47
  cudaGetLastError(); // reset last error to success
48
- return (status != cudaErrorInvalidValue);
48
+ // Since CUDA 11 this succeeds for host memory as well and reports the kind
49
+ // in attrs.type, so the status alone no longer tells the two apart.
50
+ if (status != cudaSuccess) { return false; }
51
+ return (attrs.type != cudaMemoryTypeUnregistered);
49
52
  }
50
53
 
54
+ // A kernel cannot raise, so it reports a bad argument through this flag and the
55
+ // caller turns it into an exception. The buffer is pinned host memory the device
56
+ // writes through, not pool memory: a four-byte pool allocation lands in the same
57
+ // bin as a small output array, and the managed page then migrates back and forth
58
+ // once per operation.
59
+ int* cumo_cuda_runtime_error_flag_new(void);
60
+ bool cumo_cuda_runtime_error_flag_get(int *flag);
61
+
51
62
  #if defined(__cplusplus)
52
63
  #if 0
53
64
  { /* satisfy cc-mode */
@@ -84,6 +84,11 @@ VALUE cumo_na_at_main(int nidx, VALUE *idx, VALUE self, int keep_dim, int result
84
84
  // defined in array, used in math
85
85
  VALUE cumo_na_ary_composition_dtype(VALUE ary);
86
86
 
87
+ // defined in rand, used by the generated rand and rand_norm
88
+ u_int64_t cumo_cuda_rand_seed(void);
89
+ u_int64_t cumo_cuda_rand_offset(void);
90
+ void cumo_cuda_rand_set_offset(u_int64_t offset);
91
+
87
92
  #include "ruby/version.h"
88
93
 
89
94
  #if RUBY_API_VERSION_CODE == 20100 // 2.1.0