cumo 0.11.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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +10 -0
  3. data/3rd_party/mkmf-cu/lib/mkmf-cu/cli.rb +1 -1
  4. data/CHANGELOG.md +17 -0
  5. data/README.md +36 -0
  6. data/cumo.gemspec +2 -2
  7. data/ext/cumo/cuda/cublas.c +12 -19
  8. data/ext/cumo/cuda/cudnn.c +10 -18
  9. data/ext/cumo/cuda/cusolver.c +1238 -0
  10. data/ext/cumo/cuda/runtime.c +122 -31
  11. data/ext/cumo/cumo.c +2 -0
  12. data/ext/cumo/depend.erb +8 -4
  13. data/ext/cumo/extconf.rb +36 -33
  14. data/ext/cumo/include/cumo/cuda/cusolver.h +35 -0
  15. data/ext/cumo/include/cumo/cuda/runtime.h +11 -0
  16. data/ext/cumo/include/cumo.h +2 -2
  17. data/ext/cumo/narray/array.c +3 -2
  18. data/ext/cumo/narray/data.c +40 -3
  19. data/ext/cumo/narray/data_kernel.cu +39 -0
  20. data/ext/cumo/narray/gen/cogen_kernel.rb +36 -1
  21. data/ext/cumo/narray/gen/cogen_runner.rb +16 -0
  22. data/ext/cumo/narray/gen/kernel_parts.rb +7 -0
  23. data/ext/cumo/narray/gen/tmpl/accum_arg_kernel.cu +6 -2
  24. data/ext/cumo/narray/gen/tmpl/accum_binary_kernel.cu +2 -0
  25. data/ext/cumo/narray/gen/tmpl/accum_index_kernel.cu +6 -2
  26. data/ext/cumo/narray/gen/tmpl/complex_accum_kernel.cu +7 -1
  27. data/ext/cumo/narray/gen/tmpl/cum.c +1 -4
  28. data/ext/cumo/narray/gen/tmpl/float_accum_kernel.cu +7 -1
  29. data/ext/cumo/narray/gen/tmpl/real_accum_kernel.cu +3 -1
  30. data/ext/cumo/narray/gen/tmpl/store_array.c +9 -6
  31. data/ext/cumo/narray/gen/tmpl_bit/store_array.c +3 -2
  32. data/ext/cumo/narray/narray.c +3 -2
  33. data/lib/cumo/linalg.rb +1403 -0
  34. data/lib/cumo/narray/extra.rb +8 -6
  35. data/lib/cumo.rb +2 -0
  36. metadata +8 -75
  37. data/.gitignore +0 -32
  38. data/.rubocop.yml +0 -15
  39. data/.rubocop_todo.yml +0 -1691
  40. data/Dockerfile +0 -34
  41. data/Gemfile +0 -14
  42. data/bench/bench.rb +0 -235
  43. data/bench/broadcast_fp32.rb +0 -140
  44. data/bench/cg_bench.rb +0 -271
  45. data/bench/crossover_bench.rb +0 -374
  46. data/bench/cumo_bench.rb +0 -218
  47. data/bench/cumo_probe.rb +0 -795
  48. data/bench/cumo_shape_probe.rb +0 -264
  49. data/bench/cumo_sync_probe.rb +0 -505
  50. data/bench/ising_bench.rb +0 -242
  51. data/bench/kmeans_bench.rb +0 -254
  52. data/bench/numo_bench.rb +0 -142
  53. data/bench/particle_bench.rb +0 -261
  54. data/bench/reduction_fp32.rb +0 -119
  55. data/bench/transformer_bench.rb +0 -264
  56. data/bin/console +0 -15
  57. data/bin/setup +0 -8
  58. data/docker-build.sh +0 -4
  59. data/docker-launch.sh +0 -4
  60. data/test/bfloat_test.rb +0 -238
  61. data/test/bit_test.rb +0 -1237
  62. data/test/cuda/compiler_test.rb +0 -153
  63. data/test/cuda/device_test.rb +0 -73
  64. data/test/cuda/driver_test.rb +0 -108
  65. data/test/cuda/elementwise_kernel_test.rb +0 -305
  66. data/test/cuda/function_test.rb +0 -162
  67. data/test/cuda/memory_pool_test.rb +0 -179
  68. data/test/cuda/module_test.rb +0 -15
  69. data/test/cuda/name_expression_test.rb +0 -172
  70. data/test/cuda/nvrtc_test.rb +0 -119
  71. data/test/cuda/pinned_memory_test.rb +0 -138
  72. data/test/cuda/reduction_kernel_test.rb +0 -276
  73. data/test/cuda/runtime_test.rb +0 -74
  74. data/test/cuda/stream_test.rb +0 -247
  75. data/test/cudnn_test.rb +0 -1008
  76. data/test/cumo_test.rb +0 -169
  77. data/test/extra_test.rb +0 -1209
  78. data/test/fused_test.rb +0 -608
  79. data/test/hfloat_test.rb +0 -717
  80. data/test/math_test.rb +0 -983
  81. data/test/narray_alt_coverage_test.rb +0 -980
  82. data/test/narray_test.rb +0 -8924
  83. data/test/ractor_test.rb +0 -54
  84. data/test/test_helper.rb +0 -105
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d50aabaf6d47d63e97fc6c68726cac94af2ecb8b737a3441de618860d2f5ab6a
4
- data.tar.gz: 9d7bfc02015586d0ef533802be1d48558abf93fddf362ce445c55b0b4713ab7b
3
+ metadata.gz: 48de893a0d01a5e8376c2a9cda402be28dd47835661f22bd69e7ed17561abed8
4
+ data.tar.gz: c403ffc7d25b80a0b50e8513b81439ee46f775cbd8c126667f83656077488a79
5
5
  SHA512:
6
- metadata.gz: 99f47260f8dbbb9320d2f74984262e328fd42c2dac0321300b6087712dd129d03f80d65a2089f12f624ea2814e59eb8400748c9bf08f3937b09c0bceb452f417
7
- data.tar.gz: a3ccc77b320c0bfdaa64abed339cc22ce5099638f8a52e9abc5a44a1e144295f56581b43ebf99f5a431d54bbe7bed2fe590975abd59cb6b7e1a9008878c91f23
6
+ metadata.gz: '00189a16920241771a70155a98d60c5d53b6fcd23bfcddbaed3094bbbe5d3476b2d6b5ce70f9d8d899e090a9bf3be2c4249c1b98b6d14360736a4a5ba1aa5594'
7
+ data.tar.gz: bd6a1c8b78837b387f93f5b57f13c26917c71c123a1441deff8e3afb7250a42bc799ded9271b9ad9d46d4c3b928660c38653f85427b024bd00797d6786535734
data/.yardopts ADDED
@@ -0,0 +1,10 @@
1
+ --no-private
2
+ --exclude ext/cumo/narray/gen/
3
+ -e ext/cumo/narray/gen/cogen_runner.rb
4
+ lib/**/*.rb
5
+ ext/**/*.c
6
+ ext/**/*.cpp
7
+ tmp/yard/types/*.c
8
+ -
9
+ *.md
10
+ LICENSE.txt
@@ -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,20 @@
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
+
1
18
  # 0.11.0 (2026/09/27)
2
19
 
3
20
  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
 
@@ -282,6 +283,40 @@ q.dot(kt)
282
283
  All of this is about the two-dimensional case, where cuBLAS is given one matrix.
283
284
  A batched multiplication takes another path through the same flag.
284
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.
319
+
285
320
  ### Fused Operations
286
321
 
287
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.
@@ -962,6 +997,7 @@ export CUMO_ALLOW_TF32=1
962
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.
963
998
 
964
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.
965
1001
  Off, the answer is the one cuBLAS gives at single precision, bit for bit.
966
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.
967
1003
  The double types are not affected either way.
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.match(%r{^(.github|test|spec|features|docker|examples)/})
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"]
@@ -43,31 +43,22 @@ cumo_cuda_cublas_check_status(cublasStatus_t status)
43
43
  }
44
44
  }
45
45
 
46
- // Lazily initialize cublas handle, and cache it
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
- // One table per thread: the stream is set on the handle before each call,
51
- // and another thread with another current stream must not reset it in
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(&handles[device]));
58
+ cumo_cuda_cublas_check_status(cublasCreate(handle));
68
59
  }
69
- cumo_cuda_cublas_check_status(cublasSetStream(handles[device], cumo_cuda_stream()));
70
- return handles[device];
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
  */
@@ -72,30 +72,21 @@ cumo_cuda_cudnn_release_conv_held(VALUE held)
72
72
  return Qnil;
73
73
  }
74
74
 
75
- // Lazily initialize cudnn handle, and cache it
75
+ static cumo_cuda_thread_local_t handles;
76
+
77
+ // One handle per thread, as in cublas.c: the stream set on the handle must
78
+ // not be reset by another thread before the call.
76
79
  cudnnHandle_t
77
80
  cumo_cuda_cudnn_handle()
78
81
  {
79
- // One table per thread, as in cublas.c: the stream set on the handle must
80
- // not be reset by another thread before the call. A handle is never destroyed.
81
- static __thread cudnnHandle_t *handles = 0;
82
- int device;
83
- if (handles == 0) {
84
- int i;
85
- int device_count = cumo_cuda_runtime_get_device_count();
86
- handles = ALLOC_N(cudnnHandle_t, device_count);
87
- for (i = 0; i < device_count; ++i) {
88
- handles[i] = 0;
89
- }
90
- }
91
- device = cumo_cuda_runtime_get_device();
92
- if (handles[device] == 0) {
82
+ cudnnHandle_t *handle = (cudnnHandle_t*)cumo_cuda_thread_local_get(&handles);
83
+ if (*handle == 0) {
93
84
  // A discarded status leaves the handle NULL, and cuDNN reports that as
94
85
  // CUDNN_STATUS_NOT_INITIALIZED at the next call instead of the reason.
95
- cumo_cuda_cudnn_check_status(cudnnCreate(&handles[device]));
86
+ cumo_cuda_cudnn_check_status(cudnnCreate(handle));
96
87
  }
97
- cumo_cuda_cudnn_check_status(cudnnSetStream(handles[device], cumo_cuda_stream()));
98
- return handles[device];
88
+ cumo_cuda_cudnn_check_status(cudnnSetStream(*handle, cumo_cuda_stream()));
89
+ return *handle;
99
90
  }
100
91
 
101
92
  #endif // CUDNN_FOUND
@@ -144,6 +135,7 @@ Init_cumo_cuda_cudnn(void)
144
135
 
145
136
  rb_define_singleton_method(mCUDNN, "available?", rb_cudnn_available_p, 0);
146
137
  #ifdef CUDNN_FOUND
138
+ cumo_cuda_thread_local_init(&handles, sizeof(cudnnHandle_t));
147
139
  init_max_workspace_size();
148
140
  rb_define_singleton_method(mCUDNN, "max_workspace_size", rb_cudnn_max_workspace_size, 0);
149
141
  rb_define_const(mCUDNN, "CUDNN_POOLING_MAX", INT2NUM(CUDNN_POOLING_MAX));