cumo 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +10 -0
- data/3rd_party/mkmf-cu/lib/mkmf-cu/cli.rb +1 -1
- data/CHANGELOG.md +59 -0
- data/README.md +85 -208
- data/cumo.gemspec +2 -2
- data/ext/cumo/cuda/cublas.c +12 -19
- data/ext/cumo/cuda/cudnn.c +10 -18
- data/ext/cumo/cuda/cudnn_impl.cpp +2 -3
- data/ext/cumo/cuda/cusolver.c +1238 -0
- data/ext/cumo/cuda/driver.c +57 -1
- data/ext/cumo/cuda/memory_pool.cpp +12 -2
- data/ext/cumo/cuda/runtime.c +122 -31
- data/ext/cumo/cumo.c +2 -0
- data/ext/cumo/depend.erb +9 -5
- data/ext/cumo/extconf.rb +36 -33
- data/ext/cumo/include/cumo/bit_reduce_kernel.h +2 -2
- data/ext/cumo/include/cumo/cuda/cumo_thrust.hpp +7 -0
- data/ext/cumo/include/cumo/cuda/cusolver.h +35 -0
- data/ext/cumo/include/cumo/cuda/runtime.h +11 -0
- data/ext/cumo/include/cumo/indexer.h +126 -9
- data/ext/cumo/include/cumo/intern.h +5 -1
- data/ext/cumo/include/cumo/reduce_kernel.h +29 -27
- data/ext/cumo/include/cumo/row_kernel.h +77 -15
- data/ext/cumo/include/cumo/template.h +1 -5
- data/ext/cumo/include/cumo/template_kernel.h +14 -0
- data/ext/cumo/include/cumo.h +2 -2
- data/ext/cumo/narray/array.c +3 -2
- data/ext/cumo/narray/data.c +121 -0
- data/ext/cumo/narray/data_kernel.cu +259 -76
- data/ext/cumo/narray/gen/cogen_kernel.rb +60 -2
- data/ext/cumo/narray/gen/cogen_runner.rb +16 -0
- data/ext/cumo/narray/gen/kernel_parts.rb +7 -0
- data/ext/cumo/narray/gen/tmpl/accum_arg_kernel.cu +6 -2
- data/ext/cumo/narray/gen/tmpl/accum_binary_kernel.cu +2 -0
- data/ext/cumo/narray/gen/tmpl/accum_index_kernel.cu +6 -2
- data/ext/cumo/narray/gen/tmpl/binary2_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/binary_kernel.cu +72 -22
- data/ext/cumo/narray/gen/tmpl/binary_s_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/clip_kernel.cu +4 -31
- data/ext/cumo/narray/gen/tmpl/complex_accum_kernel.cu +7 -1
- data/ext/cumo/narray/gen/tmpl/cond_binary_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/cond_unary_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/cum.c +12 -64
- data/ext/cumo/narray/gen/tmpl/cum_kernel.cu +53 -24
- data/ext/cumo/narray/gen/tmpl/ewcomp_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/fill.c +11 -3
- data/ext/cumo/narray/gen/tmpl/fill_kernel.cu +20 -11
- data/ext/cumo/narray/gen/tmpl/float_accum_kernel.cu +7 -1
- data/ext/cumo/narray/gen/tmpl/frexp_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/layer_norm_kernel.cu +98 -0
- data/ext/cumo/narray/gen/tmpl/logseq.c +28 -14
- data/ext/cumo/narray/gen/tmpl/logseq_kernel.cu +19 -23
- data/ext/cumo/narray/gen/tmpl/poly_kernel.cu +1 -10
- data/ext/cumo/narray/gen/tmpl/pow_kernel.cu +3 -21
- data/ext/cumo/narray/gen/tmpl/rand.c +9 -3
- data/ext/cumo/narray/gen/tmpl/rand_kernel.cu +10 -14
- data/ext/cumo/narray/gen/tmpl/rand_norm.c +9 -3
- data/ext/cumo/narray/gen/tmpl/rand_norm_kernel.cu +10 -14
- data/ext/cumo/narray/gen/tmpl/real_accum_kernel.cu +3 -1
- data/ext/cumo/narray/gen/tmpl/rms_norm_kernel.cu +67 -0
- data/ext/cumo/narray/gen/tmpl/seq.c +9 -3
- data/ext/cumo/narray/gen/tmpl/seq_kernel.cu +11 -15
- data/ext/cumo/narray/gen/tmpl/set2.c +13 -4
- data/ext/cumo/narray/gen/tmpl/set2_kernel.cu +9 -15
- data/ext/cumo/narray/gen/tmpl/softmax_kernel.cu +70 -4
- data/ext/cumo/narray/gen/tmpl/sort.c +1 -3
- data/ext/cumo/narray/gen/tmpl/sort_index.c +1 -1
- data/ext/cumo/narray/gen/tmpl/store.c +6 -0
- data/ext/cumo/narray/gen/tmpl/store_array.c +9 -6
- data/ext/cumo/narray/gen/tmpl/store_bit_kernel.cu +1 -10
- data/ext/cumo/narray/gen/tmpl/store_from_kernel.cu +6 -21
- data/ext/cumo/narray/gen/tmpl/store_numeric.c +9 -5
- data/ext/cumo/narray/gen/tmpl/unary2_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/unary_kernel.cu +40 -12
- data/ext/cumo/narray/gen/tmpl/unary_ret2_kernel.cu +2 -11
- data/ext/cumo/narray/gen/tmpl/unary_s_kernel.cu +40 -12
- data/ext/cumo/narray/gen/tmpl_bit/binary_kernel.cu +1 -10
- data/ext/cumo/narray/gen/tmpl_bit/fill_kernel.cu +1 -10
- data/ext/cumo/narray/gen/tmpl_bit/store_array.c +3 -2
- data/ext/cumo/narray/gen/tmpl_bit/store_bit_kernel.cu +1 -10
- data/ext/cumo/narray/gen/tmpl_bit/store_from_kernel.cu +1 -10
- data/ext/cumo/narray/gen/tmpl_bit/unary_kernel.cu +1 -10
- data/ext/cumo/narray/index.c +2 -2
- data/ext/cumo/narray/narray.c +236 -23
- data/ext/cumo/narray/ndloop.c +9 -3
- data/ext/cumo/narray/ndloop_kernel.cu +87 -89
- data/lib/cumo/cuda/compiler.rb +12 -0
- data/lib/cumo/cuda/elementwise_kernel.rb +23 -10
- data/lib/cumo/cuda/reduction_kernel.rb +47 -16
- data/lib/cumo/cuda/user_kernel.rb +90 -6
- data/lib/cumo/linalg.rb +1403 -0
- data/lib/cumo/narray/extra.rb +13 -8
- data/lib/cumo.rb +2 -0
- metadata +8 -75
- data/.gitignore +0 -32
- data/.rubocop.yml +0 -15
- data/.rubocop_todo.yml +0 -1691
- data/Dockerfile +0 -34
- data/Gemfile +0 -14
- data/bench/bench.rb +0 -235
- data/bench/broadcast_fp32.rb +0 -140
- data/bench/cg_bench.rb +0 -271
- data/bench/crossover_bench.rb +0 -374
- data/bench/cumo_bench.rb +0 -218
- data/bench/cumo_probe.rb +0 -795
- data/bench/cumo_shape_probe.rb +0 -264
- data/bench/cumo_sync_probe.rb +0 -505
- data/bench/ising_bench.rb +0 -242
- data/bench/kmeans_bench.rb +0 -254
- data/bench/numo_bench.rb +0 -142
- data/bench/particle_bench.rb +0 -261
- data/bench/reduction_fp32.rb +0 -119
- data/bench/transformer_bench.rb +0 -264
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/docker-build.sh +0 -4
- data/docker-launch.sh +0 -4
- data/test/bfloat_test.rb +0 -238
- data/test/bit_test.rb +0 -1237
- data/test/cuda/compiler_test.rb +0 -153
- data/test/cuda/device_test.rb +0 -73
- data/test/cuda/driver_test.rb +0 -108
- data/test/cuda/elementwise_kernel_test.rb +0 -184
- data/test/cuda/function_test.rb +0 -162
- data/test/cuda/memory_pool_test.rb +0 -179
- data/test/cuda/module_test.rb +0 -15
- data/test/cuda/name_expression_test.rb +0 -172
- data/test/cuda/nvrtc_test.rb +0 -119
- data/test/cuda/pinned_memory_test.rb +0 -138
- data/test/cuda/reduction_kernel_test.rb +0 -189
- data/test/cuda/runtime_test.rb +0 -74
- data/test/cuda/stream_test.rb +0 -246
- data/test/cudnn_test.rb +0 -1008
- data/test/cumo_test.rb +0 -169
- data/test/extra_test.rb +0 -1163
- data/test/fused_test.rb +0 -587
- data/test/hfloat_test.rb +0 -717
- data/test/math_test.rb +0 -983
- data/test/narray_alt_coverage_test.rb +0 -980
- data/test/narray_test.rb +0 -8298
- data/test/ractor_test.rb +0 -54
- data/test/test_helper.rb +0 -105
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48de893a0d01a5e8376c2a9cda402be28dd47835661f22bd69e7ed17561abed8
|
|
4
|
+
data.tar.gz: c403ffc7d25b80a0b50e8513b81439ee46f775cbd8c126667f83656077488a79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '00189a16920241771a70155a98d60c5d53b6fcd23bfcddbaed3094bbbe5d3476b2d6b5ce70f9d8d899e090a9bf3be2c4249c1b98b6d14360736a4a5ba1aa5594'
|
|
7
|
+
data.tar.gz: bd6a1c8b78837b387f93f5b57f13c26917c71c123a1441deff8e3afb7250a42bc799ded9271b9ad9d46d4c3b928660c38653f85427b024bd00797d6786535734
|
data/.yardopts
ADDED
|
@@ -38,7 +38,7 @@ module MakeMakefileCuda
|
|
|
38
38
|
# TODO(sonots): Make it possible to configure "nvcc" and additional arguments
|
|
39
39
|
def nvcc_command
|
|
40
40
|
s = MakeMakefileCuda::Nvcc.generate(argv)
|
|
41
|
-
cmd = "nvcc #{s}"
|
|
41
|
+
cmd = "nvcc #{s} -Xfatbin=-compress-all"
|
|
42
42
|
if ENV['CUMO_NVCC_GENERATE_CODE']
|
|
43
43
|
ENV['CUMO_NVCC_GENERATE_CODE'].split.each do |code|
|
|
44
44
|
cmd << " --generate-code=#{code}"
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
|
+
# 0.12.0 (2026/10/04)
|
|
2
|
+
|
|
3
|
+
Fixes:
|
|
4
|
+
|
|
5
|
+
* Fix a stream left current in a thread that ended never being destroyed. Neither `Stream#destroy` nor the finalizer could free it, so about 0.5 MiB of device memory stayed behind per such stream, and under M:N threads the next Ruby thread on the same native thread inherited it. The current stream now lives on the Ruby `Thread`, which adds about 2% to the fixed cost of a launch (PR #593)
|
|
6
|
+
* Fix a device memory leak each time a thread that used cuBLAS, cuSOLVER or cuDNN ends: 64 MiB per thread for cuBLAS, 32 MiB more for cuSOLVER and about 8 MiB for cuDNN. An ending thread now leaves its handles to the next one, so device memory stays at what the most threads alive at once needed (PR #591)
|
|
7
|
+
* Fix `x.inplace.cumsum` and `cumprod` answering a new array and leaving `x` as it was, which they did since 0.10.0 (PR #582)
|
|
8
|
+
|
|
9
|
+
Changes:
|
|
10
|
+
|
|
11
|
+
* Add `Cumo::Linalg`, the functions of `Numo::Linalg` from numo-linalg-alt 0.10.1 on cuSOLVER and cuBLAS, with the same names, arguments, return values and errors: `blas_char`, `dot`, `matmul`, `matrix_power`, `solve`, `inv`, `det`, `slogdet`, `lu`, `lu_fact`, `lu_solve`, `lu_inv`, `cholesky`, `cho_fact`, `cho_solve`, `cho_inv`, `ldl`, `qr`, `eig`, `eigvals`, `eigh`, `eigvalsh`, `svd`, `svdvals`, `pinv`, `lstsq`, `matrix_rank`, `orth`, `null_space`, `norm`, `cond` and `expm`. Load it with `require "cumo/linalg"`. Cumo builds it when the toolkit has cuSOLVER's 64-bit API, and `Cumo::CUDA::Cusolver.available?` says whether it did. `eig` and `eigvals` need `cusolverDnXgeev`, which CUDA 11 does not have, and `ldl` of a Hermitian complex matrix needs `cusolverDnXhetrf`, which CUDA 11 and 12 do not have. Where it differs from numo-linalg-alt on purpose, as in `expm`, `norm` and `cond`, the README says how (PR #594, PR #592, PR #589, PR #588, PR #587, PR #586, PR #584, PR #581, PR #580, PR #577, PR #576, PR #575, PR #574, PR #573, PR #572, PR #571, PR #570, PR #569, PR #568, PR #567)
|
|
12
|
+
* `require "cumo"` sets `CUDA_MODULE_LOADING` to `LAZY` unless it is set already, as PyTorch does. Drivers before 535 otherwise load every kernel module when CUDA starts, which adds about 2.2 GB of RSS. Child processes inherit the setting (PR #590)
|
|
13
|
+
* `triu`, `tril`, `triu!` and `tril!` clear the triangle of a contiguous array with an Integer `k` in one kernel rather than through an index array: `triu` of a 16 x 16 DFloat takes 0.006 ms where it took 0.49 (PR #585)
|
|
14
|
+
* The first kernel of a type costs less host memory. Each type builds separate modules for its reductions, their nan-aware forms and `mulsum` against another type, so DFloat adds 88 MB of RSS for elementwise work and 137 MB once it reduces, where its first kernel of any kind added 181 MB. Every fatbin image is compressed, which takes `cumo.so` from 120 MB to 112 MB (PR #583)
|
|
15
|
+
* The API docs show the NArray methods defined in C, such as `shape` and `NArray.zeros`, and keep the docstrings of the typed-class methods. `.yardopts` keeps the build-time code generators out of the docs, and the gemspec names rubydoc.info as the documentation (PR #579, PR #578)
|
|
16
|
+
* The gem no longer ships the test suite, `bench/` and the files for working on cumo (PR #566)
|
|
17
|
+
|
|
18
|
+
# 0.11.0 (2026/09/27)
|
|
19
|
+
|
|
20
|
+
Breaking changes:
|
|
21
|
+
|
|
22
|
+
* `sort`, `sort_index`, `cumsum` and `cumprod` raise `ArgumentError` on `keepdims:`, which they accepted and ignored, since they keep the shape of their input. They take `axis:` and `nan:`, and `nan:` stays on every type as it does for `sum` and `median`. Numo still accepts `keepdims:` here without a word (PR #552)
|
|
23
|
+
* `reverse` raises `ArgumentError` on `keepdims:` and `nan:`, which it accepted and ignored, and takes `axis:` only. Numo still accepts both without a word (PR #551)
|
|
24
|
+
|
|
25
|
+
Fixes:
|
|
26
|
+
|
|
27
|
+
* Fix a store from an overlapping part of the same array, such as `a[0...-k] = a[k..]`, writing wrong values, since threads read elements other threads had already overwritten. With 10 million SFloat elements and a shift of 1024 every run was wrong, on every path and every dtype. The source is now copied first when it shares an element with the destination, which gives numpy's answer (PR #553, PR #542)
|
|
28
|
+
* Fix `ElementwiseKernel` and `ReductionKernel` racing when an input shares memory with an output but is not read element for element, such as a contiguous input shifted against its output. Such an input is copied first (PR #535)
|
|
29
|
+
* Fix a leak of the new shape when `expand_dims` cannot allocate its strides (PR #563)
|
|
30
|
+
|
|
31
|
+
Changes:
|
|
32
|
+
|
|
33
|
+
* `cumsum` and `cumprod` down axis 0, or along a transposed view, gather and scatter their buffer through a shared-memory tile: on a 2048 x 2048 SFloat, `a.cumsum(axis: 0)` spends 133 us on the GPU where it spent 315, and the gather for `a.transpose.cumsum(axis: 1)` takes 51 us where it took 154 (PR #565)
|
|
34
|
+
* An elementwise op on a view built on an index array gathers it into a buffer a typed element at a time rather than with a memcpy of a run-time size: `a[idx, true] + 1` on a 512 x 512 SFloat spends 2.75 us in the gather where it spent 5.2 (PR #564)
|
|
35
|
+
* Say in the API docs of `MemoryPool.used_bytes` and `total_bytes` what each counts and when, and in the README that `used_bytes` waits for the garbage collector where CuPy's `used_bytes()` and PyTorch's `memory_allocated()` do not (PR #562)
|
|
36
|
+
* `ElementwiseKernel` and `ReductionKernel` accept `Cumo::HFloat` and `Cumo::BFloat` as `float16` and `bfloat16`, CUDA's `__half` and `__nv_bfloat16`. A Ruby number handed to one is rounded on the host the way `Cumo::HFloat[x]` and `Cumo::BFloat[x]` round it (PR #561)
|
|
37
|
+
* CI builds against CUDA 13.4.1 and 12.9.2 where it built against 13.2.1, and keeps 12.8.1 (PR #560)
|
|
38
|
+
* `concatenate`, and `hstack`, `vstack` and `dstack` through it, write every part in one kernel launch when every part is a contiguous array of the result's class, and no longer fill the result with zeros first: `hstack` of six SFloat parts of 1500 x 64 takes 5.2 us where it took 18.3 (PR #559)
|
|
39
|
+
* Elementwise ops, bias adds and math functions such as `gelu` move 16 bytes of each operand a thread when the last axis is contiguous in every operand: at 1500 x 384 and 1500 x 1536 SFloat, an add takes 4.18 us where it took 5.67, a bias add 15.1 where it took 27.4, and `gelu` 16.7 where it took 23.7. An HFloat add is 2.3 times faster and an Int8 add 3.6 times. Integer `div`, `mod` and `reciprocal`, and DComplex, keep the old loop (PR #558)
|
|
40
|
+
* Say in the README and in comments why one spelling is faster than another rather than the timings of one GPU, which read as wrong on another card (PR #557, PR #556)
|
|
41
|
+
* `layer_norm` and `rms_norm` hold a row in registers when it divides into sixteen-byte vectors and fits in eight of them a thread, and read it once: `layer_norm` over 1500 x 384 takes 6.7 us where it took 18.5. `layer_norm` takes the variance about the mean in a second pass, which is also more accurate, and a row whose sum overflows near the top of the range stays finite (PR #555)
|
|
42
|
+
* `softmax` holds a row of up to 16 elements a thread in registers and reads it once: 1500 x 1500 takes 18.8 us where it took 34.4. SFloat and DFloat answers move by a few ulp, and HFloat and BFloat come closer to the reference, since `exp` is no longer rounded to half precision before the scale (PR #554)
|
|
43
|
+
* `sort`, `sort_index`, `cumsum`, `cumprod` and `reverse` accept an empty array and answer an empty array of the same shape, where they raised. A missing axis still raises `DimensionError` (PR #550, PR #549)
|
|
44
|
+
* A copy that the wide byte copy cannot take, as reductions, `argmax` and `sort` make of a non-contiguous input, goes a typed element at a time: `argmax` over a column slice runs 1.9 times faster and a copy of a column slice 1.3 times (PR #548)
|
|
45
|
+
* A kernel over a view of 2 to 4 dimensions whose offsets fit an int32 does its address arithmetic in 32 bits: a store with a cast, `abs` or `clip` on such a view runs 1.17 to 1.32 times faster (PR #547)
|
|
46
|
+
* A DFloat `logseq` of base 10 or 2 calls `exp10` or `exp2` rather than `pow`: 200,000 elements take 20 us where they took 84 (PR #546)
|
|
47
|
+
* The rank switch of every generated kernel launcher comes from one helper in the kernel generator, and the generated sources are token for token what they were (PR #545)
|
|
48
|
+
* `logseq` writes a strided, index or transposed view in one launch rather than one per row: a 6250 x 32 DFloat view takes 87 us where it took 14 ms (PR #544)
|
|
49
|
+
* A reduction along a last axis of 16 elements or fewer gives a row that spans more than one 32-byte sector two threads: `Cumo::SFloat.ones(1536, 64)[true, 0...16].sum(axis: 1)` takes 2.18 us where it took 4.20, and `max`, `mean`, `mulsum` and `argmax` over short rows run 1.2 to 1.8 times faster (PR #543)
|
|
50
|
+
* A store between views of the same type moves 16, 8, 4 or 2 bytes a thread over a run contiguous and aligned on both sides, through one set of kernels the copy behind `dup` shares: the store of an im2col unfold runs about 2 times faster (PR #541)
|
|
51
|
+
* `cumsum` and `cumprod` no longer wait for the GPU. A 1-D scan shorter than 8192 elements ran on the host after a device synchronize, and every other shape waited for the stream to drain: `x = a * 1; x.cumsum + 1` takes about 9 us where it took 0.3 to 0.8 ms (PR #540)
|
|
52
|
+
* `rand`, `rand_norm` and `seq` write a view built on an index array directly rather than through a buffer copied out and back, and so do `real=` and `imag=`: `rand` into a `[6250, 1024][true, idx64]` SFloat view takes 12.7 us where it took 80.7. An axis of length one merges with its neighbour, so a `[N, 1]` column runs what a flat array runs. A seed draws the same values as before (PR #539)
|
|
53
|
+
* `ElementwiseKernel` and `ReductionKernel` read a reversed or stepped view, or a frozen array, where it is rather than copying it on every call, and `ReductionKernel` reads a transposed view the same way: a reduction along the first axis of a transposed 4000 by 4000 array no longer pays a copy that took longer than the reduction. An `ElementwiseKernel` input that is transposed is still copied, in tiles, which beats reading it across rows (PR #538, PR #535)
|
|
54
|
+
* `ReductionKernel` over a leading axis gives a block up to 32 outputs that lie end to end, so a warp reads neighbouring outputs: a sum over axis 0 of a [4000, 4000] SFloat takes 0.169 ms where it took 0.413, what Cumo's own `sum` takes (PR #537)
|
|
55
|
+
* `ElementwiseKernel` and `ReductionKernel` walk neighbouring dimensions that every array lays out end to end as one: a `ReductionKernel` sum over everything of a [160, 100, 1000] SFloat takes 0.162 ms where it took 0.193 (PR #536)
|
|
56
|
+
* Fix a stream test that failed about once in a thousand runs, since its fill on a non-blocking stream was not ordered before its launch on the null stream (PR #534)
|
|
57
|
+
* Say in the README where `CUMO_ALLOW_TF32` pays and where it does not: it speeds up matrix products and convolutions, buys nothing where reading the weights takes the time, and its rounding can push a training step past a tolerance single precision meets (PR #533)
|
|
58
|
+
* A number stored with `a[...] = x`, `a[] = x` or `store(x)` goes through `fill`, one kernel launch with nothing reserved, where it filled a 0-dimensional array first. `fill` writes a view built on an index array or a mask directly rather than through a copy of the whole view (PR #532)
|
|
59
|
+
|
|
1
60
|
# 0.10.0 (2026/09/22)
|
|
2
61
|
|
|
3
62
|
Breaking changes:
|
data/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Cumo (pronounced "koomo") is a CUDA-aware, GPU-optimized numerical library that
|
|
|
10
10
|
* NVIDIA GPU Compute Capability 3.5 (Kepler) or later
|
|
11
11
|
* CUDA 11.0 or later
|
|
12
12
|
* cuDNN 8.0 or later (optional, for the cuDNN features)
|
|
13
|
+
* cuSOLVER with its 64-bit API, which comes with the CUDA Toolkit (optional, for `Cumo::Linalg`)
|
|
13
14
|
|
|
14
15
|
## Preparation
|
|
15
16
|
|
|
@@ -185,21 +186,11 @@ The 0-dimensional return is what lets an iterative loop stay on the GPU.
|
|
|
185
186
|
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.
|
|
186
187
|
What a read costs is not a fixed price either: it is however much work happens to be queued when it is taken.
|
|
187
188
|
|
|
188
|
-
`bench/cg_bench.rb`
|
|
189
|
-
|
|
190
|
-
```
|
|
191
|
-
scalars convergence test us/iter readbacks/iter
|
|
192
|
-
Ruby Floats every iteration 136.7 2.02
|
|
193
|
-
Ruby Floats never 134.4 2.02
|
|
194
|
-
0-dim NArray every iteration 142.6 1.02
|
|
195
|
-
0-dim NArray every 20th 65.6 0.06
|
|
196
|
-
0-dim NArray never 60.3 0.02
|
|
197
|
-
```
|
|
198
|
-
|
|
189
|
+
`bench/cg_bench.rb` measures this with a conjugate gradient solve over a 512x512 grid, with the scalars kept either way and the convergence test taken every iteration, every twentieth or never.
|
|
199
190
|
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.
|
|
200
|
-
Thinning the test cannot get under that floor, and keeping the scalars as 0-dimensional NArrays buys nothing on its own.
|
|
201
|
-
The two only pay together, and together they
|
|
202
|
-
The relative residual is identical
|
|
191
|
+
Thinning the test cannot get under that floor, and keeping the scalars as 0-dimensional NArrays buys nothing on its own, since the test still reads back every iteration.
|
|
192
|
+
The two only pay together, and together they let the loop run well ahead of the host.
|
|
193
|
+
The relative residual is identical either way.
|
|
203
194
|
|
|
204
195
|
```ruby
|
|
205
196
|
alpha = rs_old / pap # a 0-dimensional NArray, divided on the device
|
|
@@ -208,8 +199,8 @@ x += p_dir * alpha # and consumed there, without crossing the bus
|
|
|
208
199
|
|
|
209
200
|
Read the value back once the loop is done, or every k iterations if it has to test something.
|
|
210
201
|
|
|
211
|
-
A read costs the wait and a copy of the block it needs into pinned host memory
|
|
212
|
-
Reading managed memory from the host directly would fault its page over instead, and a small block shares a page with other live blocks that the next kernel touches, so a fresh scalar
|
|
202
|
+
A read costs the wait and a copy of the block it needs into pinned host memory.
|
|
203
|
+
Reading managed memory from the host directly would fault its page over instead, and a small block shares a page with other live blocks that the next kernel touches, so a fresh scalar costs far more that way than the copy does.
|
|
213
204
|
The reads that answer values, from `Float(x)` and `to_a` to `each` and `inspect`, take the copy.
|
|
214
205
|
|
|
215
206
|
### Reshape Copies, Reshape! Does Not
|
|
@@ -226,18 +217,7 @@ a.reshape(2, 12)[0, 0] = 99.0 # a copy, so a does not
|
|
|
226
217
|
This is what Numo does too, and numpy is where the expectation comes from: there `reshape` answers a view whenever the strides allow one.
|
|
227
218
|
On a GPU the difference is an allocation and a copy kernel, paid every call.
|
|
228
219
|
`reshape!` changes the receiver in place and costs neither.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
```
|
|
232
|
-
shape reshape reshape! the copy allocates
|
|
233
|
-
1x768 5.1 us 0.04 us 3 KB
|
|
234
|
-
1024x768 39.1 us 0.18 us 3 MB
|
|
235
|
-
4096x768 58.1 us 0.22 us 12 MB
|
|
236
|
-
8192x768 142.7 us 0.43 us 24 MB
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
`reshape!` is host-side bookkeeping, so it stays under a microsecond whatever the array weighs.
|
|
240
|
-
The `reshape` column is the copy, and it grows with the bytes.
|
|
220
|
+
It is host-side bookkeeping, so its cost does not depend on what the array weighs, where the copy `reshape` makes grows with the bytes.
|
|
241
221
|
|
|
242
222
|
The catch is that `reshape!` changes the array everything else is holding.
|
|
243
223
|
It fits a temporary the calling expression owns, and not an argument, an ivar, or anything a cache still points at:
|
|
@@ -262,28 +242,17 @@ a + 2.0 #=> Cumo::SFloat
|
|
|
262
242
|
```
|
|
263
243
|
|
|
264
244
|
Numo promotes the same way, and on a CPU it costs nothing: Numo's single-precision math computes in double and narrows the result anyway.
|
|
265
|
-
On a GeForce card, whose double-precision rate is a
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
```
|
|
269
|
-
SFloat DFloat
|
|
270
|
-
a * 2.0 11.8 us 12.8 us
|
|
271
|
-
sqrt 11.0 us 43.0 us
|
|
272
|
-
sin 11.5 us 97.5 us
|
|
273
|
-
atan 11.1 us 122.9 us
|
|
274
|
-
atan2 12.5 us 192.4 us
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
Only the transcendentals pay for the promotion; a double multiply runs at the speed of a single one.
|
|
245
|
+
On a GeForce card, whose double-precision rate is a small fraction of its single-precision one, it costs a great deal.
|
|
246
|
+
Only the transcendentals pay for the promotion, and the more work the function does the more they pay: a double multiply runs at about the speed of a single one, while `sqrt`, `sin`, `atan` and `atan2` in double take several times as long as in single.
|
|
278
247
|
The methods a Float can reach as a second argument are `atan2`, `hypot` and `ldexp`.
|
|
279
|
-
`ldexp` pays a different way, since scaling by a power of two is cheap in either precision: `Cumo::NMath.ldexp(a, 2.0)`
|
|
248
|
+
`ldexp` pays a different way, since scaling by a power of two is cheap in either precision: `Cumo::NMath.ldexp(a, 2.0)` is still far slower than `Cumo::NMath.ldexp(a, 2)`, and the difference there is the doubled arrays it has to allocate rather than the arithmetic.
|
|
280
249
|
|
|
281
250
|
Pass a 0-dimensional array instead of a Float and the call stays single precision.
|
|
282
251
|
That is what `[]` hands back, so a scalar taken out of an array is already in the right form:
|
|
283
252
|
|
|
284
253
|
```ruby
|
|
285
254
|
two = Cumo::SFloat[2.0][0] # a 0-dimensional Cumo::SFloat
|
|
286
|
-
Cumo::NMath.atan2(a, two) #=> Cumo::SFloat
|
|
255
|
+
Cumo::NMath.atan2(a, two) #=> Cumo::SFloat
|
|
287
256
|
```
|
|
288
257
|
|
|
289
258
|
Naming the module directly works too, under both libraries:
|
|
@@ -300,22 +269,9 @@ The 0-dimensional form has no effect under Numo, where `[]` returns a Ruby Float
|
|
|
300
269
|
That saves the copy and the memory it needs.
|
|
301
270
|
What it costs is the kernel cuBLAS then picks, which for most shapes is slower than the one it picks for an operand already laid out its way.
|
|
302
271
|
|
|
303
|
-
RTX 5070 Ti Laptop, `Cumo::SFloat`, `q[M,K].dot(k[N,K].transpose)`, medians of nine rounds:
|
|
304
|
-
|
|
305
|
-
```
|
|
306
|
-
M K N M*K as it is copied first the copy
|
|
307
|
-
1 64 1500 64 5.6 us 11.4 us 2.8 us
|
|
308
|
-
512 64 512 32,768 8.5 11.2 2.9
|
|
309
|
-
1500 64 1500 96,000 33.0 29.3 3.0
|
|
310
|
-
512 256 512 131,072 21.8 20.7 2.8
|
|
311
|
-
4096 64 4096 262,144 229.1 179.3 4.2
|
|
312
|
-
512 768 512 393,216 54.0 39.2 3.3
|
|
313
|
-
256 3072 768 786,432 131.1 103.0 12.5
|
|
314
|
-
```
|
|
315
|
-
|
|
316
272
|
The copy weighs `N * K`, and the faster kernel it buys is worth `M * N * K`, so `M` is what decides.
|
|
317
|
-
A matrix-vector product, where `M` is one, is the clearest case against copying
|
|
318
|
-
|
|
273
|
+
A matrix-vector product, where `M` is one, is the clearest case against copying.
|
|
274
|
+
As `M * K` grows the copy starts paying for itself; where it starts depends on the card, so measure the shape that matters before choosing.
|
|
319
275
|
|
|
320
276
|
Where a profile says one of these multiplications matters, hand it an operand that is already contiguous:
|
|
321
277
|
|
|
@@ -324,13 +280,47 @@ kt = k.transpose.dup # or build k transposed in the first place
|
|
|
324
280
|
q.dot(kt)
|
|
325
281
|
```
|
|
326
282
|
|
|
327
|
-
|
|
328
|
-
A batched multiplication takes another path through the same flag
|
|
283
|
+
All of this is about the two-dimensional case, where cuBLAS is given one matrix.
|
|
284
|
+
A batched multiplication takes another path through the same flag.
|
|
285
|
+
|
|
286
|
+
### Linear Algebra
|
|
287
|
+
|
|
288
|
+
`require "cumo/linalg"` brings in `Cumo::Linalg`, the functions of `Numo::Linalg` from [numo-linalg-alt](https://github.com/yoshoku/numo-linalg-alt) on the GPU.
|
|
289
|
+
They keep numo-linalg-alt's names, arguments, return values and errors, and run on cuSOLVER and cuBLAS.
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
require "cumo/linalg"
|
|
293
|
+
|
|
294
|
+
a = Cumo::DFloat.new(3, 3).rand + Cumo::DFloat.eye(3) * 3
|
|
295
|
+
b = Cumo::DFloat.new(3).rand
|
|
296
|
+
x = Cumo::Linalg.solve(a, b)
|
|
297
|
+
w, v = Cumo::Linalg.eigh(a + a.transpose)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The functions are `blas_char`, `dot`, `matmul`, `matrix_power`, `solve`, `inv`, `det`, `slogdet`, `lu`, `lu_fact`, `lu_solve`, `lu_inv`, `cholesky`, `cho_fact`, `cho_solve`, `cho_inv`, `ldl`, `qr`, `eig`, `eigvals`, `eigh`, `eigvalsh`, `svd`, `svdvals`, `pinv`, `lstsq`, `matrix_rank`, `orth`, `null_space`, `norm`, `cond` and `expm`.
|
|
301
|
+
They take `SFloat`, `DFloat`, `SComplex` and `DComplex`.
|
|
302
|
+
An integer or `Bit` matrix is computed in `DFloat`, as numo-linalg-alt does, and `HFloat` and `BFloat` raise `TypeError`.
|
|
303
|
+
|
|
304
|
+
Cumo looks for cuSOLVER when it is built.
|
|
305
|
+
Without it, the functions that need it raise `NotImplementedError`, and `Cumo::CUDA::Cusolver.available?` answers whether it is there.
|
|
306
|
+
`eig` and `eigvals` also need `cusolverDnXgeev`, which CUDA 11 does not have.
|
|
307
|
+
`ldl` of a Hermitian complex matrix needs `cusolverDnXhetrf`, which CUDA 11 and 12 do not have.
|
|
308
|
+
|
|
309
|
+
Cumo differs from numo-linalg-alt in a few places on purpose:
|
|
310
|
+
|
|
311
|
+
* Where numo-linalg-alt answers wrong, Cumo answers as numpy does: a wide matrix in `orth` and `lstsq`, a zero matrix in `pinv`, a matrix of full rank in `null_space`, an integer matrix in `qr` and `svd`, and empty matrices.
|
|
312
|
+
* `expm` scales and squares the way scipy does, after Al-Mohy and Higham, which stays accurate where the norm of the matrix is large. Passing the order of the Pade approximant, as in `expm(a, 3)`, keeps numo-linalg-alt's algorithm.
|
|
313
|
+
* `norm` takes the 2-norm and the nuclear norm of matrices stacked along other axes, which numo-linalg-alt refuses.
|
|
314
|
+
* `cond` checks `ord` before it inverts the matrix, so an invalid `ord` raises `ArgumentError` even for a singular matrix, where numo-linalg-alt raises `LapackError`.
|
|
315
|
+
|
|
316
|
+
A function waits for the current stream when it answers a Ruby number, or when it reads back whether cuSOLVER could do its work.
|
|
317
|
+
`solve`, `inv` and `det` read back whether the matrix was singular, `cho_fact` whether it was positive definite, and `eigh` and `svd` whether they converged.
|
|
318
|
+
`matmul`, `dot` of two matrices, `matrix_power` to a positive power, `cholesky`, `cho_solve`, `qr` and `norm` along an axis wait for nothing, so a run of them is queued without the host waiting.
|
|
329
319
|
|
|
330
320
|
### Fused Operations
|
|
331
321
|
|
|
332
322
|
`layer_norm`, `rms_norm` and `softmax` normalize along the last axis in one kernel each, and `quantize_symmetric` takes it to 8-bit integers in one more.
|
|
333
|
-
Written out of the operators they take nine launches, six, five and six, and a launch
|
|
323
|
+
Written out of the operators they take nine launches, six, five and six, and a launch has a fixed cost whatever it is handed, so a short row pays for the launches rather than for its bytes.
|
|
334
324
|
|
|
335
325
|
```ruby
|
|
336
326
|
y = x.layer_norm(gamma, beta, eps: 1e-5) # (x - mean) / sqrt(var + eps) * gamma + beta
|
|
@@ -351,47 +341,8 @@ numpy, torch -2 -2 0 0 2 2
|
|
|
351
341
|
|
|
352
342
|
`rint` is the other rule, at the cost of writing the quantization out: `(x / scale[false, :new]).rint.clip(-127, 127)` answers what numpy answers. A tie needs `x` to be an exact odd multiple of half the scale, so whether one ever comes up is a property of the data rather than of the arithmetic. Over a million random single-precision elements the two spellings disagreed four times, and every disagreement was a tie.
|
|
353
343
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
```
|
|
357
|
-
layer_norm SFloat DFloat HFloat
|
|
358
|
-
shape fused written fused written fused written
|
|
359
|
-
1 x 768 3.9 30.1 9.7 30.3 3.9 29.8
|
|
360
|
-
256 x 768 6.6 22.5 22.0 35.4 7.8 22.4
|
|
361
|
-
4096 x 768 32.7 177.4 300.7 610.5 28.2 142.9
|
|
362
|
-
1 x 1000000 19.0 59.4 125.4 175.4 22.3 68.5
|
|
363
|
-
|
|
364
|
-
softmax SFloat DFloat HFloat
|
|
365
|
-
shape fused written fused written fused written
|
|
366
|
-
1 x 768 4.3 19.8 7.4 26.5 6.3 34.4
|
|
367
|
-
256 x 768 8.8 13.9 23.4 35.1 6.8 18.5
|
|
368
|
-
4096 x 768 49.2 122.7 318.3 549.0 28.2 109.8
|
|
369
|
-
1 x 1000000 27.8 44.0 108.6 148.5 32.1 53.8
|
|
370
|
-
|
|
371
|
-
rms_norm SFloat DFloat HFloat
|
|
372
|
-
shape fused written fused written fused written
|
|
373
|
-
1 x 768 2.9 14.2 5.8 14.1 3.1 15.1
|
|
374
|
-
256 x 768 4.6 16.7 6.9 20.2 5.2 15.5
|
|
375
|
-
4096 x 768 38.5 122.5 130.8 367.0 19.2 80.8
|
|
376
|
-
1 x 1000000 12.5 35.8 23.7 102.2 12.0 33.2
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
```
|
|
380
|
-
quantize_symmetric SFloat DFloat HFloat
|
|
381
|
-
shape fused written fused written fused written
|
|
382
|
-
1 x 768 3.7 20.2 6.0 15.7 2.3 11.9
|
|
383
|
-
256 x 768 4.7 15.0 20.2 21.3 4.7 13.0
|
|
384
|
-
4096 x 768 24.4 105.4 289.5 333.3 23.8 80.3
|
|
385
|
-
1 x 1000000 18.8 40.8 97.1 81.6 15.7 37.5
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
The tables were taken in separate sessions, so read each row against the row beside it and not across the tables.
|
|
389
|
-
|
|
390
|
-
`quantize_symmetric` in `Cumo::DFloat` is worth less than the others and loses outright on a million elements, whatever shape they are in. The division it does per element is what costs: on this card one row of a million takes 44.0 us to divide in double against 10.8 in single, where the reduction over the same row takes 25.3 and 15.4. Six kernels give that division a kernel of its own to fill the device with, and one kernel leaves it behind the reduction.
|
|
391
|
-
|
|
392
|
-
All three pay off in every precision, by the most where the row is short enough that the launches were all it was doing, and by the least in double, where the reduction itself costs more than the launches ever did.
|
|
393
|
-
The memory clock on this card steps between 9001 and 11001 MHz under a benchmark this short, and the absolute figures move with it.
|
|
394
|
-
The ratios hold across the steps.
|
|
344
|
+
Against the same arithmetic spelled with operators they are faster, by the most where the row is short enough that the launches were all it was doing, and by the least in double, where the arithmetic itself costs more than the launches ever did.
|
|
345
|
+
The exception is `quantize_symmetric` in `Cumo::DFloat`, which can lose on a long row. The division it does per element is what costs, since double is slow on a card with a low double-precision rate: six kernels give that division a kernel of its own to fill the device with, and one kernel leaves it behind the reduction.
|
|
395
346
|
|
|
396
347
|
All three normalize along the last axis only.
|
|
397
348
|
Other axes are reachable through `transpose`.
|
|
@@ -447,17 +398,7 @@ It is also called Swish.
|
|
|
447
398
|
Cumo::NMath.silu(x) # x / (1 + exp(-x)), in one kernel rather than five
|
|
448
399
|
```
|
|
449
400
|
|
|
450
|
-
Written out of the operators it costs five launches, and one kernel
|
|
451
|
-
|
|
452
|
-
```
|
|
453
|
-
silu SFloat DFloat HFloat
|
|
454
|
-
elements fused written fused written fused written
|
|
455
|
-
768 3.6 13.4 3.6 9.5 3.4 14.3
|
|
456
|
-
786432 7.0 25.9 90.5 102.4 5.2 19.2
|
|
457
|
-
16777216 353.2 1312.9 1984.7 3258.8 199.6 851.8
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
The three rows are three regimes rather than one curve: 768 elements pay for the launches, 786432 fit in L2 and move faster than this card reads from memory, and 16777216 are what it costs from DRAM.
|
|
401
|
+
Written out of the operators it costs five launches, and one kernel is faster in every precision, by the most on an array small enough that the launches were all it was doing.
|
|
461
402
|
|
|
462
403
|
It follows `torch.nn.functional.silu`, answering within one unit in the last place of the true value at the nine double points measured, as torch does.
|
|
463
404
|
At the edges it matches torch exactly: `-Float::INFINITY` answers `NaN`, since that is what infinity times zero is, and a large negative `x` answers a signed zero.
|
|
@@ -474,15 +415,7 @@ Half reaches it at -21, because the round back to half gets there first.
|
|
|
474
415
|
Cumo::NMath.sigmoid(x) # in one kernel rather than four
|
|
475
416
|
```
|
|
476
417
|
|
|
477
|
-
Written out of the operators it costs four launches, and one kernel
|
|
478
|
-
|
|
479
|
-
```
|
|
480
|
-
sigmoid SFloat DFloat HFloat
|
|
481
|
-
elements fused written fused written fused written
|
|
482
|
-
768 2.9 9.0 2.5 8.8 2.0 9.6
|
|
483
|
-
786432 5.6 23.0 94.6 111.3 7.3 23.6
|
|
484
|
-
16777216 343.5 1234.7 1978.5 2941.1 210.5 798.1
|
|
485
|
-
```
|
|
418
|
+
Written out of the operators it costs four launches, and one kernel is faster in every precision, by the most on an array small enough that the launches were all it was doing.
|
|
486
419
|
|
|
487
420
|
The kernel keeps the exponent's argument negative, which the plain quotient does not: writing `1 / (1 + exp(-x))` out asks `exp` for a value it cannot hold once `x` is negative enough, and the quotient then answers a zero where the curve is still a number the type carries.
|
|
488
421
|
|
|
@@ -513,17 +446,8 @@ A non-negative `x` takes the plain quotient unchanged, bit for bit, so only the
|
|
|
513
446
|
Cumo::NMath.softplus(x) # log(1 + exp(x)), in one kernel rather than three
|
|
514
447
|
```
|
|
515
448
|
|
|
516
|
-
Written out of the operators it costs three launches, and one kernel
|
|
517
|
-
|
|
518
|
-
```
|
|
519
|
-
softplus SFloat DFloat HFloat
|
|
520
|
-
elements fused written fused written fused written
|
|
521
|
-
768 2.0 6.1 2.3 6.2 2.1 6.0
|
|
522
|
-
786432 5.9 18.3 166.6 171.8 5.7 15.0
|
|
523
|
-
16777216 344.2 939.5 3503.7 3938.2 206.2 591.2
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
Double barely moves at the two larger sizes, the arithmetic rather than the launches being what it pays for there.
|
|
449
|
+
Written out of the operators it costs three launches, and one kernel is faster, by the most on an array small enough that the launches were all it was doing.
|
|
450
|
+
Double gains little on a large array, the arithmetic rather than the launches being what it pays for there.
|
|
527
451
|
|
|
528
452
|
Writing it out also gives out earlier than the kernel does, because the intermediate is an array of the receiver's type:
|
|
529
453
|
|
|
@@ -603,54 +527,22 @@ a.gemm(b, alpha: 0.001) #=> 102.375, the true 102400 scaled down
|
|
|
603
527
|
|
|
604
528
|
#### What half is faster at
|
|
605
529
|
|
|
606
|
-
`gemm` reaches the tensor cores
|
|
607
|
-
|
|
608
|
-
```
|
|
609
|
-
HFloat SFloat DFloat
|
|
610
|
-
1024x1024 0.043 ms 49.7 TF 0.161 ms 13.3 TF 5.370 ms 0.40 TF
|
|
611
|
-
2048x2048 0.362 ms 47.4 TF 1.360 ms 12.6 TF 43.46 ms 0.40 TF
|
|
612
|
-
4096x4096 3.335 ms 41.2 TF 10.40 ms 13.2 TF 328.1 ms 0.42 TF
|
|
613
|
-
```
|
|
530
|
+
`gemm` reaches the tensor cores, which single and double precision do not, so a large matrix product runs several times faster in half than in single.
|
|
614
531
|
|
|
615
532
|
An odd number of columns costs half far more than it costs the others, because a row then starts on a two-byte boundary and the vectorized path is gone.
|
|
616
|
-
It is the column count of either operand that matters, not the row count.
|
|
617
|
-
1024x1024 times 1024x1024, with one dimension made odd at a time:
|
|
618
|
-
|
|
619
|
-
```
|
|
620
|
-
all even M odd K odd N odd
|
|
621
|
-
HFloat 51.8 TF 49.2 TF 23.3 TF 23.3 TF
|
|
622
|
-
SFloat 11.9 TF - 11.6 TF 10.8 TF
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
The run-to-run spread on these is a few per cent and reaches fifteen at the top end, so the M column says the row count does not matter rather than that it costs 5 per cent.
|
|
533
|
+
It is the column count of either operand that matters, not the row count: making `K` or `N` odd loses a large share of the speed, where making `M` odd does not.
|
|
626
534
|
|
|
627
535
|
The penalty is on the arithmetic, so it does not reach a matrix-vector product, which is bound by how fast the matrix can be read whatever its shape.
|
|
628
|
-
A 1x768 by 768x50257 gemv
|
|
536
|
+
A 1x768 by 768x50257 gemv runs as fast with that odd 50257 as with 50256, while the same matrix against 256 rows does not.
|
|
629
537
|
Pad the inner dimensions of a real matrix product; leave a gemv alone.
|
|
630
538
|
|
|
631
539
|
`conv` is a different story, and worth reading before reaching for half in a network.
|
|
632
540
|
cuDNN chooses its algorithm from the ones that fit in a scratch buffer, and the half algorithms that use the tensor cores ask for a lot of it.
|
|
633
|
-
The default ceiling reaches them; the 8MB one Cumo used to ship does not.
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
```
|
|
637
|
-
HFloat BFloat SFloat
|
|
638
|
-
CUMO_CUDNN_MAX_WORKSPACE_SIZE=8388608 1.27 ms 1.26 ms 1.19 ms
|
|
639
|
-
unset (128MB) 0.54 ms 0.60 ms 1.19 ms
|
|
640
|
-
CUMO_CUDNN_MAX_WORKSPACE_SIZE=268435456 0.52 ms 0.59 ms 0.59 ms
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
This shape's single-precision algorithm wants more than the default, which is the other half of the reason to look at the ceiling for a network that spends its time in `conv`.
|
|
541
|
+
The default ceiling reaches them; the 8MB one Cumo used to ship does not, and under it a half convolution such as N=32, C=K=64, 56x56, 3x3 runs no faster than single.
|
|
542
|
+
The single-precision algorithm for that shape wants more than the default, which is the other half of the reason to look at the ceiling for a network that spends its time in `conv`.
|
|
644
543
|
|
|
645
544
|
Tensor cores also want the channel counts to be multiples of eight, which the first layer of a network never satisfies.
|
|
646
|
-
That layer
|
|
647
|
-
|
|
648
|
-
```
|
|
649
|
-
C=3, K=64, 56x56, 3x3 0.085 ms 0.170 ms
|
|
650
|
-
C=K=64, 56x56, 1x1 0.037 ms 0.120 ms
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
Neither of those reaches a tensor core; they move half the bytes.
|
|
545
|
+
That layer can still be faster in half, but for the other reason: it moves half the bytes rather than reaching a tensor core.
|
|
654
546
|
|
|
655
547
|
#### Batch normalization takes single-precision parameters
|
|
656
548
|
|
|
@@ -713,23 +605,8 @@ What replaces it is the mantissa: a bfloat16 resolves about three decimal digits
|
|
|
713
605
|
|
|
714
606
|
#### What bfloat16 is faster at
|
|
715
607
|
|
|
716
|
-
`gemm` reaches the tensor cores and
|
|
717
|
-
|
|
718
|
-
```
|
|
719
|
-
BFloat HFloat SFloat
|
|
720
|
-
1024x1024 0.044 ms 48.9 TF 0.044 ms 48.5 TF 0.144 ms 14.9 TF
|
|
721
|
-
2048x2048 0.298 ms 57.6 TF 0.303 ms 56.7 TF 1.404 ms 12.2 TF
|
|
722
|
-
4096x4096 2.839 ms 48.4 TF 2.898 ms 47.4 TF 9.540 ms 14.4 TF
|
|
723
|
-
```
|
|
724
|
-
|
|
608
|
+
`gemm` reaches the tensor cores and runs at the same speed as half.
|
|
725
609
|
The odd-column penalty is the same as half's and for the same reason, a row starting on a two-byte boundary.
|
|
726
|
-
1024x1024 times 1024x1024, with one dimension made odd at a time:
|
|
727
|
-
|
|
728
|
-
```
|
|
729
|
-
all even M odd K odd N odd
|
|
730
|
-
BFloat 48.9 TF 48.7 TF 23.5 TF 23.0 TF
|
|
731
|
-
HFloat 48.5 TF - 24.0 TF 23.1 TF
|
|
732
|
-
```
|
|
733
610
|
|
|
734
611
|
So the choice between the two sixteen-bit types is about range and resolution, not speed.
|
|
735
612
|
Take bfloat16 where the magnitudes came from somewhere else and binary16 where three more mantissa bits are worth having.
|
|
@@ -739,7 +616,7 @@ Take bfloat16 where the magnitudes came from somewhere else and binary16 where t
|
|
|
739
616
|
cuBLAS supports `CUDA_R_16BF`, which is what `gemm` and `dot` reach, from compute capability 8.0.
|
|
740
617
|
Storing, casting and elementwise arithmetic have no such floor, because every operation is computed in single precision and rounded back, and they build and run wherever cumo does.
|
|
741
618
|
A `dot` on a pre-Ampere card is the one to expect trouble from.
|
|
742
|
-
**This is not measured here**: the
|
|
619
|
+
**This is not measured here**: the requirement is read from cuBLAS's documentation rather than reproduced on a card older than Ampere.
|
|
743
620
|
|
|
744
621
|
cuDNN reaches bfloat16 as `CUDNN_DATA_BFLOAT16`, and its own bfloat16 kernels want Ampere for the same reason cuBLAS does.
|
|
745
622
|
A convolution is given `CUDNN_DATA_FLOAT` to accumulate in, so it passes 256 the way a reduction does.
|
|
@@ -879,13 +756,19 @@ squared_diff.call(x, y) # => the (2, 5) array of squared differences
|
|
|
879
756
|
squared_diff.call(x, 5) # => the same against a scalar
|
|
880
757
|
```
|
|
881
758
|
|
|
882
|
-
A type is one of `float64`, `float32`, `int64`, `int32`, `int16`, `int8`, `uint64`, `uint32`, `uint16` and `uint8`, or a single letter that stands for whichever dtype the argument has.
|
|
759
|
+
A type is one of `float64`, `float32`, `float16`, `bfloat16`, `int64`, `int32`, `int16`, `int8`, `uint64`, `uint32`, `uint16` and `uint8`, or a single letter that stands for whichever dtype the argument has.
|
|
883
760
|
Outputs decide a letter before inputs do, and a letter that only a Ruby number reaches becomes `int64` or `float64`.
|
|
884
761
|
Array arguments are broadcast against each other, and an output may be given after the inputs, or is allocated.
|
|
762
|
+
An input is read through its own strides, so a reversed or stepped view goes to the kernel as it is.
|
|
763
|
+
A transposed view is copied first, since the copy reorders it in tiles where the kernel would read it a row apart per thread, and so is a view built on an index array, or an input that shares memory with an output other than element for element, since the kernel would read what another thread has already written; a raw argument has to be contiguous and is copied when it is not.
|
|
885
764
|
An argument marked `raw T y` is handed over as a pointer for the operation to index itself, with `i` the element index and `_ind.size()` the element count, and when every argument is raw or a number, `size:` says how many elements there are.
|
|
886
765
|
Inputs are `const`, so an operation that writes one does not compile, and a number handed to an integer type has to be an Integer that fits.
|
|
887
766
|
`preamble:` is placed before the kernel, after the typedefs of the letters, so a device function can be written in terms of `T`.
|
|
888
|
-
`
|
|
767
|
+
`float16` and `bfloat16` are CUDA's `__half` and `__nv_bfloat16`, compiled with the toolkit's `cuda_fp16.h` and `cuda_bf16.h`, which are found through `CUDA_PATH` or the `nvcc` on `PATH`.
|
|
768
|
+
They convert to and from every arithmetic type, so an operation that mixes one with another type or a literal does not compile until one side is cast: `y = x * T(2)`, `y = x > T(0) ? x : T(0)` or `y = (float)x * 2`.
|
|
769
|
+
With CUDA 11, arithmetic on `float16` does not compile for a GPU below compute capability 5.3, which has none.
|
|
770
|
+
A number handed to one is rounded to it the way `Cumo::HFloat[x]` and `Cumo::BFloat[x]` round it.
|
|
771
|
+
`Cumo::Bit`, `Cumo::SComplex`, `Cumo::DComplex` and `Cumo::RObject` cannot be handed to one of these kernels yet.
|
|
889
772
|
|
|
890
773
|
### Writing A Reduction Kernel
|
|
891
774
|
|
|
@@ -940,7 +823,8 @@ Cumo::CUDA::MemoryPool.total_bytes # what the pool has taken from the card
|
|
|
940
823
|
Cumo::CUDA::MemoryPool.used_bytes # what it has handed out and not taken back
|
|
941
824
|
```
|
|
942
825
|
|
|
943
|
-
`used_bytes` still counts blocks whose last reference is gone but which Ruby's garbage collector has not reached, so read it after `GC.start` when you mean the arrays that are alive
|
|
826
|
+
`used_bytes` still counts blocks whose last reference is gone but which Ruby's garbage collector has not reached, so read it after `GC.start` when you mean the arrays that are alive.
|
|
827
|
+
Python frees an array when its last reference goes, so CuPy's `used_bytes()` and PyTorch's `memory_allocated()` drop at once where this waits for the collector:
|
|
944
828
|
|
|
945
829
|
```ruby
|
|
946
830
|
a = Cumo::SFloat.new(1024, 1024).seq
|
|
@@ -1084,14 +968,8 @@ export CUMO_SHOW_WARNING_ONCE=OFF
|
|
|
1084
968
|
cuDNN picks a convolution algorithm by benchmarking the ones that fit in a scratch buffer, and the ceiling on that buffer is 128MB.
|
|
1085
969
|
The search reserves the whole ceiling whatever the convolution's size and hands it back to the pool afterwards, so the ceiling costs a peak rather than a residency.
|
|
1086
970
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
```
|
|
1090
|
-
ResNet-18 N=32, C=K=64, 56x56, 3x3
|
|
1091
|
-
CUMO_CUDNN_MAX_WORKSPACE_SIZE=8388608 9.80 ms 1.19 ms
|
|
1092
|
-
unset (128MB) 5.23 ms 1.19 ms
|
|
1093
|
-
CUMO_CUDNN_MAX_WORKSPACE_SIZE=268435456 5.24 ms 0.59 ms
|
|
1094
|
-
```
|
|
971
|
+
The old 8MB ceiling kept the twenty convolutions of a ResNet-18 forward pass at batch 16 off their faster algorithms, which the default reaches.
|
|
972
|
+
Some shapes want more than the default, such as the single-precision case from [Half Precision](#half-precision), which reaches its faster algorithm only above it.
|
|
1095
973
|
|
|
1096
974
|
To raise it:
|
|
1097
975
|
|
|
@@ -1116,20 +994,19 @@ To trade the accuracy for the speed:
|
|
|
1116
994
|
export CUMO_ALLOW_TF32=1
|
|
1117
995
|
```
|
|
1118
996
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
```
|
|
1122
|
-
pass worst layer
|
|
1123
|
-
tensor cores off (default) 5.24 ms 1.4e-05
|
|
1124
|
-
tensor cores on 4.31 ms 2.4e-04
|
|
1125
|
-
```
|
|
997
|
+
Over the convolutions of a ResNet-18 forward pass at batch 16, with the ceiling raised to 256MB, the pass gets faster and the worst layer moves from 1.4e-05 to 2.4e-04 against a double precision reference.
|
|
1126
998
|
|
|
1127
999
|
The same flag puts `SFloat` and `SComplex` `gemm` on the tensor cores as TF32, and `dot` where it goes through `gemm`.
|
|
1000
|
+
`Cumo::Linalg.matrix_power` and `Cumo::Linalg.expm` multiply through `gemm`, so the flag reaches them too.
|
|
1128
1001
|
Off, the answer is the one cuBLAS gives at single precision, bit for bit.
|
|
1129
|
-
On, a
|
|
1130
|
-
Its answer is then about 3e-04 from a double precision reference, where it was 4e-07.
|
|
1002
|
+
On, a large `SFloat` `gemm` runs faster, and its answer is then about 3e-04 from a double precision reference, where it was 4e-07.
|
|
1131
1003
|
The double types are not affected either way.
|
|
1132
1004
|
|
|
1005
|
+
What the flag buys depends on where the time goes.
|
|
1006
|
+
It pays where matrix products and convolutions take the time, as in training or in a batch through a convolutional network.
|
|
1007
|
+
It buys nothing where reading the operands takes the time, as in decoding one token at a time, where each step reads every weight once and the GEMM waits on memory rather than on arithmetic.
|
|
1008
|
+
Over a training run the rounding adds up, so a loss that meets a tolerance at single precision can miss it on some steps; compare a few steps with the flag off before relying on it.
|
|
1009
|
+
|
|
1133
1010
|
`Cumo.allow_tf32?` reads back the value in force.
|
|
1134
1011
|
|
|
1135
1012
|
## Contributing
|
data/cumo.gemspec
CHANGED
|
@@ -18,6 +18,7 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
|
|
19
19
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
20
|
spec.metadata["source_code_uri"] = spec.homepage
|
|
21
|
+
spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/cumo/"
|
|
21
22
|
spec.metadata["changelog_uri"] = "https://github.com/sonots/cumo/blob/master/CHANGELOG.md"
|
|
22
23
|
spec.metadata["bug_tracker_uri"] = "https://github.com/sonots/cumo/issues"
|
|
23
24
|
spec.metadata["rubygems_mfa_required"] = "true"
|
|
@@ -25,9 +26,8 @@ Gem::Specification.new do |spec|
|
|
|
25
26
|
spec.required_ruby_version = ">= 3.0.0"
|
|
26
27
|
|
|
27
28
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
28
|
-
f.
|
|
29
|
+
f.start_with?(*%w[.github/ test/ examples/ bin/ bench/ docker- Dockerfile Gemfile .gitignore .rubocop])
|
|
29
30
|
end
|
|
30
|
-
spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
31
31
|
spec.bindir = "exe"
|
|
32
32
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
33
33
|
spec.require_paths = ["lib"]
|
data/ext/cumo/cuda/cublas.c
CHANGED
|
@@ -43,31 +43,22 @@ cumo_cuda_cublas_check_status(cublasStatus_t status)
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
static cumo_cuda_thread_local_t handles;
|
|
47
|
+
|
|
48
|
+
// One handle per thread: the stream is set on the handle before each call,
|
|
49
|
+
// and another thread with another current stream must not reset it in
|
|
50
|
+
// between. A thread that ends leaves its handle to the next one.
|
|
47
51
|
cublasHandle_t
|
|
48
52
|
cumo_cuda_cublas_handle()
|
|
49
53
|
{
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// between. A handle is never destroyed.
|
|
53
|
-
static __thread cublasHandle_t *handles = 0;
|
|
54
|
-
int device;
|
|
55
|
-
if (handles == 0) {
|
|
56
|
-
int i;
|
|
57
|
-
int device_count = cumo_cuda_runtime_get_device_count();
|
|
58
|
-
handles = ALLOC_N(cublasHandle_t, device_count);
|
|
59
|
-
for (i = 0; i < device_count; ++i) {
|
|
60
|
-
handles[i] = 0;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
device = cumo_cuda_runtime_get_device();
|
|
64
|
-
if (handles[device] == 0) {
|
|
54
|
+
cublasHandle_t *handle = (cublasHandle_t*)cumo_cuda_thread_local_get(&handles);
|
|
55
|
+
if (*handle == 0) {
|
|
65
56
|
// A discarded status leaves the handle NULL, and cuBLAS reports that as
|
|
66
57
|
// CUBLAS_STATUS_NOT_INITIALIZED at the next call instead of the reason.
|
|
67
|
-
cumo_cuda_cublas_check_status(cublasCreate(
|
|
58
|
+
cumo_cuda_cublas_check_status(cublasCreate(handle));
|
|
68
59
|
}
|
|
69
|
-
cumo_cuda_cublas_check_status(cublasSetStream(
|
|
70
|
-
return
|
|
60
|
+
cumo_cuda_cublas_check_status(cublasSetStream(*handle, cumo_cuda_stream()));
|
|
61
|
+
return *handle;
|
|
71
62
|
}
|
|
72
63
|
|
|
73
64
|
VALUE
|
|
@@ -88,6 +79,8 @@ Init_cumo_cuda_cublas(void)
|
|
|
88
79
|
VALUE mCumo = rb_define_module("Cumo");
|
|
89
80
|
VALUE mCUDA = rb_define_module_under(mCumo, "CUDA");
|
|
90
81
|
|
|
82
|
+
cumo_cuda_thread_local_init(&handles, sizeof(cublasHandle_t));
|
|
83
|
+
|
|
91
84
|
/*
|
|
92
85
|
Document-module: Cumo::Cublas
|
|
93
86
|
*/
|