cumo 0.5.7 → 0.5.9

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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +85 -0
  3. data/bench/bench.rb +235 -0
  4. data/bench/cg_bench.rb +271 -0
  5. data/bench/crossover_bench.rb +374 -0
  6. data/bench/cumo_probe.rb +348 -0
  7. data/bench/cumo_sync_probe.rb +442 -0
  8. data/bench/ising_bench.rb +242 -0
  9. data/bench/kmeans_bench.rb +254 -0
  10. data/bench/particle_bench.rb +261 -0
  11. data/bench/transformer_bench.rb +264 -0
  12. data/ext/cumo/cuda/cublas.c +4 -2
  13. data/ext/cumo/cuda/cudnn.c +4 -2
  14. data/ext/cumo/cuda/driver.c +12 -4
  15. data/ext/cumo/cuda/memory_pool.cpp +104 -30
  16. data/ext/cumo/cuda/runtime.c +58 -0
  17. data/ext/cumo/extconf.rb +4 -0
  18. data/ext/cumo/include/cumo/cuda/cudnn.h +1 -1
  19. data/ext/cumo/include/cumo/cuda/cumo_thrust.hpp +24 -122
  20. data/ext/cumo/include/cumo/cuda/memory_pool.h +5 -0
  21. data/ext/cumo/include/cumo/cuda/runtime.h +17 -1
  22. data/ext/cumo/include/cumo/indexer.h +152 -2
  23. data/ext/cumo/include/cumo/intern.h +5 -0
  24. data/ext/cumo/include/cumo/ndloop.h +1 -1
  25. data/ext/cumo/include/cumo/reduce_kernel.h +494 -107
  26. data/ext/cumo/include/cumo/template.h +44 -4
  27. data/ext/cumo/include/cumo/template_kernel.h +18 -2
  28. data/ext/cumo/include/cumo/types/bit_kernel.h +122 -0
  29. data/ext/cumo/include/cumo/types/complex_kernel.h +2 -2
  30. data/ext/cumo/include/cumo/types/complex_macro_kernel.h +1 -38
  31. data/ext/cumo/include/cumo/types/real_accum_kernel.h +0 -37
  32. data/ext/cumo/include/cumo/types/xint_macro_kernel.h +2 -2
  33. data/ext/cumo/include/cumo.h +2 -2
  34. data/ext/cumo/narray/array.c +1 -1
  35. data/ext/cumo/narray/data.c +53 -9
  36. data/ext/cumo/narray/data_kernel.cu +96 -0
  37. data/ext/cumo/narray/gen/spec.rb +1 -1
  38. data/ext/cumo/narray/gen/tmpl/accum.c +10 -2
  39. data/ext/cumo/narray/gen/tmpl/accum_arg.c +17 -3
  40. data/ext/cumo/narray/gen/tmpl/accum_arg_kernel.cu +4 -2
  41. data/ext/cumo/narray/gen/tmpl/accum_binary_kernel.cu +67 -37
  42. data/ext/cumo/narray/gen/tmpl/accum_index.c +32 -5
  43. data/ext/cumo/narray/gen/tmpl/accum_index_kernel.cu +4 -2
  44. data/ext/cumo/narray/gen/tmpl/alloc_func.c +16 -6
  45. data/ext/cumo/narray/gen/tmpl/allocate.c +9 -0
  46. data/ext/cumo/narray/gen/tmpl/aref.c +6 -0
  47. data/ext/cumo/narray/gen/tmpl/aset.c +1 -1
  48. data/ext/cumo/narray/gen/tmpl/binary.c +11 -5
  49. data/ext/cumo/narray/gen/tmpl/binary2.c +41 -19
  50. data/ext/cumo/narray/gen/tmpl/binary2_kernel.cu +37 -7
  51. data/ext/cumo/narray/gen/tmpl/binary_kernel.cu +17 -4
  52. data/ext/cumo/narray/gen/tmpl/binary_s_kernel.cu +1 -0
  53. data/ext/cumo/narray/gen/tmpl/bincount.c +79 -41
  54. data/ext/cumo/narray/gen/tmpl/bincount_kernel.cu +86 -0
  55. data/ext/cumo/narray/gen/tmpl/clip.c +112 -49
  56. data/ext/cumo/narray/gen/tmpl/clip_kernel.cu +90 -0
  57. data/ext/cumo/narray/gen/tmpl/complex_accum_kernel.cu +62 -82
  58. data/ext/cumo/narray/gen/tmpl/cond_binary.c +21 -11
  59. data/ext/cumo/narray/gen/tmpl/cond_binary_kernel.cu +22 -9
  60. data/ext/cumo/narray/gen/tmpl/cond_unary.c +47 -26
  61. data/ext/cumo/narray/gen/tmpl/cond_unary_kernel.cu +30 -0
  62. data/ext/cumo/narray/gen/tmpl/cum.c +13 -0
  63. data/ext/cumo/narray/gen/tmpl/cum_kernel.cu +72 -0
  64. data/ext/cumo/narray/gen/tmpl/ewcomp.c +20 -12
  65. data/ext/cumo/narray/gen/tmpl/ewcomp_kernel.cu +23 -7
  66. data/ext/cumo/narray/gen/tmpl/eye_kernel.cu +1 -0
  67. data/ext/cumo/narray/gen/tmpl/fill.c +32 -23
  68. data/ext/cumo/narray/gen/tmpl/fill_kernel.cu +19 -20
  69. data/ext/cumo/narray/gen/tmpl/float_accum_kernel.cu +56 -75
  70. data/ext/cumo/narray/gen/tmpl/format.c +1 -1
  71. data/ext/cumo/narray/gen/tmpl/format_to_a.c +1 -1
  72. data/ext/cumo/narray/gen/tmpl/frexp.c +9 -17
  73. data/ext/cumo/narray/gen/tmpl/frexp_kernel.cu +31 -0
  74. data/ext/cumo/narray/gen/tmpl/gemm.c +8 -2
  75. data/ext/cumo/narray/gen/tmpl/inspect.c +1 -0
  76. data/ext/cumo/narray/gen/tmpl/logseq.c +2 -1
  77. data/ext/cumo/narray/gen/tmpl/logseq_kernel.cu +2 -0
  78. data/ext/cumo/narray/gen/tmpl/median.c +33 -0
  79. data/ext/cumo/narray/gen/tmpl/minmax.c +45 -12
  80. data/ext/cumo/narray/gen/tmpl/new_dim0.c +8 -0
  81. data/ext/cumo/narray/gen/tmpl/new_dim0_kernel.cu +3 -0
  82. data/ext/cumo/narray/gen/tmpl/poly.c +36 -8
  83. data/ext/cumo/narray/gen/tmpl/poly_kernel.cu +46 -0
  84. data/ext/cumo/narray/gen/tmpl/pow.c +39 -18
  85. data/ext/cumo/narray/gen/tmpl/pow_kernel.cu +44 -14
  86. data/ext/cumo/narray/gen/tmpl/rand.c +43 -14
  87. data/ext/cumo/narray/gen/tmpl/rand_kernel.cu +97 -0
  88. data/ext/cumo/narray/gen/tmpl/rand_norm.c +28 -57
  89. data/ext/cumo/narray/gen/tmpl/rand_norm_kernel.cu +73 -0
  90. data/ext/cumo/narray/gen/tmpl/real_accum_kernel.cu +81 -17
  91. data/ext/cumo/narray/gen/tmpl/seq.c +18 -16
  92. data/ext/cumo/narray/gen/tmpl/seq_kernel.cu +19 -22
  93. data/ext/cumo/narray/gen/tmpl/set2.c +8 -44
  94. data/ext/cumo/narray/gen/tmpl/set2_kernel.cu +28 -0
  95. data/ext/cumo/narray/gen/tmpl/sort.c +41 -0
  96. data/ext/cumo/narray/gen/tmpl/sort_index.c +48 -0
  97. data/ext/cumo/narray/gen/tmpl/store_array.c +25 -29
  98. data/ext/cumo/narray/gen/tmpl/store_array_kernel.cu +4 -0
  99. data/ext/cumo/narray/gen/tmpl/store_bit.c +17 -19
  100. data/ext/cumo/narray/gen/tmpl/store_bit_kernel.cu +22 -57
  101. data/ext/cumo/narray/gen/tmpl/store_from.c +48 -17
  102. data/ext/cumo/narray/gen/tmpl/store_from_kernel.cu +41 -45
  103. data/ext/cumo/narray/gen/tmpl/unary.c +19 -36
  104. data/ext/cumo/narray/gen/tmpl/unary2.c +61 -29
  105. data/ext/cumo/narray/gen/tmpl/unary2_kernel.cu +44 -0
  106. data/ext/cumo/narray/gen/tmpl/unary_kernel.cu +34 -62
  107. data/ext/cumo/narray/gen/tmpl/unary_ret2.c +9 -16
  108. data/ext/cumo/narray/gen/tmpl/unary_ret2_kernel.cu +32 -0
  109. data/ext/cumo/narray/gen/tmpl/unary_s.c +14 -19
  110. data/ext/cumo/narray/gen/tmpl/unary_s_kernel.cu +23 -51
  111. data/ext/cumo/narray/gen/tmpl_bit/allocate.c +1 -0
  112. data/ext/cumo/narray/gen/tmpl_bit/aref.c +6 -0
  113. data/ext/cumo/narray/gen/tmpl_bit/binary.c +25 -110
  114. data/ext/cumo/narray/gen/tmpl_bit/binary_kernel.cu +52 -0
  115. data/ext/cumo/narray/gen/tmpl_bit/bit_count_kernel.cu +4 -0
  116. data/ext/cumo/narray/gen/tmpl_bit/bit_reduce.c +18 -4
  117. data/ext/cumo/narray/gen/tmpl_bit/bit_reduce_kernel.cu +77 -0
  118. data/ext/cumo/narray/gen/tmpl_bit/extract.c +7 -1
  119. data/ext/cumo/narray/gen/tmpl_bit/extract_cpu.c +7 -1
  120. data/ext/cumo/narray/gen/tmpl_bit/fill.c +21 -37
  121. data/ext/cumo/narray/gen/tmpl_bit/fill_kernel.cu +42 -0
  122. data/ext/cumo/narray/gen/tmpl_bit/format.c +1 -1
  123. data/ext/cumo/narray/gen/tmpl_bit/format_to_a.c +1 -1
  124. data/ext/cumo/narray/gen/tmpl_bit/store_array.c +76 -44
  125. data/ext/cumo/narray/gen/tmpl_bit/store_array_kernel.cu +89 -0
  126. data/ext/cumo/narray/gen/tmpl_bit/store_bit.c +26 -73
  127. data/ext/cumo/narray/gen/tmpl_bit/store_bit_kernel.cu +47 -0
  128. data/ext/cumo/narray/gen/tmpl_bit/store_from.c +21 -1
  129. data/ext/cumo/narray/gen/tmpl_bit/store_from_kernel.cu +30 -0
  130. data/ext/cumo/narray/gen/tmpl_bit/unary.c +23 -78
  131. data/ext/cumo/narray/gen/tmpl_bit/unary_kernel.cu +48 -0
  132. data/ext/cumo/narray/gen/tmpl_bit/where.c +42 -5
  133. data/ext/cumo/narray/gen/tmpl_bit/where2.c +18 -5
  134. data/ext/cumo/narray/gen/tmpl_bit/where_kernel.cu +107 -0
  135. data/ext/cumo/narray/index.c +4 -3
  136. data/ext/cumo/narray/index_kernel.cu +11 -0
  137. data/ext/cumo/narray/narray.c +90 -24
  138. data/ext/cumo/narray/ndloop.c +91 -47
  139. data/ext/cumo/narray/ndloop_kernel.cu +2 -0
  140. data/ext/cumo/narray/rand.c +34 -2
  141. data/ext/cumo/narray/sort_kernel.cu +345 -0
  142. data/lib/cumo/narray/extra.rb +13 -12
  143. data/test/bit_test.rb +16 -0
  144. data/test/cuda/driver_test.rb +16 -0
  145. data/test/cuda/memory_pool_test.rb +123 -0
  146. data/test/cumo_test.rb +35 -0
  147. data/test/extra_test.rb +1095 -0
  148. data/test/math_test.rb +638 -0
  149. data/test/narray_alt_coverage_test.rb +856 -0
  150. data/test/narray_test.rb +2901 -4
  151. metadata +36 -2
  152. data/ext/cumo/include/cumo/cuda/cumo_thrust_complex.hpp +0 -79
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ddc7b756681afdf9fccd45652c422d2f67accfd1cf238d1fd05f6827fb5029d
4
- data.tar.gz: 47848503fd8a388b7dfee79d2da0e627032fd4d8c8717645da70d8f09770f599
3
+ metadata.gz: f211eb1858b22b8418a8494efb431afeca60f023de409d74e6bdb1ae1fc3753a
4
+ data.tar.gz: '085be7ea42f37e235945e62783c51522c206c22eb539557b965812cc71ce422d'
5
5
  SHA512:
6
- metadata.gz: f59ddc27c8f96a5eceec7fb4ae6665d1fae6dd20ef039a83563c0e0f5a76780fd2ab9b5ebbe06e116a3f01cd59c7493eee06dc6c1166a0bb82ba546c43331f03
7
- data.tar.gz: 16543c06d74f79089799442abc30741ffb2a26556da953410023b19711fd25e15adb05548f9664c79bf8430a3be9c8f15c0813dccfae4b5e918ffc55a5fc68c0
6
+ metadata.gz: 19a6ca3c65b190e2347624232fa33302f4485e84d90df7de6972d727d76353e20a15a00bf8a7c8c96d281a97bbd7f8084016b8c2961e6781e4d94b00b17287d1
7
+ data.tar.gz: 2df512d0a5e2b201cd82cc91d4dc258f4d3463097e2969250ab0aa81ba591abaaa89ea0c6d1e818d8b85f32d698a6b9cc529d961a8e432be540fb7f4144f2e70
data/CHANGELOG.md CHANGED
@@ -1,3 +1,88 @@
1
+ # 0.5.9 (2026/08/20)
2
+
3
+ Breaking changes:
4
+
5
+ * `sort_index` resolves ties to the lowest index, the GPU sort being stable, where the host quicksort left them in whatever order it produced (PR #261)
6
+
7
+ Fixes:
8
+
9
+ * Fix `a[[2, 1], true].store(array)` writing every row onto row 0, on every dtype: ndloop read the index array on the host before the kernel that fills it had run (PR #265)
10
+ * Fix a segmentation fault storing a Ruby Array into a reversed `Cumo::Bit` view that starts past the first word (PR #266)
11
+ * Fix `sort` in place of a view backed by an index array reading row 0 for every row (PR #262)
12
+ * Fix a `Cumo::RObject` store reading an index array before the kernel writes it, which answered zeroes from the second store onwards (PR #258)
13
+ * Fix a store of an Array of narrays into an indexed destination writing one element per row (PR #257)
14
+ * Fix the zero fill of a row stored from a shorter narray starting one element too far (PR #256)
15
+ * Fix grid-stride kernels hanging from about 2**32 elements, the step wrapping to zero (PR #240)
16
+ * Fix `Cumo::Bit` views with a negative step reading out of bounds (PR #238)
17
+ * Fix index reductions answering a later index on ties, where `Numo::NArray` answers the first (PR #236)
18
+ * Fix wrong values from reductions with `nan: true` (PR #234)
19
+
20
+ Changes:
21
+
22
+ * Run `sort`, `sort_index` and `median` on the GPU with `cub::DeviceSegmentedRadixSort`; 1M `SFloat` `sort` 115.7 -> 0.47 ms, `median` 107.8 -> 0.13 ms, `sort_index` 115.8 -> 1.69 ms (PR #259, PR #260, PR #261)
23
+ * Run `poly` on the GPU, which had called its iterator once per element behind a synchronization; 2^20 `DFloat` 163.8 -> 0.021 ms (PR #263)
24
+ * Run `bincount` on the GPU; 1M `Int32` 1.62 -> 0.05 ms (PR #255)
25
+ * Run `cumsum` and `cumprod` on the GPU (PR #242)
26
+ * Run `minmax`, `abs`, `isnan` and the rest of `cond_unary`, `modf` and `frexp` on the GPU (PR #230, PR #231, PR #232, PR #233)
27
+ * Run the `Cumo::Bit` operators, reductions, `where`, `where2`, `fill` and the stores on the GPU (PR #237, PR #239, PR #241, PR #252, PR #254, PR #264, PR #266)
28
+ * Run complex `real=` and `imag=` on the GPU; 2^22 `DComplex` 4.63 -> 0.45 ms (PR #267)
29
+ * Run the accumulating reductions and `mulsum` on more than one GPU thread, where they had been running device-side thrust in a single thread (PR #243, PR #244)
30
+ * Address a whole non-contiguous view in one kernel launch instead of one per row, in the elementwise templates, the stores, the copies and the Bit operands (PR #245, PR #246, PR #249, PR #251, PR #252)
31
+ * Read a flat reduction operand without the indexer, and reduce over an outer axis with host-side addressing; `sum(axis: 0)` on 4096x4096 2.01 -> 0.017 ms (PR #248, PR #253)
32
+ * Split a reduction with too few outputs across more blocks (PR #235)
33
+ * Build the C++ and CUDA sources with optimization: mkmf leaves `$(optflags)` out of `CXXFLAGS`, so every `.cpp` and the host half of every `.cu` had been built at `-O0`; `a + b` 2.86 -> 2.00 us (PR #250)
34
+ * Warn about the synchronization `a[idx]` and `inspect` perform (PR #247)
35
+ * Add benchmark scripts under `bench/` for a transformer block, k-means, a 2D Ising model, conjugate gradient, a particle simulation and the CPU/GPU crossover, and two probes for calls that synchronize or launch one kernel per row
36
+
37
+ # 0.5.8 (2026/08/17)
38
+
39
+ Breaking changes:
40
+
41
+ * 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)
42
+ * Integer division by zero raises `ZeroDivisionError` instead of returning whatever the hardware produced, matching `Numo::NArray` (PR #218)
43
+ * A numeric index into an unallocated NArray raises instead of returning a view nothing ever reads (PR #225)
44
+
45
+ Fixes:
46
+
47
+ * Fix `rb_raise` from inside a C++ catch leaking the exception object (PR #229)
48
+ * Fix a failed Array store leaking its staging buffer, as much as the destination holds per failure (PR #228)
49
+ * Fix the GC free hook raising when a device free fails, which surfaced at whatever line triggered the collection (PR #227)
50
+ * Fix `from_binary` and `store_binary` writing managed memory while a kernel was still reading it (PR #226)
51
+ * Fix integer `sum` and `prod` truncating every partial the reduction merged (PR #224)
52
+ * Fix `minmax` ignoring compatible mode and returning zero-dimensional NArrays (PR #220)
53
+ * Fix `nan` poisoning `max` and an all-`nan` `min` answering `DBL_MAX` (PR #219)
54
+ * Fix `reshape!` and `marshal_load` mutating the array before they validate their arguments (PR #217)
55
+ * Fix `ptp` answering 1 whatever the input (PR #216)
56
+ * Fix `mulsum` ignoring the accumulator and the operand strides, which faulted the GPU on 8-bit dtypes (PR #215)
57
+ * Fix `dot` handing `gemm` operands of a different dtype, an out-of-bounds read (PR #214)
58
+ * Fix view offsets being added in the wrong unit (PR #213)
59
+ * Fix interpreter abort on a zero-dimensional view (PR #212)
60
+ * Fix `Cumo::RObject#logseq` returning `Infinity` (PR #210)
61
+ * Fix `to_i`, `to_f` and `to_c` recursing forever on a one-element array (PR #208)
62
+ * Fix complex `log2` and `log10` discarding the logarithm on the GPU (PR #207)
63
+ * Fix segfault in `max_index` and `min_index` with `nan: true` (PR #206)
64
+ * Fix `poly` failing on every input from an uninitialized `ndfunc` dimension (PR #205)
65
+ * Fix out-of-bounds writes in `bincount` from an overflowing length and a stale scan (PR #204)
66
+ * Fix `bincount` raising `TypeError` for every input (PR #203)
67
+ * 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)
68
+ * Fix out-of-bounds access from three unchecked `size_t` multiplications (PR #201)
69
+ * Fix segfaults from an unvalidated marshal payload (PR #200)
70
+ * Fix `store_array` reading a source Array that its own elements rewrite (PR #198)
71
+ * Fix CUDA initialisation statuses being ignored at `require` time (PR #196)
72
+ * Fix kernel launch errors being discarded, which let a rejected launch return an untouched buffer as success (PR #195)
73
+ * Fix wrong `Cumo::RObject` results from driving host memory with asynchronous CUDA work (PR #194)
74
+ * Fix device memory being invisible to the GC, which let a program churning temporaries run the GPU out of memory (PR #221)
75
+
76
+ Changes:
77
+
78
+ * Run `rand` and `rand_norm` on the GPU (PR #223)
79
+ * Run `clip` on the GPU (PR #222)
80
+ * Correct spelling in messages, docs and comments (PR #211)
81
+ * Add the math, extra and narray test suites `numo-narray-alt` has and cumo lacked (PR #207, PR #208, PR #209)
82
+ * Add a `store_array` regression test for a shrunk source Array (PR #199)
83
+ * Stop the ccache cache growing past the 10 GB limit in CI (PR #197)
84
+ * Add a benchmark script under `bench/` (commit 3127f84)
85
+
1
86
  # 0.5.7 (2026/08/13)
2
87
 
3
88
  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)
data/bench/cg_bench.rb ADDED
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # ---------------------------------------------------------------------------
5
+ # Conjugate gradient, to price what a host readback costs.
6
+ #
7
+ # ruby cg_bench.rb
8
+ # GPU=1 ruby cg_bench.rb # the plain way: readbacks every iteration
9
+ # GPU=1 SCALAR=device ruby cg_bench.rb # keep alpha and beta on the device
10
+ # GPU=1 SCALAR=device CHECK=20 ruby cg_bench.rb # and test convergence rarely
11
+ # GPU=1 OP=dense N=4096 ruby cg_bench.rb # heavier arithmetic per iteration
12
+ #
13
+ # CG makes two scalars an iteration, alpha and beta, and multiplies arrays by
14
+ # them. Written plainly that reads them back to the host every iteration, and
15
+ # the convergence test reads a third, each one waiting for the queue to drain.
16
+ # An iteration is not much arithmetic, so the waiting is what shows.
17
+ #
18
+ # SCALAR=host alpha and beta become Ruby Floats, as one would write it
19
+ # SCALAR=device they stay as the 0-dimensional NArray sum answers, and are
20
+ # never read (Cumo returns 0-dim; on Numo sum is a Float
21
+ # already, so the setting makes no difference there)
22
+ # CHECK=k test convergence every k iterations, 0 to never test
23
+ #
24
+ # A readback does not cost a fixed amount. What it costs is however much work
25
+ # is queued when it happens, so it is better read as a cap on how deep the
26
+ # pipeline is allowed to get than as a price per call. Several shallow drains
27
+ # an iteration cost little each; the last one costs a lot, because until it is
28
+ # gone no iteration can overlap the next.
29
+ #
30
+ # On the host path the floor is two readbacks an iteration whatever CHECK is
31
+ # set to, since alpha needs pap and beta needs rs_new as Floats. Keeping the
32
+ # scalars on the device is what makes fewer than that possible, and thinning
33
+ # the convergence test only pays once they are there.
34
+ #
35
+ # Correctness is checked against a known solution: x_true is chosen first and
36
+ # b = A x_true built from it, so the answer has something to be compared to.
37
+ #
38
+ # Timings vary by a factor of a few between runs. Take the best of several.
39
+ # ---------------------------------------------------------------------------
40
+
41
+ GPU = !%w[0 false].include?(ENV['GPU'].to_s.downcase) && !ENV['GPU'].to_s.empty?
42
+
43
+ if GPU
44
+ require 'cumo/narray'
45
+ XM = Cumo
46
+ else
47
+ require 'numo/narray'
48
+ XM = Numo
49
+ end
50
+
51
+ OP = (ENV['OP'] || 'stencil') # stencil | dense
52
+ GRID = (ENV['GRID'] || 512).to_i # side of the grid, for stencil
53
+ N = (ENV['N'] || 2048).to_i # number of unknowns, for dense
54
+ MAXITER = (ENV['MAXITER'] || 200).to_i
55
+ SCALAR = (ENV['SCALAR'] || 'host') # host | device
56
+ CHECK = (ENV['CHECK'] || 1).to_i # test convergence every CHECK, 0 to never
57
+ TOL = (ENV['TOL'] || 1.0e-6).to_f
58
+ DOT = (ENV['DOT'] || 'sum') # sum | blas
59
+ DETAIL = ENV['DETAIL'] != '0'
60
+ REPEAT = (ENV['REPEAT'] || 3).to_i # solve this many times, keep the fastest
61
+ SEED = (ENV['SEED'] || 42).to_i
62
+
63
+ TIMES = Hash.new(0.0)
64
+ READBACKS = [0]
65
+
66
+ def sync
67
+ XM::CUDA::Runtime.cudaDeviceSynchronize if GPU
68
+ end
69
+
70
+ # A readback, which is where the host waits. Counted, since that is the point.
71
+ def scalar(v)
72
+ READBACKS[0] += 1
73
+ return v if v.is_a?(Numeric)
74
+
75
+ v.respond_to?(:extract_cpu) ? v.extract_cpu : v.extract
76
+ end
77
+
78
+ def timed(key)
79
+ return yield unless DETAIL
80
+
81
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
82
+ r = yield
83
+ sync
84
+ TIMES[key] += Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
85
+ r
86
+ end
87
+
88
+ def dotp(a, b)
89
+ if DOT == 'blas'
90
+ a.reshape(a.size).dot(b.reshape(b.size))
91
+ else
92
+ (a * b).sum
93
+ end
94
+ end
95
+
96
+ # --- the operator ----------------------------------------------------------
97
+
98
+ # The five-point Laplacian, negated to be positive definite. The border stays
99
+ # zero and is never touched.
100
+ def apply_stencil(p)
101
+ out = XM::SFloat.zeros(GRID, GRID)
102
+ out[1..-2, 1..-2] =
103
+ 4.0 * p[1..-2, 1..-2] -
104
+ p[0..-3, 1..-2] - p[2..-1, 1..-2] -
105
+ p[1..-2, 0..-3] - p[1..-2, 2..-1]
106
+ out
107
+ end
108
+
109
+ # A diagonally dominant symmetric matrix. The identity is built with
110
+ # arithmetic rather than a fancy index.
111
+ def build_dense
112
+ r = XM::SFloat.new(N, N).rand * 2.0 - 1.0
113
+ sym = (r + r.transpose) * 0.5
114
+ i = XM::SFloat.new(N, 1).seq
115
+ j = XM::SFloat.new(1, N).seq
116
+ off = (XM::SFloat.zeros(N, N) + (i - j)).abs.clip(0.0, 1.0).ceil # 0 on the diagonal
117
+ sym + (1.0 - off) * (N * 1.0)
118
+ end
119
+
120
+ # --- setup -----------------------------------------------------------------
121
+
122
+ version = begin
123
+ XM::NArray::VERSION
124
+ rescue StandardError
125
+ 'unknown'
126
+ end
127
+
128
+ begin
129
+ XM::NArray.srand(SEED)
130
+ rescue StandardError
131
+ nil
132
+ end
133
+
134
+ if OP == 'dense'
135
+ amat = build_dense
136
+ x_true = XM::SFloat.new(N).rand * 2.0 - 1.0
137
+ apply = ->(p) { amat.dot(p) }
138
+ unknowns = N
139
+ else
140
+ x_true = XM::SFloat.zeros(GRID, GRID)
141
+ x_true[1..-2, 1..-2] = XM::SFloat.new(GRID - 2, GRID - 2).rand * 2.0 - 1.0
142
+ apply = ->(p) { apply_stencil(p) }
143
+ unknowns = (GRID - 2) * (GRID - 2)
144
+ end
145
+
146
+ b = apply.call(x_true)
147
+
148
+ puts "ruby : #{RUBY_VERSION} (#{RUBY_PLATFORM})"
149
+ puts "backend : #{XM} #{version}"
150
+ puts "problem : OP=#{OP} unknowns=#{unknowns} MAXITER=#{MAXITER}"
151
+ puts "scalar : SCALAR=#{SCALAR} CHECK=#{CHECK} DOT=#{DOT}"
152
+ puts
153
+
154
+ # Warm up
155
+ warm = XM::SFloat.new(64, 64).seq
156
+ warm_sum = (warm * warm).sum
157
+ warm_sum = warm_sum.extract if warm_sum.respond_to?(:extract)
158
+ sync
159
+ GC.start
160
+ READBACKS[0] = 0
161
+
162
+ # --- CG --------------------------------------------------------------------
163
+
164
+ # Each distinct scalar is read at most once an iteration. Reading rs_old in
165
+ # both alpha and beta, and rs_new in both the convergence test and beta, would
166
+ # wait twice for a value already in hand.
167
+ def solve(apply, b, rs0, shape)
168
+ x = XM::SFloat.zeros(*shape)
169
+ r = b + 0.0 # a copy
170
+ p_dir = r + 0.0
171
+ rs_old = timed(:dot) { dotp(r, r) }
172
+ rs_old_h = SCALAR == 'device' ? nil : rs0
173
+ iters = 0
174
+ converged = nil
175
+
176
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
177
+ MAXITER.times do |it|
178
+ iters = it + 1
179
+
180
+ ap = timed(:apply) { apply.call(p_dir) }
181
+ pap = timed(:dot) { dotp(p_dir, ap) }
182
+
183
+ alpha = timed(:alpha) do
184
+ SCALAR == 'device' ? rs_old / pap : rs_old_h / scalar(pap).to_f
185
+ end
186
+
187
+ timed(:axpy) do
188
+ x += p_dir * alpha
189
+ r -= ap * alpha
190
+ end
191
+
192
+ rs_new = timed(:dot) { dotp(r, r) }
193
+ rs_new_h = SCALAR == 'device' ? nil : scalar(rs_new).to_f
194
+
195
+ if CHECK.positive? && (iters % CHECK).zero?
196
+ rel = Math.sqrt((rs_new_h || scalar(rs_new).to_f) / rs0)
197
+ if rel < TOL
198
+ converged = rel
199
+ break
200
+ end
201
+ end
202
+
203
+ beta = timed(:beta) do
204
+ SCALAR == 'device' ? rs_new / rs_old : rs_new_h / rs_old_h
205
+ end
206
+
207
+ timed(:update_p) { p_dir = r + p_dir * beta }
208
+ rs_old = rs_new
209
+ rs_old_h = rs_new_h
210
+ end
211
+ sync
212
+ { x: x, iters: iters, converged: converged,
213
+ elapsed: Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0 }
214
+ end
215
+
216
+ shape = OP == 'dense' ? [N] : [GRID, GRID]
217
+ rs0 = scalar(dotp(b, b)).to_f # the reference for the relative residual, read once
218
+
219
+ # Solve it a few times and keep the fastest. A single timing here varies by a
220
+ # factor of a few, which is enough to invent a trend that is not there.
221
+ best = nil
222
+ REPEAT.times do
223
+ TIMES.clear
224
+ READBACKS[0] = 0
225
+ run = solve(apply, b, rs0, shape)
226
+ run[:times] = TIMES.dup
227
+ run[:readbacks] = READBACKS[0]
228
+ best = run if best.nil? || run[:elapsed] < best[:elapsed]
229
+ end
230
+ x = best[:x]
231
+ iters = best[:iters]
232
+ elapsed = best[:elapsed]
233
+ TIMES.replace(best[:times])
234
+ READBACKS[0] = best[:readbacks]
235
+ puts format(' converged at iter %d, relative residual %.3e', iters, best[:converged]) if best[:converged]
236
+
237
+ # --- results ---------------------------------------------------------------
238
+
239
+ res = apply.call(x) - b
240
+ rel_res = Math.sqrt(scalar((res * res).sum).to_f / rs0)
241
+ err = x - x_true
242
+ rel_err = Math.sqrt(scalar((err * err).sum).to_f) /
243
+ Math.sqrt(scalar((x_true * x_true).sum).to_f)
244
+
245
+ puts format(' iterations : %d', iters)
246
+ puts format(' rel. residual : %.3e', rel_res)
247
+ puts format(' rel. error : %.3e (against the known x_true)', rel_err)
248
+ puts format(' time : %.4f s (%.1f us/iter, best of %d)',
249
+ elapsed, elapsed / iters * 1e6, REPEAT)
250
+ puts format(' readbacks : %d (%.2f per iteration)',
251
+ READBACKS[0], READBACKS[0].to_f / iters)
252
+
253
+ if DETAIL
254
+ puts
255
+ measured = TIMES.values.sum
256
+ puts 'breakdown (per iteration):'
257
+ TIMES.sort_by { |_, v| -v }.each do |key, v|
258
+ puts format(' %-10s %9.1f us %5.1f%%', key, v / iters * 1e6, 100.0 * v / measured)
259
+ end
260
+ puts ' (these sections synchronize, so they add up to more than us/iter above,'
261
+ puts ' and DETAIL=1 is much noisier than DETAIL=0 because of it)'
262
+ end
263
+
264
+ puts
265
+ puts ' What to compare:'
266
+ puts ' SCALAR=host CHECK=1 the plain way, several readbacks an iteration'
267
+ puts ' SCALAR=device CHECK=1 alpha and beta stay on the device, one readback'
268
+ puts ' SCALAR=device CHECK=20 the convergence test thinned out too'
269
+ puts ' SCALAR=device CHECK=0 no readback at all inside the loop'
270
+ puts ' Thinning the test on its own changes nothing: the host path still reads'
271
+ puts ' pap and rs_new every iteration. The two settings only pay together.'