cumo 0.9.0 → 0.10.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/CHANGELOG.md +51 -0
- data/README.md +404 -13
- data/bench/cumo_probe.rb +6 -2
- data/bench/cumo_sync_probe.rb +1 -1
- data/cumo.gemspec +1 -1
- data/ext/cumo/cuda/cublas.c +6 -1
- data/ext/cumo/cuda/cudnn.c +5 -1
- data/ext/cumo/cuda/cudnn_impl.cpp +29 -3
- data/ext/cumo/cuda/driver.c +158 -1
- data/ext/cumo/cuda/memory_pool.cpp +3 -7
- data/ext/cumo/cuda/nvrtc.c +42 -0
- data/ext/cumo/cuda/runtime.c +627 -1
- data/ext/cumo/cumo.c +43 -12
- data/ext/cumo/include/cumo/bit_reduce_kernel.h +8 -8
- data/ext/cumo/include/cumo/cuda/cudnn.h +14 -5
- data/ext/cumo/include/cumo/cuda/runtime.h +77 -3
- data/ext/cumo/include/cumo/intern.h +8 -1
- data/ext/cumo/include/cumo/ndloop.h +21 -0
- data/ext/cumo/include/cumo/reduce_kernel.h +14 -14
- data/ext/cumo/include/cumo/row_kernel.h +2 -2
- data/ext/cumo/include/cumo/template_kernel.h +4 -1
- data/ext/cumo/include/cumo/types/f16_macro.h +23 -1
- data/ext/cumo/include/cumo/types/f16_macro_kernel.h +23 -1
- data/ext/cumo/include/cumo/types/float_macro.h +23 -1
- data/ext/cumo/include/cumo/types/float_macro_kernel.h +23 -1
- data/ext/cumo/include/cumo.h +9 -2
- data/ext/cumo/narray/data.c +25 -2
- data/ext/cumo/narray/data_kernel.cu +23 -23
- data/ext/cumo/narray/gen/def/bfloat.rb +0 -2
- data/ext/cumo/narray/gen/def/bit.rb +0 -3
- data/ext/cumo/narray/gen/def/dcomplex.rb +1 -3
- data/ext/cumo/narray/gen/def/dfloat.rb +1 -3
- data/ext/cumo/narray/gen/def/hfloat.rb +0 -2
- data/ext/cumo/narray/gen/def/int16.rb +0 -3
- data/ext/cumo/narray/gen/def/int32.rb +0 -3
- data/ext/cumo/narray/gen/def/int64.rb +0 -3
- data/ext/cumo/narray/gen/def/int8.rb +0 -3
- data/ext/cumo/narray/gen/def/robject.rb +0 -3
- data/ext/cumo/narray/gen/def/scomplex.rb +1 -3
- data/ext/cumo/narray/gen/def/sfloat.rb +1 -3
- data/ext/cumo/narray/gen/def/uint16.rb +0 -3
- data/ext/cumo/narray/gen/def/uint32.rb +0 -3
- data/ext/cumo/narray/gen/def/uint64.rb +0 -3
- data/ext/cumo/narray/gen/def/uint8.rb +0 -3
- data/ext/cumo/narray/gen/narray_def.rb +7 -0
- data/ext/cumo/narray/gen/spec.rb +2 -0
- data/ext/cumo/narray/gen/tmpl/aref_cpu.c +8 -0
- data/ext/cumo/narray/gen/tmpl/binary2_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/binary_kernel.cu +4 -4
- data/ext/cumo/narray/gen/tmpl/binary_s_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/bincount_kernel.cu +4 -4
- data/ext/cumo/narray/gen/tmpl/clip_kernel.cu +6 -6
- data/ext/cumo/narray/gen/tmpl/cond_binary_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/cond_unary_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/conv.c +3 -2
- data/ext/cumo/narray/gen/tmpl/conv_grad_w.c +3 -2
- data/ext/cumo/narray/gen/tmpl/conv_transpose.c +3 -2
- data/ext/cumo/narray/gen/tmpl/cum.c +88 -10
- data/ext/cumo/narray/gen/tmpl/cum_kernel.cu +179 -17
- data/ext/cumo/narray/gen/tmpl/each.c +4 -3
- data/ext/cumo/narray/gen/tmpl/each_with_index.c +4 -3
- data/ext/cumo/narray/gen/tmpl/ewcomp_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/extract_cpu.c +8 -0
- data/ext/cumo/narray/gen/tmpl/extract_data.c +8 -0
- data/ext/cumo/narray/gen/tmpl/eye_kernel.cu +1 -1
- data/ext/cumo/narray/gen/tmpl/fill_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/format.c +2 -2
- data/ext/cumo/narray/gen/tmpl/format_to_a.c +2 -2
- data/ext/cumo/narray/gen/tmpl/frexp_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/gemm.c +21 -28
- data/ext/cumo/narray/gen/tmpl/inspect.c +0 -1
- data/ext/cumo/narray/gen/tmpl/logseq_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/map_with_index.c +5 -1
- data/ext/cumo/narray/gen/tmpl/median.c +9 -4
- data/ext/cumo/narray/gen/tmpl/new_dim0_kernel.cu +1 -1
- data/ext/cumo/narray/gen/tmpl/poly_kernel.cu +3 -3
- data/ext/cumo/narray/gen/tmpl/pow_kernel.cu +4 -4
- data/ext/cumo/narray/gen/tmpl/rand.c +16 -15
- data/ext/cumo/narray/gen/tmpl/rand_kernel.cu +19 -24
- data/ext/cumo/narray/gen/tmpl/rand_norm.c +7 -25
- data/ext/cumo/narray/gen/tmpl/rand_norm_kernel.cu +19 -24
- data/ext/cumo/narray/gen/tmpl/seq_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/set2_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/softmax_kernel.cu +3 -3
- data/ext/cumo/narray/gen/tmpl/sort.c +7 -4
- data/ext/cumo/narray/gen/tmpl/store.c +8 -0
- data/ext/cumo/narray/gen/tmpl/store_array.c +3 -2
- data/ext/cumo/narray/gen/tmpl/store_array_kernel.cu +4 -4
- data/ext/cumo/narray/gen/tmpl/store_bit_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/store_from_kernel.cu +4 -4
- data/ext/cumo/narray/gen/tmpl/to_a.c +1 -2
- data/ext/cumo/narray/gen/tmpl/unary2_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/unary_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/unary_ret2_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/unary_s_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl_bit/aref_cpu.c +3 -2
- data/ext/cumo/narray/gen/tmpl_bit/binary_kernel.cu +4 -4
- data/ext/cumo/narray/gen/tmpl_bit/bit_count.c +6 -5
- data/ext/cumo/narray/gen/tmpl_bit/bit_reduce.c +23 -21
- data/ext/cumo/narray/gen/tmpl_bit/each.c +4 -3
- data/ext/cumo/narray/gen/tmpl_bit/each_with_index.c +4 -3
- data/ext/cumo/narray/gen/tmpl_bit/extract_cpu.c +1 -2
- data/ext/cumo/narray/gen/tmpl_bit/fill_kernel.cu +3 -3
- data/ext/cumo/narray/gen/tmpl_bit/format.c +2 -3
- data/ext/cumo/narray/gen/tmpl_bit/format_to_a.c +2 -3
- data/ext/cumo/narray/gen/tmpl_bit/inspect.c +1 -2
- data/ext/cumo/narray/gen/tmpl_bit/mask_kernel.cu +3 -3
- data/ext/cumo/narray/gen/tmpl_bit/store_array.c +3 -2
- data/ext/cumo/narray/gen/tmpl_bit/store_array_kernel.cu +5 -5
- data/ext/cumo/narray/gen/tmpl_bit/store_bit_kernel.cu +4 -4
- data/ext/cumo/narray/gen/tmpl_bit/store_from_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl_bit/to_a.c +2 -3
- data/ext/cumo/narray/gen/tmpl_bit/unary.c +6 -0
- data/ext/cumo/narray/gen/tmpl_bit/unary_kernel.cu +4 -4
- data/ext/cumo/narray/gen/tmpl_bit/where.c +2 -2
- data/ext/cumo/narray/gen/tmpl_bit/where_kernel.cu +4 -4
- data/ext/cumo/narray/index.c +3 -2
- data/ext/cumo/narray/index_kernel.cu +13 -13
- data/ext/cumo/narray/narray.c +130 -41
- data/ext/cumo/narray/ndloop.c +322 -23
- data/ext/cumo/narray/ndloop_kernel.cu +18 -18
- data/ext/cumo/narray/sort_kernel.cu +88 -40
- data/lib/cumo/cuda/compiler.rb +83 -30
- data/lib/cumo/cuda/device.rb +8 -1
- data/lib/cumo/cuda/elementwise_kernel.rb +131 -0
- data/lib/cumo/cuda/event.rb +68 -0
- data/lib/cumo/cuda/function.rb +45 -0
- data/lib/cumo/cuda/module.rb +30 -3
- data/lib/cumo/cuda/nvrtc_program.rb +16 -1
- data/lib/cumo/cuda/pinned_memory.rb +93 -0
- data/lib/cumo/cuda/reduction_kernel.rb +279 -0
- data/lib/cumo/cuda/stream.rb +114 -0
- data/lib/cumo/cuda/user_kernel.rb +169 -0
- data/lib/cumo/cuda.rb +6 -0
- data/lib/cumo/narray/extra.rb +30 -1
- data/test/bit_test.rb +233 -7
- data/test/cuda/compiler_test.rb +84 -0
- data/test/cuda/device_test.rb +42 -0
- data/test/cuda/elementwise_kernel_test.rb +184 -0
- data/test/cuda/function_test.rb +162 -0
- data/test/cuda/module_test.rb +15 -0
- data/test/cuda/name_expression_test.rb +172 -0
- data/test/cuda/pinned_memory_test.rb +138 -0
- data/test/cuda/reduction_kernel_test.rb +189 -0
- data/test/cuda/runtime_test.rb +21 -0
- data/test/cuda/stream_test.rb +246 -0
- data/test/cudnn_test.rb +54 -0
- data/test/cumo_test.rb +47 -0
- data/test/math_test.rb +163 -1
- data/test/narray_alt_coverage_test.rb +117 -1
- data/test/narray_test.rb +577 -0
- metadata +22 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78444d61470bd2cf534dfadba1facef1d92785cf85db498ca14d893242532591
|
|
4
|
+
data.tar.gz: 7bb662f78bd6e130c5a93db7fe6ac44fc2dad14f58f3788de48c04f7830197b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 46d001da3c8c82e4f835fe6643cec1a1f5a4b28864283286a52f98887d975673914ee475ecd90d97042e5c16e45c525060d595ecd8301a427fed5060a8efc7b0
|
|
7
|
+
data.tar.gz: ae0052bea91487469a2dd6052c08f9bb8c74799447ea165d189babc88ccd94b7c686e50d1ff69f1df75ec2d20986c7113ff2d3b949205c4ac72132f7b8d468af
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,54 @@
|
|
|
1
|
+
# 0.10.0 (2026/09/22)
|
|
2
|
+
|
|
3
|
+
Breaking changes:
|
|
4
|
+
|
|
5
|
+
* `CUMO_COMPATIBLE_MODE`, `CUMO_SHOW_WARNING`, `CUMO_SHOW_WARNING_ONCE` and `CUMO_MEMORY_POOL` read `off`, `false` and `no` as a no, in any case, where anything but `OFF`, `0` and `NO` used to turn the flag on without a word. A value they do not recognise keeps the flag's default and warns, and an empty value reads as unset (PR #514)
|
|
6
|
+
* An empty array keeps its shape through everything that goes through ndloop: `Cumo::Int32.new(0, 3).copy` and `(a + 1)` answer `[0, 3]` where they answered `[0, 0]`. `to_a` nests down to the first zero and no further, the way numpy's `tolist` does, so `(3, 0)` is `[[], [], []]` and `(0, 3, 4)` is `[]` (PR #513)
|
|
7
|
+
* `Marshal.dump` of a subclass of `Cumo::RObject` writes its objects, where it wrote the raw addresses the buffer holds. Data the old dump wrote for such a subclass no longer loads, and what it held was addresses from a process that has since exited (PR #509)
|
|
8
|
+
|
|
9
|
+
Fixes:
|
|
10
|
+
|
|
11
|
+
* Fix `all?(axis: [])` answering a zero-dimensional `Cumo::Bit`, which is truthy whatever it holds, for an array it is false for. Whether an axis was named is now read off the reduction rather than off the list (PR #512)
|
|
12
|
+
* Fix `all?`, `any?` and `none?` answering a zero-dimensional `Cumo::Bit` when a default keyword such as `axis: nil` or `keepdims: false` is written out, so `Cumo::Bit[1, 1, 0].all?(axis: nil)` read as true (PR #510)
|
|
13
|
+
* Fix an axis reduction of an empty `Cumo::Bit` answering an empty array: `Cumo::Bit.new(0, 3).all?(axis: 0)` answers `[1, 1, 1]` and `count_true(axis: 0)` answers `[0, 0, 0]` (PR #508)
|
|
14
|
+
* Fix the five `Cumo::Bit` reductions answering false, or an Integer, for an empty array before reading their arguments. `Cumo::Bit[].all?` is true and `any?` false, as Ruby's are, `count_true` answers a `UInt64`, and an axis that is not there raises `DimensionError` (PR #507)
|
|
15
|
+
* Fix `CastError` for a subclass of a dtype, which broke `store`, arithmetic and the reductions, and put an element of one into an RObject array as one object repeated (PR #506)
|
|
16
|
+
* Fix `Device#with` running its block on the device it was already on, whichever device it was given (PR #499)
|
|
17
|
+
* Fix `reshape` writing the new shape over whatever the receiver's `dup` answered, which could leave an array claiming a size its buffer cannot hold. The answer `dup` gives has to be the same class, own its data and not be frozen (PR #494)
|
|
18
|
+
* Fix `gemm` walking a copy of a non-contiguous operand with the original's dimensions when `dup` answered something else (PR #493)
|
|
19
|
+
* Fix `marshal_dump` and `to_binary` handing out memory past the end of the array when a view's `dup` answered another shape, class or layout (PR #492)
|
|
20
|
+
* Fix `median` and `sort` ignoring the axes a view marked with `:sum`, so `f[:sum, true].median` answered the whole array. `median` keeps its receiver rather than standing its copy in its place, and no longer copies an operand it only reads, so the pool holds less afterwards (PR #489, PR #488, PR #487)
|
|
21
|
+
* Fix `Cumo::Bit#copy` answering its own receiver when the receiver is inplace, which let a write to the copy reach the original and gave every reduction that copies an index-backed view first the wrong rows (PR #486)
|
|
22
|
+
* Fix the interpreter abort when `all?` or `any?` reduces a `[:sum, true]` view (PR #485)
|
|
23
|
+
|
|
24
|
+
Changes:
|
|
25
|
+
|
|
26
|
+
* `compile_with_cache` answers the same module for the same source, options and device for the rest of the process, so it can be called wherever the kernel is launched: a repeated call costs 0.0009 ms where it read the cubin back and loaded a new module for 0.033 ms, nine times the launch itself. A module nobody holds any more is unloaded when Ruby collects it, and `Cumo::CUDA::Compiler.clear_modules` makes the next call read the disk cache or compile again (PR #531)
|
|
27
|
+
* The ports of the CuPy examples live in `examples/`, one Ruby file per original, with `backend.rb` running the CPU side on Numo and the GPU side on Cumo under `GPU=1`. The gem leaves them out (PR #530)
|
|
28
|
+
* Drop a dead first definition of `Compiler#save_cache` that `ruby -W` warned about (PR #529)
|
|
29
|
+
* Reading a result back from the device copies the range it needs into pinned host memory rather than reading managed memory in place, which faulted the page over and back once per read where a small block shares its page with others: `Float(r.sum)` takes 0.012 ms where it took 0.885, `to_a` of eight elements 0.016 ms where it took 0.97, and `to_binary` of 8 MB 2.3 ms where it took 7.6. `to_a`, `each`, `each_with_index`, `map_with_index`, `inspect`, `format`, `extract_cpu`, `aref_cpu`, `to_binary` and the RObject marshal all take the copy, and a block that writes the array it is walking still sees what it wrote (PR #528)
|
|
30
|
+
* `rand` and `rand_norm` fill an array whose last axis has length one in one kernel launch rather than one per row: a `[1048576, 1]` column takes 0.018 ms where it took 1799. A seed produces the same values as before whatever the layout (PR #527)
|
|
31
|
+
* A module answers a template kernel such as `kernel<float>` by its name expression: `NVRTCProgram.new` and `compile_with_cache` take `name_expressions:`, the mangled names are cached beside the cubin, and `Module#get_function` looks an expression up before asking the driver. Arrays of structs are arrays whose trailing axes are the fields, and a struct by value is a packed String. The cubin cache had been writing empty files where `/tmp` and the cache directory are different file systems, so every `compile_with_cache` compiled again; it now writes next to its place (PR #526)
|
|
32
|
+
* Add `Cumo::CUDA::PinnedMemory`, a page-locked host buffer read and written as bytes, and `NArray#set` and `NArray#get`, which copy through it asynchronously on the current stream or on `stream:` the way `cupy.ndarray.set` and `get` do. A copy in flight keeps both ends until it is done (PR #525)
|
|
33
|
+
* Add `Cumo::CUDA::Stream` and `Cumo::CUDA::Event`, after `cupy.cuda`: `Stream.new`, `with`, `use`, `synchronize`, `record`, `wait_event`, `Stream.null`, `Stream.current`, `Event.new`, `record`, `synchronize`, `done?` and `Cumo::CUDA.get_elapsed_time`, and `Function#launch` takes `stream:`. `Stream#with` orders the block after what the previous stream had queued and waits for the stream on the way out (PR #524)
|
|
34
|
+
* Every kernel, copy, cuBLAS and cuDNN call, thrust and CUB call goes to the current stream of the thread, which is the null stream until a `Stream` is used, so a stream a caller picks reaches all of it. `Runtime.cudaStreamCreateWithFlags`, `cudaStreamDestroy`, `cudaStreamSynchronize` and `current_stream=` are bound (PR #523)
|
|
35
|
+
* Add `Runtime.cudaDeviceCanAccessPeer` and `Device#can_access_peer?`. A CUDA error raised for a device that is not there no longer stays as the runtime's last error for the next kernel launch to read as its own (PR #521)
|
|
36
|
+
* The CI ccache ceiling is 2 GB, since one build no longer fit in 1 GB and the CUDA 12 jobs compiled again what had been evicted (PR #522)
|
|
37
|
+
* Add `Cumo::CUDA::ReductionKernel`, a reduction written as a map, a reduce and a post expression with an identity, called with `axis:` and `keepdims:`, after CuPy's. This is the last half of #106 (PR #520)
|
|
38
|
+
* Add `Cumo::CUDA::ElementwiseKernel`, a kernel written as one expression over a parameter list, with one-letter types resolved from the arguments and broadcasting, compiled once per set of types, after CuPy's. Bit, half precision and complex arguments are refused for now (PR #519)
|
|
39
|
+
* A kernel compiled with NVRTC can be launched: `Module#get_function` answers a `Cumo::CUDA::Function` whose `launch` takes the arguments, the grid, the block and the dynamic shared memory. An NArray hands over its device pointer, an Integer goes as a `long long`, a Float as a `double`, and a String as its bytes. On CUDA 12.4 or later the count and the sizes of the arguments are checked against the kernel before the launch (PR #518, PR #500)
|
|
40
|
+
* `CUMO_ALLOW_TF32` puts single precision `gemm` on the tensor cores as well as convolutions: a `[4096, 4096]` SFloat gemm goes from 17.8 to 28.4 TFLOP/s, and its answer moves from 4e-07 to 3e-04 of a double precision reference. Off, which is the default, answers bit for bit what it did. The flag is read in `cumo.c` as `Cumo.allow_tf32?` rather than in `cudnn.c`, so a build without cuDNN has it too (PR #517, PR #516)
|
|
41
|
+
* Every dtype's `gemm` goes through `cublasGemmStridedBatchedEx`, the entry point that can name a compute type. The answers are the same bytes, and no change in speed could be told apart from the noise (PR #515)
|
|
42
|
+
* `all?`, `any?` and `none?` on an empty `Cumo::Bit` with no axis named answer the identity without reserving anything or waiting on the device: 0.045 us a call where it took 12.3 and 512 bytes (PR #511)
|
|
43
|
+
* `sort_index`, and `sort` and `median` over a view, address by rank rather than through the run-time accessor: `sort_index` over a 4096 by 1024 SFloat takes 2.2 ms where it took 5.6, and an inplace `sort` over a column slice 2.0 ms where it took 6.5 (PR #505)
|
|
44
|
+
* `cumsum` and `cumprod` along an axis send every row to one scan keyed on its row rather than starting a scan per row, and a view no single stride reaches is gathered by a kernel that addresses by rank: over a `[512, 2048]` SFloat a column slice takes 25.7 us where 0.9.0 took 50.7, and a transposed view 45.2 us where it took 308.3. A reversed view is read through its stride and costs what a flat one does (PR #504, PR #503, PR #498, PR #496)
|
|
45
|
+
* The default cuDNN workspace ceiling is 128 MB rather than 8 MB, which reaches the faster convolution algorithms: the twenty convolutions of a ResNet-18 forward pass at batch 16 take 5.23 ms where they took 9.80, and a half precision convolution reaches its tensor core algorithms. The search reserves the whole ceiling while it runs, so the peak grows with it and goes back to the pool afterwards (PR #502)
|
|
46
|
+
* A single precision convolution stays off the tensor cores unless `CUMO_ALLOW_TF32` says yes, where cuDNN's default let the algorithm search move it there and round the operands to a 10 bit significand (PR #501)
|
|
47
|
+
* Say in the README that `contiguous?` answers true for a view whose length-one axis Numo counts as breaking contiguity, that copying a transposed operand beats handing it to cuBLAS as it is for most shapes, what the memory numbers of the pool and of `nvidia-smi` mean, where a bfloat16 integer index goes past 256, and that `quantize_symmetric` breaks ties away from zero where numpy and torch round to even (PR #497, PR #495, PR #491, PR #484)
|
|
48
|
+
* Say what routing `silu` through `sigmoid` would change, rather than a fraction that no dtype matches (PR #483)
|
|
49
|
+
* Add `Cumo::NMath.sigmoid`, which keeps a value where `1 / (1 + exp(-x))` written out gives out: single reaches zero at -104 rather than -89, double at -745 rather than -710 (PR #482)
|
|
50
|
+
* Add `Cumo::NMath.softplus`, which does not overflow where `log(1 + exp(x))` written out does, and answers `x` to the last bit past 11.09 in half and 88.7 in single (PR #481)
|
|
51
|
+
|
|
1
52
|
# 0.9.0 (2026/09/19)
|
|
2
53
|
|
|
3
54
|
Breaking changes:
|
data/README.md
CHANGED
|
@@ -121,7 +121,7 @@ a[0] < 1.0 #=> Cumo::Bit#shape=[] holding 0
|
|
|
121
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
122
|
Read the value back to the host before branching on it, or run under `compatible_mode`.
|
|
123
123
|
|
|
124
|
-
Set the `CUMO_COMPATIBLE_MODE` environment variable to `ON` to force Numo NArray compatibility (for worse performance).
|
|
124
|
+
Set the `CUMO_COMPATIBLE_MODE` environment variable to `ON` to force Numo NArray compatibility (for worse performance). Every such flag takes `1`, `on`, `yes` or `true` for a yes and `0`, `off`, `no` or `false` for a no, in any case; anything else keeps the default and warns.
|
|
125
125
|
Running a Numo test suite that way keeps its assertions meaningful.
|
|
126
126
|
|
|
127
127
|
You may enable or disable `compatible_mode` as:
|
|
@@ -149,6 +149,36 @@ Float(a.sum) #=> 7.0 in either mode
|
|
|
149
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
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
151
|
|
|
152
|
+
#### A Length-One Axis Does Not Break Contiguity
|
|
153
|
+
|
|
154
|
+
`contiguous?` answers true for some views Numo calls false.
|
|
155
|
+
An axis of length one is only ever indexed at zero, so whatever stride it carries is multiplied by zero and never moves the pointer.
|
|
156
|
+
Cumo leaves such an axis out of the chain it walks; Numo does not, and calls the view strided because of an axis that cannot stride.
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
a = Cumo::DFloat.new(1, 4).seq
|
|
160
|
+
a[true, 0...2].contiguous? #=> true, where Numo gives false
|
|
161
|
+
a[true, 0...2].to_a #=> [[0.0, 1.0]], the same either way
|
|
162
|
+
|
|
163
|
+
Cumo::DFloat.new(2, 4).seq[true, 0...2].contiguous? #=> false in both
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The elements the view holds are the same in either library.
|
|
167
|
+
What changes is who is willing to read them where they lie:
|
|
168
|
+
|
|
169
|
+
* `reshape!` is accepted on these views, where Numo raises
|
|
170
|
+
* `dot`, `gemm`, `conv` and the rest take them as they are, instead of copying them into a contiguous array first
|
|
171
|
+
|
|
172
|
+
A three-dimensional slice behaves the same way, and so does the transpose of a single row:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
Cumo::DFloat.new(1, 1, 4).seq[true, true, 0...2].contiguous? #=> true
|
|
176
|
+
Cumo::DFloat.new(1, 3).seq.transpose.contiguous? #=> true
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Code that runs against both libraries should not read `contiguous?` and expect the same answer.
|
|
180
|
+
Where it wants a contiguous array it can ask for one, since `dup` answers one in either library.
|
|
181
|
+
|
|
152
182
|
### Keeping Scalars On The Device
|
|
153
183
|
|
|
154
184
|
The 0-dimensional return is what lets an iterative loop stay on the GPU.
|
|
@@ -178,6 +208,10 @@ x += p_dir * alpha # and consumed there, without crossing the bus
|
|
|
178
208
|
|
|
179
209
|
Read the value back once the loop is done, or every k iterations if it has to test something.
|
|
180
210
|
|
|
211
|
+
A read costs the wait and a copy of the block it needs into pinned host memory, a few microseconds for a scalar.
|
|
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 cost 0.8 ms that way on the machine above, forty times what the copy costs.
|
|
213
|
+
The reads that answer values, from `Float(x)` and `to_a` to `each` and `inspect`, take the copy.
|
|
214
|
+
|
|
181
215
|
### Reshape Copies, Reshape! Does Not
|
|
182
216
|
|
|
183
217
|
`reshape` answers a copy of the whole array, never a view.
|
|
@@ -260,6 +294,39 @@ Cumo::SFloat::Math.atan2(a, 2.0) #=> Cumo::SFloat
|
|
|
260
294
|
|
|
261
295
|
The 0-dimensional form has no effect under Numo, where `[]` returns a Ruby Float.
|
|
262
296
|
|
|
297
|
+
### A Transposed Operand Goes To cuBLAS As It Is
|
|
298
|
+
|
|
299
|
+
`dot` and `gemm` hand a transposed operand to cuBLAS with its transpose flag set, rather than copying it into the layout cuBLAS reads fastest.
|
|
300
|
+
That saves the copy and the memory it needs.
|
|
301
|
+
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
|
+
|
|
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
|
+
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: it takes twice as long that way.
|
|
318
|
+
Past an `M * K` of roughly fifty thousand on this card the copy starts paying for itself, and past a few hundred thousand it is worth a fifth of the time.
|
|
319
|
+
|
|
320
|
+
Where a profile says one of these multiplications matters, hand it an operand that is already contiguous:
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
kt = k.transpose.dup # or build k transposed in the first place
|
|
324
|
+
q.dot(kt)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
The table above is two-dimensional, where cuBLAS is given one matrix.
|
|
328
|
+
A batched multiplication takes another path through the same flag, and these numbers do not cover it.
|
|
329
|
+
|
|
263
330
|
### Fused Operations
|
|
264
331
|
|
|
265
332
|
`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.
|
|
@@ -274,6 +341,16 @@ xq, scale = x.quantize_symmetric # scale = max(|x|) / 127, xq = round(
|
|
|
274
341
|
|
|
275
342
|
`quantize_symmetric` answers a `Cumo::Int8` shaped like self and the scale of every row, which is self's shape without its last axis. `xq * scale[false, :new]` is what the row stood for. A row of zeros has no scale to divide by and answers zero for both, and a row holding an infinity or a NaN answers that in its scale and zeros in the row, since neither is a value 8 bits could carry. The scale comes back in the class the reduction accumulates in, which is `Cumo::SFloat` for `Cumo::HFloat` and `Cumo::BFloat` and self's own otherwise.
|
|
276
343
|
|
|
344
|
+
The rounding takes a tie away from zero, which is the rule `round` follows here and the rule Ruby's `Float#round` follows. numpy and torch take a tie to the nearest even value instead, so code ported from either answers differently wherever the quotient lands exactly halfway.
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
x / scale -2.5 -1.5 -0.5 0.5 1.5 2.5
|
|
348
|
+
quantize_symmetric -3 -2 -1 1 2 3
|
|
349
|
+
numpy, torch -2 -2 0 0 2 2
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`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
|
+
|
|
277
354
|
On an RTX 5070 Ti Laptop, against the same arithmetic spelled with operators, in microseconds:
|
|
278
355
|
|
|
279
356
|
```
|
|
@@ -363,7 +440,7 @@ runs out of significant digits and answers zero, and `-Float::INFINITY` answers
|
|
|
363
440
|
|
|
364
441
|
### SiLU
|
|
365
442
|
|
|
366
|
-
`Cumo::NMath.silu` is `x
|
|
443
|
+
`Cumo::NMath.silu` is `x / (1 + exp(-x))`, the activation Llama and the models after it use where GPT-2 uses `gelu_tanh`. It is the curve `x * sigmoid(x)` names, written as one division, and the two spellings do not answer alike: see Sigmoid below.
|
|
367
444
|
It is also called Swish.
|
|
368
445
|
|
|
369
446
|
```ruby
|
|
@@ -389,6 +466,86 @@ Where that zero starts depends on the type, and not on `exp` alone.
|
|
|
389
466
|
Single and bfloat16 reach it at -89, where `exp(-x)` passes what a float holds, and double never does.
|
|
390
467
|
Half reaches it at -21, because the round back to half gets there first.
|
|
391
468
|
|
|
469
|
+
### Sigmoid
|
|
470
|
+
|
|
471
|
+
`Cumo::NMath.sigmoid` is `1 / (1 + exp(-x))`, the logistic curve.
|
|
472
|
+
|
|
473
|
+
```ruby
|
|
474
|
+
Cumo::NMath.sigmoid(x) # in one kernel rather than four
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Written out of the operators it costs four launches, and one kernel runs 1.2x to 4.8x faster on an RTX 5070 Ti Laptop, in microseconds:
|
|
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
|
+
```
|
|
486
|
+
|
|
487
|
+
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
|
+
|
|
489
|
+
```ruby
|
|
490
|
+
Cumo::NMath.sigmoid(Cumo::SFloat[-100.0]).to_a.first #=> 3.783506e-44
|
|
491
|
+
(1.0 / (1.0 + Cumo::NMath.exp(Cumo::SFloat[100.0]))).to_a.first #=> 0.0
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Where each type reaches zero, written the one way and the other:
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
sigmoid written out
|
|
498
|
+
Cumo::HFloat -17.5 -11.5
|
|
499
|
+
Cumo::BFloat -93.0 -89.0
|
|
500
|
+
Cumo::SFloat -104.0 -89.0
|
|
501
|
+
Cumo::DFloat -745.0 -710.0
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
A non-negative `x` takes the plain quotient unchanged, bit for bit, so only the negative half moves. It moves toward the true value more often than away, but not by much and not always: over the 360,000 single points between -88 and 0 the guarded spelling is closer at 90,423, further at 72,593 and the same at the rest, and both spellings pass one unit in the last place, the guarded one at 2,289 points and the plain one at 4,489, neither worse than three.
|
|
505
|
+
|
|
506
|
+
`silu` is `x / (1 + exp(-x))` rather than `x * sigmoid(x)`, one division rather than a division and a multiply. The second spelling rounds once more and moves a third of the answers, and it moves them in kind at the bottom: `silu` answers the signed zero torch answers below -89, where `x * sigmoid(x)` still carries a number. The two are written apart for that reason.
|
|
507
|
+
|
|
508
|
+
### Softplus
|
|
509
|
+
|
|
510
|
+
`Cumo::NMath.softplus` is `log(1 + exp(x))`, the smooth positive part, and the function a selective state space model puts its step size through.
|
|
511
|
+
|
|
512
|
+
```ruby
|
|
513
|
+
Cumo::NMath.softplus(x) # log(1 + exp(x)), in one kernel rather than three
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Written out of the operators it costs three launches, and one kernel runs 1.0x to 3.1x faster on an RTX 5070 Ti Laptop, in microseconds:
|
|
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.
|
|
527
|
+
|
|
528
|
+
Writing it out also gives out earlier than the kernel does, because the intermediate is an array of the receiver's type:
|
|
529
|
+
|
|
530
|
+
```ruby
|
|
531
|
+
x = Cumo::HFloat[22.26]
|
|
532
|
+
Cumo::NMath.log(1.0 + Cumo::NMath.exp(x)) #=> Infinity
|
|
533
|
+
Cumo::NMath.softplus(x) #=> 22.265625
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Half holds `exp(x)` only to 11.09 and single to 88.7, where softplus is `x` to the last bit. The kernel takes the single-precision `exp` whatever the type, and hands back `x` where even that has no value to give, so nothing overflows at either width.
|
|
537
|
+
|
|
538
|
+
It is not `log1p(exp(x))`, which is a different number: the sum is taken before the logarithm, as the definition reads and as the implementations this follows compute. `torch.nn.functional.softplus` takes the other spelling and switches to `x` above 20, so the two differ by about an ulp where both are finite.
|
|
539
|
+
|
|
540
|
+
Taking the sum first costs the other end. Once `exp(x)` falls under the type's epsilon the sum drops it, so softplus reaches zero while the true value is still a number:
|
|
541
|
+
|
|
542
|
+
```ruby
|
|
543
|
+
Cumo::NMath.softplus(Cumo::SFloat[-17.0]) #=> 0.0, where the value is 4.1e-08
|
|
544
|
+
Cumo::NMath.softplus(Cumo::DFloat[-37.0]) #=> 0.0, where the value is 8.5e-17
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Single and both sixteen-bit types reach that zero at -17 and double at -37, and the error grows before it: 5.9% at -16 in single and 4.3% at -36 in double. `log1p` is what to reach for where a small negative `x` has to keep its digits.
|
|
548
|
+
|
|
392
549
|
### Half Precision
|
|
393
550
|
|
|
394
551
|
`Cumo::HFloat`, also reachable as `Cumo::Float16`, holds IEEE binary16: one sign bit, five of exponent and ten of mantissa.
|
|
@@ -472,16 +629,19 @@ A 1x768 by 768x50257 gemv takes 0.211 ms with that odd 50257 and 0.205 ms with 5
|
|
|
472
629
|
Pad the inner dimensions of a real matrix product; leave a gemv alone.
|
|
473
630
|
|
|
474
631
|
`conv` is a different story, and worth reading before reaching for half in a network.
|
|
475
|
-
cuDNN chooses its algorithm from the ones that fit in a scratch buffer, and the half algorithms that use the tensor cores ask for
|
|
476
|
-
|
|
632
|
+
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.
|
|
477
634
|
N=32, C=K=64, 56x56, 3x3:
|
|
478
635
|
|
|
479
636
|
```
|
|
480
|
-
|
|
481
|
-
CUMO_CUDNN_MAX_WORKSPACE_SIZE
|
|
482
|
-
|
|
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
|
|
483
641
|
```
|
|
484
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`.
|
|
644
|
+
|
|
485
645
|
Tensor cores also want the channel counts to be multiples of eight, which the first layer of a network never satisfies.
|
|
486
646
|
That layer is still faster in half, but for the other reason:
|
|
487
647
|
|
|
@@ -535,7 +695,7 @@ The two do not contain each other, so an expression mixing them promotes to `Cum
|
|
|
535
695
|
|
|
536
696
|
Everything else promotes as `Cumo::SFloat` does, so an integer array or a Ruby Float mixed in stays bfloat16 while anything wider takes over.
|
|
537
697
|
|
|
538
|
-
`layer_norm`, `rms_norm`, `softmax`, `silu` and both spellings of `gelu` take it, and so do the reductions, `sort`, `median`, `cumsum`, `rand` and `dot`.
|
|
698
|
+
`layer_norm`, `rms_norm`, `softmax`, `silu`, `softplus`, `sigmoid` and both spellings of `gelu` take it, and so do the reductions, `sort`, `median`, `cumsum`, `rand` and `dot`.
|
|
539
699
|
The cuDNN methods take it too: `conv`, `conv_transpose`, `conv_grad_w`, `max_pool`, `avg_pool` and the three batch norm entries.
|
|
540
700
|
|
|
541
701
|
Reductions accumulate in single precision and round once at the end, exactly as they do for half, so a sum passes 256 without stopping there:
|
|
@@ -584,9 +744,169 @@ A `dot` on a pre-Ampere card is the one to expect trouble from.
|
|
|
584
744
|
cuDNN reaches bfloat16 as `CUDNN_DATA_BFLOAT16`, and its own bfloat16 kernels want Ampere for the same reason cuBLAS does.
|
|
585
745
|
A convolution is given `CUDNN_DATA_FLOAT` to accumulate in, so it passes 256 the way a reduction does.
|
|
586
746
|
The batch norm parameters are `Cumo::SFloat`, the same as they are for `Cumo::HFloat`.
|
|
587
|
-
The workspace ceiling matters here as much as it does for half
|
|
747
|
+
The workspace ceiling matters here as much as it does for half, and for the same reason.
|
|
588
748
|
See [Raise the cuDNN workspace ceiling](#raise-the-cudnn-workspace-ceiling).
|
|
589
749
|
|
|
750
|
+
#### An index rounds here rather than saturating
|
|
751
|
+
|
|
752
|
+
[Half Precision](#half-precision) says an index has to be built in a wider type and cast afterwards. That holds here at 256 rather than 2048, and it goes wrong more quietly:
|
|
753
|
+
|
|
754
|
+
```ruby
|
|
755
|
+
Cumo::BFloat[79800] #=> 79872.0
|
|
756
|
+
Cumo::HFloat[79800] #=> Infinity
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
A binary16 hands back an infinity, which the next operation carries somewhere visible. A bfloat16 hands back a plausible integer, and whatever reads it goes on. Where the index is an angle, the answer comes back with the wrong sign:
|
|
760
|
+
|
|
761
|
+
```ruby
|
|
762
|
+
Cumo::NMath.cos(Cumo::SFloat[79800]) #=> -0.9190999865531921
|
|
763
|
+
Cumo::NMath.cos(Cumo::SFloat[79872]) #=> 0.989012598991394
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
Not every integer past 256 is lost, which is what makes this one hard to catch by sampling: `Cumo::BFloat[1000]` is exact, since 1000 is a multiple of 8.
|
|
767
|
+
What ends at 256 is that consecutive integers stay distinct.
|
|
768
|
+
|
|
769
|
+
### Launching Your Own Kernel
|
|
770
|
+
|
|
771
|
+
A kernel written in CUDA C can be compiled with NVRTC and launched on the arrays.
|
|
772
|
+
It runs on the stream Cumo's own kernels use, so it sees the results of the operations issued before it and the operations after it see its.
|
|
773
|
+
|
|
774
|
+
```ruby
|
|
775
|
+
source = <<~CUDA
|
|
776
|
+
extern "C" __global__ void axpy(float* y, const float* x, float a, int n) {
|
|
777
|
+
for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < n; i += gridDim.x * blockDim.x) {
|
|
778
|
+
y[i] = a * x[i] + y[i];
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
CUDA
|
|
782
|
+
mod = Cumo::CUDA::Compiler.new.compile_with_cache(source)
|
|
783
|
+
axpy = mod.get_function("axpy")
|
|
784
|
+
|
|
785
|
+
x = Cumo::SFloat.new(1000).seq
|
|
786
|
+
y = Cumo::SFloat.ones(1000)
|
|
787
|
+
axpy.launch([y, x, [2.5].pack("f"), [1000].pack("l")], grid: 4, block: 256)
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
`compile_with_cache` compiles once and keeps the cubin on disk, and within a process the same source and options answer the same module, so it can be called wherever the kernel is launched.
|
|
791
|
+
That module is shared by everything in the process that compiles the source, so unload it only when nothing else uses it; a module nobody holds any more is unloaded when Ruby collects it.
|
|
792
|
+
`Cumo::CUDA::Compiler.clear_modules` makes the next call read the disk cache or compile again.
|
|
793
|
+
An NArray argument hands over its device pointer, so it has to be contiguous.
|
|
794
|
+
An Integer is passed as a `long long` and a Float as a `double`.
|
|
795
|
+
Anything narrower, and a struct passed by value, goes as the packed bytes of a String: `[n].pack("l")` is an `int` and `[x].pack("f")` a `float`.
|
|
796
|
+
`grid` and `block` take one to three sizes each, and `shared_mem:` is the dynamic shared memory in bytes.
|
|
797
|
+
The kernel is asked for by the name in the source, so it is declared inside `extern "C"`, or asked for by its mangled name.
|
|
798
|
+
On CUDA 12.4 or later the count and the size of the arguments are checked against the kernel before the launch, so an Integer handed to an `int` is refused rather than read wrong.
|
|
799
|
+
A kernel may write any NArray it is handed, so a frozen one is refused, and one that has not been allocated yet is allocated on the way and holds whatever was there.
|
|
800
|
+
|
|
801
|
+
### Streams And Events
|
|
802
|
+
|
|
803
|
+
Every kernel and copy Cumo issues goes to the current stream of the thread, which is the null stream until a `Cumo::CUDA::Stream` is used.
|
|
804
|
+
`Stream#with` runs a block on a stream of its own, waits for everything the block queued, and puts the previous stream back, so what the block produced is complete when it returns.
|
|
805
|
+
The block's work is ordered after what the previous stream had queued, so an input still being computed when the block starts is read complete.
|
|
806
|
+
|
|
807
|
+
```ruby
|
|
808
|
+
s = Cumo::CUDA::Stream.new(non_blocking: true)
|
|
809
|
+
c = s.with { a.gemm(b) } # queued on s, and finished when with returns
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
`Stream#use` makes a stream current without a block, `Stream.current` answers the current one and `Stream.null` the null stream.
|
|
813
|
+
`Stream#record` records an `Event` after everything queued so far, and `Stream#wait_event` makes what is queued after it wait for one, which is how two streams are ordered against each other.
|
|
814
|
+
`Event#synchronize` waits for an event on the host, and `Cumo::CUDA.get_elapsed_time(start, stop)` answers the milliseconds between two recorded events, which is how a kernel is timed without a device-wide wait.
|
|
815
|
+
|
|
816
|
+
```ruby
|
|
817
|
+
start = Cumo::CUDA::Event.new.record
|
|
818
|
+
c = a.gemm(b)
|
|
819
|
+
stop = Cumo::CUDA::Event.new.record
|
|
820
|
+
stop.synchronize
|
|
821
|
+
Cumo::CUDA.get_elapsed_time(start, stop) # => milliseconds
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
Under a stream of the caller's, a host read such as `to_a` or `each` waits for the whole device rather than for that stream, since what it reads may have been written on another one.
|
|
825
|
+
The current stream is per thread, and fibers of one thread share it.
|
|
826
|
+
A `Function#launch` takes `stream:` to launch on a stream other than the current one.
|
|
827
|
+
|
|
828
|
+
### Pinned Host Memory
|
|
829
|
+
|
|
830
|
+
A copy between the host and the device is asynchronous only when the host side is page-locked.
|
|
831
|
+
`Cumo::CUDA::PinnedMemory` allocates such a buffer, and `NArray#set` and `NArray#get` copy through it on the current stream, or on `stream:`.
|
|
832
|
+
|
|
833
|
+
```ruby
|
|
834
|
+
pinned = Cumo::CUDA::PinnedMemory.new(a.byte_size)
|
|
835
|
+
s = Cumo::CUDA::Stream.new(non_blocking: true)
|
|
836
|
+
s.with { a.get(pinned) } # queued on s, complete when with returns
|
|
837
|
+
b = Cumo::SFloat.from_binary(pinned.read, a.shape)
|
|
838
|
+
|
|
839
|
+
src = Cumo::CUDA::PinnedMemory.from_binary(bytes)
|
|
840
|
+
c.set(src, stream: s) # the host bytes reach c once s gets there
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
`set` with a String and `get` with nothing to copy into are the synchronous `store_binary` and `to_binary`.
|
|
844
|
+
A pinned buffer is read and written as bytes with `read` and `write`, and the array has to be contiguous and of the buffer's size.
|
|
845
|
+
A copy in flight keeps its array alive and is waited for by `read`, `write`, `free` and the next copy, so the bytes read are the copy's.
|
|
846
|
+
Allocating a pinned buffer synchronizes the device, so a buffer is allocated once and reused rather than made for every transfer.
|
|
847
|
+
|
|
848
|
+
### Structs And Template Kernels
|
|
849
|
+
|
|
850
|
+
An array of structs is an array whose trailing axes are the struct's fields, so a kernel that takes `const double3*` is handed a `Cumo::DFloat` of shape `[n, 3]`, and one that takes `const Matrix<float>*` an `SFloat` of shape `[n, 4, 4]`.
|
|
851
|
+
`SComplex` and `DComplex` are `float2` and `double2` to a kernel; `Bit`, which packs its elements, and `RObject` cannot be handed to one.
|
|
852
|
+
A struct passed by value is the packed bytes of its fields, and where the struct has padding, `pack`'s `@` places each field at the offset the device reports.
|
|
853
|
+
|
|
854
|
+
```ruby
|
|
855
|
+
rhs = [x, y, z].pack("d3") # a double3 by value
|
|
856
|
+
sum_kernel.launch([lhs, rhs, out], grid: 1, block: n)
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
A template kernel has no `extern "C"` name.
|
|
860
|
+
`compile_with_cache` takes `name_expressions:`, and the module then answers `get_function` for each expression by the mangled name NVRTC reports.
|
|
861
|
+
|
|
862
|
+
```ruby
|
|
863
|
+
mod = Cumo::CUDA::Compiler.new.compile_with_cache(source, name_expressions: ["kernel<float>", "kernel<double>"])
|
|
864
|
+
mod.get_function("kernel<float>").launch([a, b, c.to_a.flatten.pack("f*"), out], grid: 1, block: n)
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### Writing An Elementwise Kernel
|
|
868
|
+
|
|
869
|
+
`Cumo::CUDA::ElementwiseKernel` takes one piece of CUDA C and applies it to every element, the way CuPy's `ElementwiseKernel` does.
|
|
870
|
+
The kernel is compiled once for each set of dtypes, once more when a different argument is a number or is broadcast, and kept for the next call.
|
|
871
|
+
|
|
872
|
+
```ruby
|
|
873
|
+
squared_diff = Cumo::CUDA::ElementwiseKernel.new(
|
|
874
|
+
"T x, T y", "T z", "z = (x - y) * (x - y)", "squared_diff")
|
|
875
|
+
|
|
876
|
+
x = Cumo::SFloat.new(2, 5).seq
|
|
877
|
+
y = Cumo::SFloat.new(5).seq
|
|
878
|
+
squared_diff.call(x, y) # => the (2, 5) array of squared differences
|
|
879
|
+
squared_diff.call(x, 5) # => the same against a scalar
|
|
880
|
+
```
|
|
881
|
+
|
|
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.
|
|
883
|
+
Outputs decide a letter before inputs do, and a letter that only a Ruby number reaches becomes `int64` or `float64`.
|
|
884
|
+
Array arguments are broadcast against each other, and an output may be given after the inputs, or is allocated.
|
|
885
|
+
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
|
+
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
|
+
`preamble:` is placed before the kernel, after the typedefs of the letters, so a device function can be written in terms of `T`.
|
|
888
|
+
`Cumo::Bit`, `Cumo::HFloat`, `Cumo::BFloat`, `Cumo::SComplex`, `Cumo::DComplex` and `Cumo::RObject` cannot be handed to one of these kernels yet.
|
|
889
|
+
|
|
890
|
+
### Writing A Reduction Kernel
|
|
891
|
+
|
|
892
|
+
`Cumo::CUDA::ReductionKernel` reduces along axes with three expressions, the way CuPy's `ReductionKernel` does: a map applied to every element, a reduce between two mapped values `a` and `b`, and a post map that writes the reduced value `a` to the output.
|
|
893
|
+
The identity starts every reduction.
|
|
894
|
+
|
|
895
|
+
```ruby
|
|
896
|
+
l2norm = Cumo::CUDA::ReductionKernel.new(
|
|
897
|
+
"T x", "T y", "x * x", "a + b", "y = sqrt(a)", "0", "l2norm")
|
|
898
|
+
|
|
899
|
+
x = Cumo::SFloat.new(2, 5).seq
|
|
900
|
+
l2norm.call(x, axis: 1) # => [5.477, 15.969]
|
|
901
|
+
l2norm.call(x) # => the 0-dimensional norm of everything
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
The parameters, the types and the broadcasting follow `ElementwiseKernel`, and `axis:` and `keepdims:` follow `sum`.
|
|
905
|
+
`reduce_type:` names the type the values are accumulated in, as a C type, a type name or one of the letters, and is the output's type unless said otherwise.
|
|
906
|
+
The post map may be several statements, so a kernel can write several outputs.
|
|
907
|
+
A `raw` parameter is not taken, since a reduction indexes every argument itself.
|
|
908
|
+
A long axis reduced to a few outputs is split across blocks and folded in a second pass, so it runs as fast as `sum`.
|
|
909
|
+
|
|
590
910
|
### Select a GPU device ID
|
|
591
911
|
|
|
592
912
|
Set the `CUDA_VISIBLE_DEVICES=id` environment variable, or
|
|
@@ -607,6 +927,37 @@ require 'cumo'
|
|
|
607
927
|
Cumo::CUDA::MemoryPool.disable
|
|
608
928
|
```
|
|
609
929
|
|
|
930
|
+
### Reading The Memory Numbers
|
|
931
|
+
|
|
932
|
+
cumo allocates with `cudaMallocManaged`, so a page can sit on the host, and `nvidia-smi` counts only what the card is holding at that moment.
|
|
933
|
+
It reports less than cumo has taken, and the gap is not a fixed one.
|
|
934
|
+
A library that allocates with `cudaMalloc` reports everything it took, so the two numbers do not belong side by side in a comparison.
|
|
935
|
+
|
|
936
|
+
From inside, the pool answers two different questions:
|
|
937
|
+
|
|
938
|
+
```ruby
|
|
939
|
+
Cumo::CUDA::MemoryPool.total_bytes # what the pool has taken from the card
|
|
940
|
+
Cumo::CUDA::MemoryPool.used_bytes # what it has handed out and not taken back
|
|
941
|
+
```
|
|
942
|
+
|
|
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:
|
|
944
|
+
|
|
945
|
+
```ruby
|
|
946
|
+
a = Cumo::SFloat.new(1024, 1024).seq
|
|
947
|
+
t = a + 1
|
|
948
|
+
t = nil
|
|
949
|
+
Cumo::CUDA::MemoryPool.used_bytes #=> 8388608
|
|
950
|
+
GC.start
|
|
951
|
+
Cumo::CUDA::MemoryPool.used_bytes #=> 4194304
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
`total_bytes` does not move with the collector and counts the free blocks the pool is keeping, so it answers how much this took rather than how much is alive.
|
|
955
|
+
|
|
956
|
+
## Examples
|
|
957
|
+
|
|
958
|
+
`examples/` holds ports of the examples in the CuPy repository, one Ruby file per original, with a `backend.rb` that runs the CPU side on Numo and the GPU side on Cumo under `GPU=1`.
|
|
959
|
+
`examples/README.md` lists what is ported and how to run them.
|
|
960
|
+
|
|
610
961
|
## Documentation
|
|
611
962
|
|
|
612
963
|
See https://github.com/ruby-numo/numo-narray#documentation, replacing Numo with Cumo.
|
|
@@ -730,17 +1081,57 @@ export CUMO_SHOW_WARNING_ONCE=OFF
|
|
|
730
1081
|
|
|
731
1082
|
### Raise the cuDNN workspace ceiling
|
|
732
1083
|
|
|
733
|
-
cuDNN picks a convolution algorithm by benchmarking the ones that fit in a scratch buffer, and the ceiling on that buffer is
|
|
734
|
-
The
|
|
735
|
-
|
|
1084
|
+
cuDNN picks a convolution algorithm by benchmarking the ones that fit in a scratch buffer, and the ceiling on that buffer is 128MB.
|
|
1085
|
+
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
|
+
|
|
1087
|
+
Some shapes want more than the default. The twenty convolutions of a ResNet-18 forward pass at batch 16, and the single-precision case from [Half Precision](#half-precision):
|
|
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
|
+
```
|
|
1095
|
+
|
|
1096
|
+
To raise it:
|
|
736
1097
|
|
|
737
1098
|
```
|
|
738
|
-
export CUMO_CUDNN_MAX_WORKSPACE_SIZE=
|
|
1099
|
+
export CUMO_CUDNN_MAX_WORKSPACE_SIZE=268435456
|
|
739
1100
|
```
|
|
740
1101
|
|
|
741
1102
|
The value is in bytes and only bounds the search; each convolution reserves what its chosen algorithm actually needs.
|
|
742
1103
|
`Cumo::CUDA::CUDNN.max_workspace_size` reads back the value in force.
|
|
743
1104
|
|
|
1105
|
+
### Single precision stays off the tensor cores
|
|
1106
|
+
|
|
1107
|
+
cuDNN reads its default math mode as "tensor cores are allowed", so a single precision convolution moves onto them as soon as the algorithm search reaches an algorithm that has them.
|
|
1108
|
+
The operands are rounded to a 10 bit significand on the way, and nothing in the call said to do that.
|
|
1109
|
+
Cumo asks cuDNN to keep single precision off the tensor cores, so raising the workspace ceiling buys speed at single precision accuracy.
|
|
1110
|
+
A different algorithm rounds differently, so the answer still moves; what it does not do is drop two digits.
|
|
1111
|
+
The half types name the tensor cores themselves and are not affected.
|
|
1112
|
+
|
|
1113
|
+
To trade the accuracy for the speed:
|
|
1114
|
+
|
|
1115
|
+
```
|
|
1116
|
+
export CUMO_ALLOW_TF32=1
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
Measured over the convolutions of a ResNet-18 forward pass at batch 16, with the ceiling raised to 256MB, each layer against a double precision reference:
|
|
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
|
+
```
|
|
1126
|
+
|
|
1127
|
+
The same flag puts `SFloat` and `SComplex` `gemm` on the tensor cores as TF32, and `dot` where it goes through `gemm`.
|
|
1128
|
+
Off, the answer is the one cuBLAS gives at single precision, bit for bit.
|
|
1129
|
+
On, a `[4096, 4096]` `SFloat` `gemm` goes from 17.8 to 28.4 TFLOP/s on an RTX 5070 Ti Laptop, 1.60 times over six interleaved rounds.
|
|
1130
|
+
Its answer is then about 3e-04 from a double precision reference, where it was 4e-07.
|
|
1131
|
+
The double types are not affected either way.
|
|
1132
|
+
|
|
1133
|
+
`Cumo.allow_tf32?` reads back the value in force.
|
|
1134
|
+
|
|
744
1135
|
## Contributing
|
|
745
1136
|
|
|
746
1137
|
Bug reports and pull requests are welcome on GitHub at https://github.com/sonots/cumo.
|
data/bench/cumo_probe.rb
CHANGED
|
@@ -77,7 +77,7 @@ DTYPES = (ENV['DTYPES'] == 'all' ? DTYPE_SETS['all'] : (ENV['DTYPES']&.split(','
|
|
|
77
77
|
|
|
78
78
|
LAYOUT_SETS = {
|
|
79
79
|
'default' => %i[contig colslice],
|
|
80
|
-
'all' => %i[contig colslice transpose step2 index]
|
|
80
|
+
'all' => %i[contig colslice transpose step2 index reverse]
|
|
81
81
|
}.freeze
|
|
82
82
|
LAYOUTS = (ENV['LAYOUTS'] == 'all' ? LAYOUT_SETS['all'] : (ENV['LAYOUTS']&.split(',')&.map(&:to_sym) || LAYOUT_SETS['default']))
|
|
83
83
|
|
|
@@ -156,6 +156,9 @@ def operand(klass, rows, cols, layout)
|
|
|
156
156
|
when :transpose then base(klass, cols, rows).transpose
|
|
157
157
|
when :step2 then base(klass, rows, cols * 2)[true, (0...(cols * 2)).step(2)]
|
|
158
158
|
when :index then base(klass, rows, cols)[true, (0...cols).to_a.rotate(7)]
|
|
159
|
+
# walks one stride like contig does, but backwards, which is the shape a
|
|
160
|
+
# kernel that only knows +elmsz hands to a buffer instead
|
|
161
|
+
when :reverse then base(klass, rows, cols).reverse(1)
|
|
159
162
|
else raise ArgumentError, "unknown layout #{layout}"
|
|
160
163
|
end
|
|
161
164
|
end
|
|
@@ -312,7 +315,8 @@ op(:scalar, 'scalar_left') { |c| c.scalar - c.sc.inplace }
|
|
|
312
315
|
|
|
313
316
|
# NMath
|
|
314
317
|
%w[sqrt exp exp2 exp10 expm1 log log2 log10 log1p sin cos tan asin acos atan
|
|
315
|
-
sinh cosh tanh asinh acosh atanh cbrt erf erfc sinc gelu gelu_tanh silu
|
|
318
|
+
sinh cosh tanh asinh acosh atanh cbrt erf erfc sinc gelu gelu_tanh silu
|
|
319
|
+
softplus sigmoid].each do |f|
|
|
316
320
|
op(:math, f, :float) { |c| XM::NMath.send(f, c.usc.inplace) }
|
|
317
321
|
end
|
|
318
322
|
op(:math, 'atan2', :float) { |c| XM::NMath.atan2(c.sc.inplace, c.b) }
|
data/bench/cumo_sync_probe.rb
CHANGED
|
@@ -342,7 +342,7 @@ if ARGV.include?('--list')
|
|
|
342
342
|
exit
|
|
343
343
|
end
|
|
344
344
|
|
|
345
|
-
warn_on = ENV['CUMO_SHOW_WARNING'].to_s.
|
|
345
|
+
warn_on = %w[1 on yes true].include?(ENV['CUMO_SHOW_WARNING'].to_s.downcase)
|
|
346
346
|
|
|
347
347
|
puts "ruby : #{RUBY_VERSION} (#{RUBY_PLATFORM})"
|
|
348
348
|
puts "cumo : #{begin
|
data/cumo.gemspec
CHANGED
|
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
|
|
|
25
25
|
spec.required_ruby_version = ">= 3.0.0"
|
|
26
26
|
|
|
27
27
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
28
|
-
f.match(%r{^(.github|test|spec|features|docker)/})
|
|
28
|
+
f.match(%r{^(.github|test|spec|features|docker|examples)/})
|
|
29
29
|
end
|
|
30
30
|
spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
31
31
|
spec.bindir = "exe"
|