Class: Ignis::NvArray

Inherits:
Object
  • Object
show all
Defined in:
lib/nvruby/array.rb

Overview

GPU-aware multi-dimensional array Similar to NumPy ndarray but with CUDA memory backing

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(shape:, dtype: :float32, device: nil, data: nil) ⇒ NvArray

Create a new NvArray

Parameters:

  • shape (Array<Integer>)

    Shape of the array

  • dtype (Symbol) (defaults to: :float32)

    Data type (default: :float32)

  • device (Integer, nil) (defaults to: nil)

    Device index (nil for host-only)

  • data (Array, String, nil) (defaults to: nil)

    Initial data



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/nvruby/array.rb', line 33

def initialize(shape:, dtype: :float32, device: nil, data: nil)
  @shape = normalize_shape(shape)
  @dtype = DType.validate!(dtype)
  @device_index = device || Ignis.configuration.default_device
  @strides = compute_strides
  @size_bytes = compute_size_bytes

  @device_memory = nil
  @host_memory = nil

  if data
    initialize_with_data(data)
  else
    allocate_memory(device ? :device : :host)
  end
end

Instance Attribute Details

#device_indexInteger (readonly)

Returns Device index.

Returns:

  • (Integer)

    Device index



26
27
28
# File 'lib/nvruby/array.rb', line 26

def device_index
  @device_index
end

#device_memoryCUDA::Memory? (readonly)

Returns Device memory (nil if on host).

Returns:

  • (CUDA::Memory, nil)

    Device memory (nil if on host)



17
18
19
# File 'lib/nvruby/array.rb', line 17

def device_memory
  @device_memory
end

#dtypeSymbol (readonly)

Returns Data type (:float32, :float64, :complex64, etc.).

Returns:

  • (Symbol)

    Data type (:float32, :float64, :complex64, etc.)



11
12
13
# File 'lib/nvruby/array.rb', line 11

def dtype
  @dtype
end

#host_memoryFFI::Pointer? (readonly)

Returns Host memory pointer (nil if on device).

Returns:

  • (FFI::Pointer, nil)

    Host memory pointer (nil if on device)



20
21
22
# File 'lib/nvruby/array.rb', line 20

def host_memory
  @host_memory
end

#location:host, :device (readonly)

Returns Current memory location.

Returns:

  • (:host, :device)

    Current memory location



23
24
25
# File 'lib/nvruby/array.rb', line 23

def location
  @location
end

#shapeArray<Integer> (readonly)

Returns Shape of the array.

Returns:

  • (Array<Integer>)

    Shape of the array



8
9
10
# File 'lib/nvruby/array.rb', line 8

def shape
  @shape
end

#stridesArray<Integer> (readonly)

Returns Strides in bytes for each dimension.

Returns:

  • (Array<Integer>)

    Strides in bytes for each dimension



14
15
16
# File 'lib/nvruby/array.rb', line 14

def strides
  @strides
end

Class Method Details

.allocate_empty_metadata(shape, dtype) ⇒ NvArray

Create an empty NvArray without allocating memory

Parameters:

  • shape (Array<Integer>)

    Shape

  • dtype (Symbol)

    Data type

Returns:



361
362
363
364
365
366
367
368
369
# File 'lib/nvruby/array.rb', line 361

def (shape, dtype)
  # We use allocate to avoid calling initialize which tries to allocate memory
  arr = allocate
  arr.instance_variable_set(:@shape, Array(shape).map(&:to_i))
  arr.instance_variable_set(:@dtype, DType.validate!(dtype))
  arr.instance_variable_set(:@strides, arr.send(:compute_strides))
  arr.instance_variable_set(:@size_bytes, arr.send(:compute_size_bytes))
  arr
end

.eye(size, dtype: :float32, device: nil) ⇒ NvArray

Create identity matrix

Parameters:

  • size (Integer)

    Size of the square matrix

  • dtype (Symbol) (defaults to: :float32)

    Data type

  • device (Integer, nil) (defaults to: nil)

    Device index

Returns:



411
412
413
414
# File 'lib/nvruby/array.rb', line 411

def eye(size, dtype: :float32, device: nil)
  data = Array.new(size * size) { |i| i / size == i % size ? 1.0 : 0.0 }
  new(shape: [size, size], dtype: dtype, device: device, data: data)
end

.from_array(data, dtype: :float32, device: nil) ⇒ NvArray

Create array from Ruby array

Parameters:

  • data (Array)

    Nested Ruby array

  • dtype (Symbol) (defaults to: :float32)

    Data type

  • device (Integer, nil) (defaults to: nil)

    Device index

Returns:



400
401
402
403
404
# File 'lib/nvruby/array.rb', line 400

def from_array(data, dtype: :float32, device: nil)
  shape = infer_shape(data)
  flat = flatten_nested(data)
  new(shape: shape, dtype: dtype, device: device, data: flat)
end

.from_device_ptr(ptr, shape:, dtype: :float32, take_ownership: false) ⇒ NvArray

Create array from existing device memory

Parameters:

  • ptr (FFI::Pointer, CUDA::Memory)

    Device pointer or Memory object

  • shape (Array<Integer>)

    Shape

  • dtype (Symbol) (defaults to: :float32)

    Data type

  • take_ownership (Boolean) (defaults to: false)

    If true, the array will manage the memory lifecycle

Returns:



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/nvruby/array.rb', line 332

def from_device_ptr(ptr, shape:, dtype: :float32, take_ownership: false)
  arr = (shape, dtype)
  
  if ptr.is_a?(CUDA::Memory)
    # If it's already a Memory object, we can use it directly
    # If we don't take ownership, we might need a non-owning version
    if take_ownership
      arr.instance_variable_set(:@device_memory, ptr)
    else
      # Create a non-owning wrapper for the same pointer
      wrapper = CUDA::Memory.new(arr.nbytes, device: ptr.device_index, ptr: ptr.device_ptr, owned: false)
      arr.instance_variable_set(:@device_memory, wrapper)
    end
  else
    # It's a raw FFI::Pointer
    # We wrap it in a Memory object
    wrapper = CUDA::Memory.new(arr.nbytes, ptr: ptr, owned: take_ownership)
    arr.instance_variable_set(:@device_memory, wrapper)
  end
  
  arr.instance_variable_set(:@location, :device)
  arr.instance_variable_set(:@device_index, arr.device_memory.device_index)
  arr
end

.linspace(start, stop, num, dtype: :float32, device: nil) ⇒ NvArray

Create array with evenly spaced values

Parameters:

  • start (Numeric)

    Start value

  • stop (Numeric)

    End value

  • num (Integer)

    Number of samples

  • dtype (Symbol) (defaults to: :float32)

    Data type

  • device (Integer, nil) (defaults to: nil)

    Device index

Returns:



320
321
322
323
324
# File 'lib/nvruby/array.rb', line 320

def linspace(start, stop, num, dtype: :float32, device: nil)
  step = (stop - start).to_f / (num - 1)
  data = (0...num).map { |i| start + step * i }
  new(shape: [num], dtype: dtype, device: device, data: data)
end

.ones(shape, dtype: :float32, device: nil) ⇒ NvArray

Create array filled with ones

Parameters:

  • shape (Array<Integer>)

    Shape

  • dtype (Symbol) (defaults to: :float32)

    Data type

  • device (Integer, nil) (defaults to: nil)

    Device index

Returns:



307
308
309
310
311
# File 'lib/nvruby/array.rb', line 307

def ones(shape, dtype: :float32, device: nil)
  size = Array(shape).reduce(1, :*)
  data = Array.new(size, 1.0)
  new(shape: shape, dtype: dtype, device: device, data: data)
end

.zeros(shape, dtype: :float32, device: nil) ⇒ NvArray

Create array filled with zeros

Parameters:

  • shape (Array<Integer>)

    Shape

  • dtype (Symbol) (defaults to: :float32)

    Data type

  • device (Integer, nil) (defaults to: nil)

    Device index

Returns:



296
297
298
299
300
# File 'lib/nvruby/array.rb', line 296

def zeros(shape, dtype: :float32, device: nil)
  arr = new(shape: shape, dtype: dtype, device: device)
  arr.zero!
  arr
end

Instance Method Details

#contiguousNvArray

Create a contiguous copy

Returns:



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/nvruby/array.rb', line 208

def contiguous
  return self if contiguous?

  # Create new array and copy data
  result = NvArray.new(shape: @shape, dtype: @dtype, device: on_device? ? @device_index : nil)

  if on_device?
    # Device-to-device copy
    result.device_memory.copy_from_device(@device_memory)
  else
    # Copy host data
    result.host_memory.put_bytes(0, @host_memory.get_bytes(0, @size_bytes))
  end

  result
end

#contiguous?Boolean

Check if memory layout is contiguous

Returns:

  • (Boolean)


227
228
229
230
231
232
233
234
235
236
# File 'lib/nvruby/array.rb', line 227

def contiguous?
  expected = itemsize
  @strides.reverse.each_with_index do |stride, i|
    dim = @shape[ndim - 1 - i]
    return false unless stride == expected || dim == 1

    expected *= dim
  end
  true
end

#device_ffi_ptrFFI::Pointer

Get device pointer wrapped as an FFI::Pointer for FFI-bound CUDA-X library calls (cuBLAS/cuSOLVER/cuFFT/cuRAND/cuSPARSE), which cannot accept the Fiddle::Pointer returned by #device_ptr.

Returns:

  • (FFI::Pointer)

    Device pointer

Raises:

  • (InvalidOperationError)

    If not on device



136
137
138
139
# File 'lib/nvruby/array.rb', line 136

def device_ffi_ptr
  ensure_device_data!
  @device_memory.ffi_ptr
end

#device_ptrFiddle::Pointer

Get device pointer for CUDA operations

Returns:

  • (Fiddle::Pointer)

    Device pointer

Raises:

  • (InvalidOperationError)

    If not on device



126
127
128
129
# File 'lib/nvruby/array.rb', line 126

def device_ptr
  ensure_device_data!
  @device_memory.device_ptr
end

#dupNvArray

Duplicate the array

Returns:



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/nvruby/array.rb', line 240

def dup
  result = NvArray.new(shape: @shape.dup, dtype: @dtype, device: on_device? ? @device_index : nil)

  if on_device?
    result.instance_variable_set(:@device_memory, CUDA::Memory.new(@size_bytes, device: @device_index))
    result.device_memory.copy_from_device(@device_memory)
    result.instance_variable_set(:@location, :device)
  else
    new_host = FFI::MemoryPointer.new(:uint8, @size_bytes)
    new_host.put_bytes(0, @host_memory.get_bytes(0, @size_bytes))
    result.instance_variable_set(:@host_memory, new_host)
    result.instance_variable_set(:@location, :host)
  end

  result
end

#flattenArray

Get flat data as Ruby array

Returns:

  • (Array)

    1D Ruby array with all elements



161
162
163
164
165
# File 'lib/nvruby/array.rb', line 161

def flatten
  synchronize_if_needed
  ensure_host_data!
  read_flat_data
end

#free!void

This method returns an undefined value.

Free all memory



271
272
273
274
275
276
# File 'lib/nvruby/array.rb', line 271

def free!
  @device_memory&.free!
  @device_memory = nil
  @host_memory = nil
  @location = nil
end

#host_ptrFFI::Pointer

Get host pointer

Returns:

  • (FFI::Pointer)

    Host pointer

Raises:

  • (InvalidOperationError)

    If not on host



144
145
146
147
# File 'lib/nvruby/array.rb', line 144

def host_ptr
  ensure_host_data!
  @host_memory
end

#inspectString

Returns Detailed inspection.

Returns:

  • (String)

    Detailed inspection



285
286
287
288
# File 'lib/nvruby/array.rb', line 285

def inspect
  "#<Ignis::NvArray:#{object_id} shape=#{@shape} dtype=#{@dtype} " \
    "location=#{@location} device=#{@device_index} bytes=#{@size_bytes}>"
end

#itemsizeInteger

Returns Size of each element in bytes.

Returns:

  • (Integer)

    Size of each element in bytes



66
67
68
# File 'lib/nvruby/array.rb', line 66

def itemsize
  DType.byte_size(@dtype)
end

#nbytesInteger

Returns Total size in bytes.

Returns:

  • (Integer)

    Total size in bytes



61
62
63
# File 'lib/nvruby/array.rb', line 61

def nbytes
  @size_bytes
end

#ndimInteger

Returns Number of dimensions.

Returns:

  • (Integer)

    Number of dimensions



56
57
58
# File 'lib/nvruby/array.rb', line 56

def ndim
  @shape.size
end

#on_device?Boolean

Check if data is on device

Returns:

  • (Boolean)


72
73
74
# File 'lib/nvruby/array.rb', line 72

def on_device?
  @location == :device
end

#on_host?Boolean

Check if data is on host

Returns:

  • (Boolean)


78
79
80
# File 'lib/nvruby/array.rb', line 78

def on_host?
  @location == :host
end

#reshape(new_shape) ⇒ NvArray

Reshape the array

Parameters:

  • new_shape (Array<Integer>)

    New shape

Returns:

  • (NvArray)

    Reshaped array (view if contiguous)

Raises:

  • (DimensionError)


170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/nvruby/array.rb', line 170

def reshape(new_shape)
  new_shape = normalize_shape(new_shape)

  # Handle -1 in shape
  if new_shape.include?(-1)
    neg_idx = new_shape.index(-1)
    other_size = new_shape.reject { |d| d == -1 }.reduce(1, :*)
    new_shape[neg_idx] = size / other_size
  end

  raise DimensionError, "Cannot reshape array of size #{size} to #{new_shape}" unless new_shape.reduce(1, :*) == size

  # Create new array with same memory
  result = dup
  result.instance_variable_set(:@shape, new_shape)
  result.instance_variable_set(:@strides, result.send(:compute_strides))
  result
end

#sizeInteger

Returns Total number of elements.

Returns:

  • (Integer)

    Total number of elements



51
52
53
# File 'lib/nvruby/array.rb', line 51

def size
  @shape.reduce(1, :*)
end

#to_aArray

Get data as Ruby array (copies to host if needed)

Returns:

  • (Array)

    Nested Ruby array with data



151
152
153
154
155
156
157
# File 'lib/nvruby/array.rb', line 151

def to_a
  synchronize_if_needed
  ensure_host_data!

  flat_data = read_flat_data
  reshape_to_nested(flat_data, @shape)
end

#to_device(device: nil, stream: nil) ⇒ self

Transfer data to GPU

Parameters:

  • device (Integer, nil) (defaults to: nil)

    Target device (nil for current)

  • stream (CUDA::Stream, nil) (defaults to: nil)

    Stream for async transfer

Returns:

  • (self)


86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/nvruby/array.rb', line 86

def to_device(device: nil, stream: nil)
  return self if on_device? && (device.nil? || device == @device_index)

  target_device = device || @device_index

  ensure_host_data!

  @device_memory = CUDA::Memory.new(@size_bytes, device: target_device)
  @device_memory.copy_from_host(@host_memory, stream: stream)

  @device_index = target_device
  @location = :device

  # Free host memory if not needed
  @host_memory = nil unless Ignis.configuration.with_lock { false } # Keep host copy option

  self
end

#to_host(stream: nil) ⇒ self

Transfer data to host

Parameters:

  • stream (CUDA::Stream, nil) (defaults to: nil)

    Stream for async transfer

Returns:

  • (self)


108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/nvruby/array.rb', line 108

def to_host(stream: nil)
  return self if on_host?

  ensure_device_data!

  @host_memory = FFI::MemoryPointer.new(:uint8, @size_bytes)
  @device_memory.copy_to_host(host_buffer: @host_memory, stream: stream)

  @location = :host
  @device_memory.free!
  @device_memory = nil

  self
end

#to_sString

Returns String representation.

Returns:

  • (String)

    String representation



279
280
281
282
# File 'lib/nvruby/array.rb', line 279

def to_s
  loc = on_device? ? "device:#{@device_index}" : "host"
  "NvArray(shape=#{@shape}, dtype=#{@dtype}, #{loc})"
end

#transpose(axes: nil) ⇒ NvArray

Transpose the array

Parameters:

  • axes (Array<Integer>, nil) (defaults to: nil)

    Permutation of axes (reverses if nil)

Returns:

Raises:

  • (DimensionError)


192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/nvruby/array.rb', line 192

def transpose(axes: nil)
  axes ||= (0...ndim).to_a.reverse

  raise DimensionError, "Invalid axes for transpose" unless axes.sort == (0...ndim).to_a

  new_shape = axes.map { |ax| @shape[ax] }
  new_strides = axes.map { |ax| @strides[ax] }

  result = dup
  result.instance_variable_set(:@shape, new_shape)
  result.instance_variable_set(:@strides, new_strides)
  result
end

#zero!(stream: nil) ⇒ self

Zero out the array

Parameters:

  • stream (CUDA::Stream, nil) (defaults to: nil)

    Stream for async operation

Returns:

  • (self)


260
261
262
263
264
265
266
267
# File 'lib/nvruby/array.rb', line 260

def zero!(stream: nil)
  if on_device?
    @device_memory.zero!(stream: stream)
  else
    @host_memory.clear
  end
  self
end