cumo 0.5.8 → 0.5.10
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.
- checksums.yaml +4 -4
- data/3rd_party/mkmf-cu/lib/mkmf-cu/cli.rb +3 -1
- data/CHANGELOG.md +76 -0
- data/Gemfile +1 -0
- data/README.md +66 -7
- data/bench/cg_bench.rb +271 -0
- data/bench/crossover_bench.rb +374 -0
- data/bench/cumo_probe.rb +348 -0
- data/bench/cumo_sync_probe.rb +442 -0
- data/bench/ising_bench.rb +242 -0
- data/bench/kmeans_bench.rb +254 -0
- data/bench/particle_bench.rb +261 -0
- data/bench/transformer_bench.rb +264 -0
- data/ext/cumo/cuda/cudnn_impl.cpp +1 -1
- data/ext/cumo/cuda/memory_pool_impl.cpp +6 -1
- data/ext/cumo/cuda/memory_pool_impl_test.cpp +47 -0
- data/ext/cumo/cuda/runtime.c +30 -1
- data/ext/cumo/extconf.rb +34 -1
- data/ext/cumo/include/cumo/cuda/cudnn.h +53 -0
- data/ext/cumo/include/cumo/cuda/cumo_thrust.hpp +16 -119
- data/ext/cumo/include/cumo/cuda/runtime.h +5 -0
- data/ext/cumo/include/cumo/indexer.h +173 -3
- data/ext/cumo/include/cumo/narray.h +4 -1
- data/ext/cumo/include/cumo/reduce_kernel.h +522 -108
- data/ext/cumo/include/cumo/template.h +26 -4
- data/ext/cumo/include/cumo/template_kernel.h +10 -1
- data/ext/cumo/include/cumo/types/bit_kernel.h +122 -0
- data/ext/cumo/include/cumo/types/complex_kernel.h +5 -3
- data/ext/cumo/include/cumo/types/complex_macro_kernel.h +1 -38
- data/ext/cumo/include/cumo/types/float_macro_kernel.h +6 -4
- data/ext/cumo/include/cumo/types/int_macro_kernel.h +0 -4
- data/ext/cumo/include/cumo/types/real_accum_kernel.h +0 -37
- data/ext/cumo/include/cumo/types/robject_kernel.h +0 -3
- data/ext/cumo/include/cumo/types/uint_macro_kernel.h +1 -0
- data/ext/cumo/include/cumo/types/xint_macro_kernel.h +2 -2
- data/ext/cumo/include/cumo.h +2 -2
- data/ext/cumo/narray/data.c +21 -0
- data/ext/cumo/narray/data_kernel.cu +92 -0
- data/ext/cumo/narray/gen/spec.rb +1 -0
- data/ext/cumo/narray/gen/tmpl/accum.c +3 -5
- data/ext/cumo/narray/gen/tmpl/accum_arg.c +16 -9
- data/ext/cumo/narray/gen/tmpl/accum_arg_kernel.cu +104 -20
- data/ext/cumo/narray/gen/tmpl/accum_binary_kernel.cu +67 -37
- data/ext/cumo/narray/gen/tmpl/accum_index.c +8 -27
- data/ext/cumo/narray/gen/tmpl/accum_index_kernel.cu +104 -20
- data/ext/cumo/narray/gen/tmpl/allocate.c +19 -0
- data/ext/cumo/narray/gen/tmpl/aset.c +8 -0
- data/ext/cumo/narray/gen/tmpl/batch_norm.c +8 -6
- data/ext/cumo/narray/gen/tmpl/batch_norm_backward.c +18 -8
- data/ext/cumo/narray/gen/tmpl/binary.c +76 -0
- data/ext/cumo/narray/gen/tmpl/binary2.c +22 -12
- data/ext/cumo/narray/gen/tmpl/binary2_kernel.cu +24 -8
- data/ext/cumo/narray/gen/tmpl/binary_kernel.cu +57 -0
- data/ext/cumo/narray/gen/tmpl/binary_s.c +68 -9
- data/ext/cumo/narray/gen/tmpl/binary_s_kernel.cu +44 -7
- data/ext/cumo/narray/gen/tmpl/bincount.c +34 -38
- data/ext/cumo/narray/gen/tmpl/bincount_kernel.cu +86 -0
- data/ext/cumo/narray/gen/tmpl/cast.c +6 -0
- data/ext/cumo/narray/gen/tmpl/clip.c +124 -31
- data/ext/cumo/narray/gen/tmpl/clip_kernel.cu +108 -30
- data/ext/cumo/narray/gen/tmpl/complex_accum_kernel.cu +202 -85
- data/ext/cumo/narray/gen/tmpl/cond_binary.c +74 -11
- data/ext/cumo/narray/gen/tmpl/cond_binary_kernel.cu +44 -9
- data/ext/cumo/narray/gen/tmpl/cond_unary.c +47 -26
- data/ext/cumo/narray/gen/tmpl/cond_unary_kernel.cu +30 -0
- data/ext/cumo/narray/gen/tmpl/conv.c +7 -5
- data/ext/cumo/narray/gen/tmpl/conv_grad_w.c +13 -10
- data/ext/cumo/narray/gen/tmpl/conv_transpose.c +8 -7
- data/ext/cumo/narray/gen/tmpl/cum.c +13 -0
- data/ext/cumo/narray/gen/tmpl/cum_kernel.cu +72 -0
- data/ext/cumo/narray/gen/tmpl/ewcomp.c +20 -12
- data/ext/cumo/narray/gen/tmpl/ewcomp_kernel.cu +22 -7
- data/ext/cumo/narray/gen/tmpl/fill.c +32 -23
- data/ext/cumo/narray/gen/tmpl/fill_kernel.cu +18 -21
- data/ext/cumo/narray/gen/tmpl/fixed_batch_norm.c +8 -6
- data/ext/cumo/narray/gen/tmpl/float_accum_kernel.cu +174 -81
- data/ext/cumo/narray/gen/tmpl/frexp.c +9 -18
- data/ext/cumo/narray/gen/tmpl/frexp_kernel.cu +31 -0
- data/ext/cumo/narray/gen/tmpl/gemm.c +82 -8
- data/ext/cumo/narray/gen/tmpl/inspect.c +1 -0
- data/ext/cumo/narray/gen/tmpl/median.c +32 -0
- data/ext/cumo/narray/gen/tmpl/minmax.c +37 -12
- data/ext/cumo/narray/gen/tmpl/new_dim0.c +17 -0
- data/ext/cumo/narray/gen/tmpl/poly.c +27 -0
- data/ext/cumo/narray/gen/tmpl/poly_kernel.cu +46 -0
- data/ext/cumo/narray/gen/tmpl/pooling_backward.c +18 -3
- data/ext/cumo/narray/gen/tmpl/pooling_forward.c +7 -5
- data/ext/cumo/narray/gen/tmpl/pow.c +130 -18
- data/ext/cumo/narray/gen/tmpl/pow_kernel.cu +90 -14
- data/ext/cumo/narray/gen/tmpl/real_accum_kernel.cu +126 -5
- data/ext/cumo/narray/gen/tmpl/seq.c +17 -15
- data/ext/cumo/narray/gen/tmpl/seq_kernel.cu +30 -23
- data/ext/cumo/narray/gen/tmpl/set2.c +8 -45
- data/ext/cumo/narray/gen/tmpl/set2_kernel.cu +28 -0
- data/ext/cumo/narray/gen/tmpl/sort.c +41 -0
- data/ext/cumo/narray/gen/tmpl/sort_index.c +51 -0
- data/ext/cumo/narray/gen/tmpl/store_array.c +5 -0
- data/ext/cumo/narray/gen/tmpl/store_bit.c +17 -19
- data/ext/cumo/narray/gen/tmpl/store_bit_kernel.cu +21 -60
- data/ext/cumo/narray/gen/tmpl/store_from.c +48 -17
- data/ext/cumo/narray/gen/tmpl/store_from_kernel.cu +40 -48
- data/ext/cumo/narray/gen/tmpl/unary.c +12 -44
- data/ext/cumo/narray/gen/tmpl/unary2.c +61 -29
- data/ext/cumo/narray/gen/tmpl/unary2_kernel.cu +44 -0
- data/ext/cumo/narray/gen/tmpl/unary_kernel.cu +23 -73
- data/ext/cumo/narray/gen/tmpl/unary_ret2.c +9 -17
- data/ext/cumo/narray/gen/tmpl/unary_ret2_kernel.cu +32 -0
- data/ext/cumo/narray/gen/tmpl/unary_s.c +14 -19
- data/ext/cumo/narray/gen/tmpl/unary_s_kernel.cu +22 -54
- data/ext/cumo/narray/gen/tmpl_bit/aset.c +8 -0
- data/ext/cumo/narray/gen/tmpl_bit/binary.c +25 -110
- data/ext/cumo/narray/gen/tmpl_bit/binary_kernel.cu +52 -0
- data/ext/cumo/narray/gen/tmpl_bit/bit_reduce.c +18 -4
- data/ext/cumo/narray/gen/tmpl_bit/bit_reduce_kernel.cu +77 -0
- data/ext/cumo/narray/gen/tmpl_bit/extract.c +7 -1
- data/ext/cumo/narray/gen/tmpl_bit/extract_cpu.c +7 -1
- data/ext/cumo/narray/gen/tmpl_bit/fill.c +21 -37
- data/ext/cumo/narray/gen/tmpl_bit/fill_kernel.cu +42 -0
- data/ext/cumo/narray/gen/tmpl_bit/mask.c +20 -5
- data/ext/cumo/narray/gen/tmpl_bit/mask_kernel.cu +54 -0
- data/ext/cumo/narray/gen/tmpl_bit/store_array.c +71 -38
- data/ext/cumo/narray/gen/tmpl_bit/store_array_kernel.cu +89 -0
- data/ext/cumo/narray/gen/tmpl_bit/store_bit.c +26 -73
- data/ext/cumo/narray/gen/tmpl_bit/store_bit_kernel.cu +47 -0
- data/ext/cumo/narray/gen/tmpl_bit/store_from.c +21 -1
- data/ext/cumo/narray/gen/tmpl_bit/store_from_kernel.cu +30 -0
- data/ext/cumo/narray/gen/tmpl_bit/unary.c +23 -78
- data/ext/cumo/narray/gen/tmpl_bit/unary_kernel.cu +48 -0
- data/ext/cumo/narray/gen/tmpl_bit/where.c +42 -5
- data/ext/cumo/narray/gen/tmpl_bit/where2.c +18 -5
- data/ext/cumo/narray/gen/tmpl_bit/where_kernel.cu +107 -0
- data/ext/cumo/narray/index.c +20 -3
- data/ext/cumo/narray/index_kernel.cu +4 -4
- data/ext/cumo/narray/narray.c +22 -7
- data/ext/cumo/narray/ndloop.c +27 -1
- data/ext/cumo/narray/sort_kernel.cu +345 -0
- data/test/bit_test.rb +20 -4
- data/test/cudnn_test.rb +134 -0
- data/test/cumo_test.rb +59 -0
- data/test/narray_test.rb +3136 -4
- metadata +27 -2
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bcb51638cb488e5abcb0bf885744ea1478f4daefd4945f6af8becf252bc85f29
|
|
4
|
+
data.tar.gz: 75aed5ea382f37a823992e29ef2f3fe9ee31a7b7c11350d2541e5f4fc0c0fb59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee03167a0966008c99598d90623757772060c09bad859af35d313dda831975461cba49c8da251a4793e6a9b933cd5ec75588038381d55e492398d0b4e2686559
|
|
7
|
+
data.tar.gz: 2e2b3e1a6f0f304be8b2375be48061ba73926973d4ec966e49d0b50f666d88d7078cec3c59ea797401bd72b91ec96a4f115bf2ed49485dc1d7ca3b6a29fb1c48
|
|
@@ -40,7 +40,9 @@ module MakeMakefileCuda
|
|
|
40
40
|
s = MakeMakefileCuda::Nvcc.generate(argv)
|
|
41
41
|
cmd = "nvcc #{s}"
|
|
42
42
|
if ENV['CUMO_NVCC_GENERATE_CODE']
|
|
43
|
-
|
|
43
|
+
ENV['CUMO_NVCC_GENERATE_CODE'].split.each do |code|
|
|
44
|
+
cmd << " --generate-code=#{code}"
|
|
45
|
+
end
|
|
44
46
|
else
|
|
45
47
|
capability = nil
|
|
46
48
|
if find_executable('nvidia-smi')
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,79 @@
|
|
|
1
|
+
# 0.5.10 (2026/08/23)
|
|
2
|
+
|
|
3
|
+
Breaking changes:
|
|
4
|
+
|
|
5
|
+
* An indexed assignment to a frozen view raises instead of writing through it; `store` and `fill` already raised (PR #306)
|
|
6
|
+
* The cuDNN and cuBLAS entry points turn away arguments they used to read or write out of bounds: `pooling_backward` requires `y` and `gy` to carry its own dtype and the pooling output shape of `x`, `gemm` requires a contiguous `c`, `batch_norm` requires the reduced size to cover x's channels, and `conv_grad_w` raises on a mismatched `gy` rather than asserting (PR #303, PR #302, PR #300, PR #278)
|
|
7
|
+
* `seq` on the unsigned types wraps a negative start and a start past 2**63 the way `fill` and `cast` do, where they collapsed to zero and clamped to INT64_MAX (PR #295, PR #294)
|
|
8
|
+
* A negative power of an unsigned array answers zero instead of spinning the device until SIGKILL (PR #293)
|
|
9
|
+
* A subscript that is a Cumo::RObject subclass, and an `expand_dims` past `CUMO_NA_MAX_DIMENSION`, raise instead of being taken (PR #272, PR #270)
|
|
10
|
+
|
|
11
|
+
Fixes:
|
|
12
|
+
|
|
13
|
+
* Fix `[]=` writing through a frozen view, the store going to a derived view whose data object is the root rather than the receiver (PR #306)
|
|
14
|
+
* Fix `at()` reading the accumulator in place of the subscript on a reversed view, which addressed far in front of the buffer (PR #305)
|
|
15
|
+
* Fix `cumo_cuda_cudnn_CreateBNTensorDescriptor` returning before it derived the descriptor, an inverted status check that no caller reached (PR #304)
|
|
16
|
+
* Fix `batch_norm`, `fixed_batch_norm` and `batch_norm_backward` reading and writing past their parameters, the sizes being checked against the reduced shape while cuDNN reaches x's channel count (PR #303)
|
|
17
|
+
* Fix `gemm` writing past its allocation when `c` is a non-contiguous inplace view, and over whatever the pool put next to it (PR #302)
|
|
18
|
+
* Fix `pooling_backward` reading past `y` and `gy`, and returning what it read in `gx` (PR #300)
|
|
19
|
+
* Fix `pow_int` negating INT32_MIN, which overflows and leaves the loop unbounded (PR #299)
|
|
20
|
+
* Fix the device free running for a subscript that allocated no index array (PR #297)
|
|
21
|
+
* Fix a NULL dereference from `at()` given a scalar subscript (PR #271)
|
|
22
|
+
* Fix an out-of-bounds write through an unchecked cuDNN output array (PR #269)
|
|
23
|
+
* Fix `gemm` reading past an operand that carries fewer batch dimensions (PR #268)
|
|
24
|
+
|
|
25
|
+
Changes:
|
|
26
|
+
|
|
27
|
+
* Pass a numeric operand to the kernel instead of casting it to a 0-dimensional array first, in the operators, `clip`, `pow`, the comparisons, the NMath functions and the coerced left-hand side; GPT-2 124M decode drops from 787 to 677 kernel launches a token (PR #296, PR #291, PR #290, PR #289, PR #288, PR #287)
|
|
28
|
+
* Run the NMath binary functions through the indexer loop instead of one kernel launch per row; 11x on a transposed operand and up to 60x on the cheap kernels (PR #292)
|
|
29
|
+
* Copy a reduction operand only when it carries an index array, where any non-contiguous view was copied whole (PR #286)
|
|
30
|
+
* Run the nan-aware reductions, the nan-aware index reductions, `kahan_sum` and `Bit#mask` on the GPU (PR #277, PR #276, PR #275, PR #273)
|
|
31
|
+
* Take a transposed batch of matrices with the cuBLAS transpose flag rather than duplicating it, which saves a temporary the size of the operand (PR #283)
|
|
32
|
+
* Emit indexer accessors up to eight dimensions (PR #284)
|
|
33
|
+
* Drop a free-list bin from the pool arena when `Malloc` empties it (PR #279)
|
|
34
|
+
* Check the cuDNN header and version, not just the library (PR #280)
|
|
35
|
+
* Document every method that returns a 0-dimensional NArray, and what keeping scalars on the device buys (PR #298, PR #282)
|
|
36
|
+
* Build two GPU architectures in CI instead of every one (PR #285)
|
|
37
|
+
* Add `fiddle` to the Gemfile (PR #281)
|
|
38
|
+
* Remove TODO comments whose questions have been answered (PR #274)
|
|
39
|
+
* Cover the Int32 array exponent in the INT_MIN power test (PR #301)
|
|
40
|
+
|
|
41
|
+
# 0.5.9 (2026/08/20)
|
|
42
|
+
|
|
43
|
+
Breaking changes:
|
|
44
|
+
|
|
45
|
+
* `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)
|
|
46
|
+
|
|
47
|
+
Fixes:
|
|
48
|
+
|
|
49
|
+
* 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)
|
|
50
|
+
* Fix a segmentation fault storing a Ruby Array into a reversed `Cumo::Bit` view that starts past the first word (PR #266)
|
|
51
|
+
* Fix `sort` in place of a view backed by an index array reading row 0 for every row (PR #262)
|
|
52
|
+
* Fix a `Cumo::RObject` store reading an index array before the kernel writes it, which answered zeroes from the second store onwards (PR #258)
|
|
53
|
+
* Fix a store of an Array of narrays into an indexed destination writing one element per row (PR #257)
|
|
54
|
+
* Fix the zero fill of a row stored from a shorter narray starting one element too far (PR #256)
|
|
55
|
+
* Fix grid-stride kernels hanging from about 2**32 elements, the step wrapping to zero (PR #240)
|
|
56
|
+
* Fix `Cumo::Bit` views with a negative step reading out of bounds (PR #238)
|
|
57
|
+
* Fix index reductions answering a later index on ties, where `Numo::NArray` answers the first (PR #236)
|
|
58
|
+
* Fix wrong values from reductions with `nan: true` (PR #234)
|
|
59
|
+
|
|
60
|
+
Changes:
|
|
61
|
+
|
|
62
|
+
* 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)
|
|
63
|
+
* 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)
|
|
64
|
+
* Run `bincount` on the GPU; 1M `Int32` 1.62 -> 0.05 ms (PR #255)
|
|
65
|
+
* Run `cumsum` and `cumprod` on the GPU (PR #242)
|
|
66
|
+
* Run `minmax`, `abs`, `isnan` and the rest of `cond_unary`, `modf` and `frexp` on the GPU (PR #230, PR #231, PR #232, PR #233)
|
|
67
|
+
* 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)
|
|
68
|
+
* Run complex `real=` and `imag=` on the GPU; 2^22 `DComplex` 4.63 -> 0.45 ms (PR #267)
|
|
69
|
+
* 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)
|
|
70
|
+
* 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)
|
|
71
|
+
* 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)
|
|
72
|
+
* Split a reduction with too few outputs across more blocks (PR #235)
|
|
73
|
+
* 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)
|
|
74
|
+
* Warn about the synchronization `a[idx]` and `inspect` perform (PR #247)
|
|
75
|
+
* 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
|
|
76
|
+
|
|
1
77
|
# 0.5.8 (2026/08/17)
|
|
2
78
|
|
|
3
79
|
Breaking changes:
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Cumo (pronounced "koomo") is a CUDA-aware, GPU-optimized numerical library that
|
|
|
9
9
|
* Ruby 3.0 or later
|
|
10
10
|
* NVIDIA GPU Compute Capability 3.5 (Kepler) or later
|
|
11
11
|
* CUDA 11.0 or later
|
|
12
|
+
* cuDNN 8.0 or later (optional, for the cuDNN features)
|
|
12
13
|
|
|
13
14
|
## Preparation
|
|
14
15
|
|
|
@@ -97,17 +98,31 @@ a = xm::DFloat.new(3,5).seq
|
|
|
97
98
|
|
|
98
99
|
### Incompatibility With Numo
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
Numo returns a Ruby numeric object wherever a result is 0-dimensional, while Cumo returns the 0-dimensional NArray itself.
|
|
102
|
+
Cumo differs in this way to avoid synchronization and minimize CPU ⇄ GPU data transfer.
|
|
103
|
+
That is not only a cost of the port; see [Keeping Scalars On The Device](#keeping-scalars-on-the-device) for what it buys.
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
* `[]`
|
|
104
|
-
* `count_true`
|
|
105
|
-
* `count_false`
|
|
105
|
+
The methods affected are:
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
* `[]` and `extract`
|
|
108
|
+
* `count_true` and `count_false`
|
|
109
|
+
* reductions down to a single value: `sum`, `prod`, `mean`, `stddev`, `var`, `rms`, `min`, `max`, `ptp`, `minmax`, `median`, `mulsum`, `dot`, `inner`
|
|
110
|
+
* index reductions: `max_index`, `min_index`, `argmax`, `argmin`
|
|
111
|
+
|
|
112
|
+
A 0-dimensional `Cumo::Bit` is truthy even when it holds 0, because Ruby treats every object but `nil` and `false` as true.
|
|
113
|
+
Comparing two scalars therefore takes the wrong branch without raising anything:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
a = Cumo::SFloat[5.0]
|
|
117
|
+
a[0] < 1.0 #=> Cumo::Bit#shape=[] holding 0
|
|
118
|
+
(a[0] < 1.0) ? :yes : :no #=> :yes, where Numo gives :no
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`assert_operator(a[0], :<, 1.0)` passes for the same reason, so a test suite written for Numo can stay green against Cumo while asserting nothing.
|
|
122
|
+
Read the value back to the host before branching on it, or run under `compatible_mode`.
|
|
109
123
|
|
|
110
124
|
Set the `CUMO_COMPATIBLE_MODE` environment variable to `ON` to force Numo NArray compatibility (for worse performance).
|
|
125
|
+
Running a Numo test suite that way keeps its assertions meaningful.
|
|
111
126
|
|
|
112
127
|
You may enable or disable `compatible_mode` as:
|
|
113
128
|
|
|
@@ -126,6 +141,43 @@ You can also use the following methods which behave like Numo's NArray methods.
|
|
|
126
141
|
* `count_true_cpu`
|
|
127
142
|
* `count_false_cpu`
|
|
128
143
|
|
|
144
|
+
```ruby
|
|
145
|
+
a.aref_cpu(0) < 1.0 #=> false in either mode
|
|
146
|
+
Float(a.sum) #=> 7.0 in either mode
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
They are methods on an NArray, so chaining one onto a result that `compatible_mode` has already turned into a Ruby object, as in `a.sum.extract_cpu`, raises `NoMethodError` while the mode is on.
|
|
150
|
+
`Kernel#Float` and `Kernel#Integer` read either representation, and read Numo's too, so they are what code that runs against both libraries wants.
|
|
151
|
+
|
|
152
|
+
### Keeping Scalars On The Device
|
|
153
|
+
|
|
154
|
+
The 0-dimensional return is what lets an iterative loop stay on the GPU.
|
|
155
|
+
Reading a scalar back to the host waits for everything queued behind it, so every read caps how far ahead the GPU is allowed to run.
|
|
156
|
+
What a read costs is not a fixed price either: it is however much work happens to be queued when it is taken.
|
|
157
|
+
|
|
158
|
+
`bench/cg_bench.rb` prices this with a conjugate gradient solve, 200 iterations over a 512x512 grid on an RTX 5070 Ti Laptop:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
scalars convergence test us/iter readbacks/iter
|
|
162
|
+
Ruby Floats every iteration 136.7 2.02
|
|
163
|
+
Ruby Floats never 134.4 2.02
|
|
164
|
+
0-dim NArray every iteration 142.6 1.02
|
|
165
|
+
0-dim NArray every 20th 65.6 0.06
|
|
166
|
+
0-dim NArray never 60.3 0.02
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Written with Ruby Floats the loop reads back twice an iteration whatever the convergence test does, since `alpha` needs `pap` and `beta` needs `rs_new` as Floats.
|
|
170
|
+
Thinning the test cannot get under that floor, and keeping the scalars as 0-dimensional NArrays buys nothing on its own.
|
|
171
|
+
The two only pay together, and together they are worth 2.1x.
|
|
172
|
+
The relative residual is identical in every row.
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
alpha = rs_old / pap # a 0-dimensional NArray, divided on the device
|
|
176
|
+
x += p_dir * alpha # and consumed there, without crossing the bus
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Read the value back once the loop is done, or every k iterations if it has to test something.
|
|
180
|
+
|
|
129
181
|
### Select a GPU device ID
|
|
130
182
|
|
|
131
183
|
Set the `CUDA_VISIBLE_DEVICES=id` environment variable, or
|
|
@@ -201,7 +253,14 @@ ln -sf "$HOME/opt/ccache/bin/ccache" "$HOME/opt/ccache/bin/nvcc"
|
|
|
201
253
|
bundle exec env CUMO_NVCC_GENERATE_CODE=arch=compute_60,code=sm_60 rake compile
|
|
202
254
|
```
|
|
203
255
|
|
|
256
|
+
Separate the entries with a space to build for more than one architecture:
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
bundle exec env CUMO_NVCC_GENERATE_CODE="arch=compute_75,code=sm_75 arch=compute_121,code=sm_121" rake compile
|
|
260
|
+
```
|
|
261
|
+
|
|
204
262
|
This is useful even on development because it makes it possible to skip JIT compilation of PTX to cubin during runtime.
|
|
263
|
+
Without it, and without an `nvidia-smi` to read the local compute capability from, the build covers every architecture the CUDA version supports.
|
|
205
264
|
|
|
206
265
|
### Run tests with gdb
|
|
207
266
|
|
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.'
|