grx-tensor 0.1.0 → 0.2.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.
data/lib/grx/optim.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module GRX
4
4
  module Optim
5
5
  # ================================================================
6
- # SGD — Stochastic Gradient Descent (con momentum opcional)
6
+ # SGD — Stochastic Gradient Descent (with optional momentum)
7
7
  # ================================================================
8
8
  class SGD
9
9
  def initialize(params, lr: 0.01, momentum: 0.0, weight_decay: 0.0)
@@ -11,7 +11,7 @@ module GRX
11
11
  @lr = lr
12
12
  @momentum = momentum
13
13
  @weight_decay = weight_decay
14
- # Buffer de velocidad para momentum
14
+ # Velocity buffer for momentum
15
15
  @velocity = params.map { |p| Tensor.zeros_like(p) }
16
16
  end
17
17
 
@@ -21,7 +21,7 @@ module GRX
21
21
 
22
22
  grad = param.grad
23
23
 
24
- # L2 regularización (weight decay)
24
+ # L2 regularization (weight decay)
25
25
  if @weight_decay > 0
26
26
  grad = grad + param.scale(@weight_decay)
27
27
  end
@@ -35,7 +35,7 @@ module GRX
35
35
  if CAPI::LOADED
36
36
  CAPI.grx_sgd_step(param.storage.ptr, grad.storage.ptr, @lr, param.numel)
37
37
  else
38
- # Fallback Ruby
38
+ # Ruby fallback
39
39
  param_data = param.to_a
40
40
  grad_data = grad.to_a
41
41
  param_data.each_with_index { |v, j| param_data[j] = v - @lr * grad_data[j] }
@@ -51,7 +51,7 @@ module GRX
51
51
 
52
52
  # ================================================================
53
53
  # Adam — Adaptive Moment Estimation (Kingma & Ba, 2015)
54
- # El optimizador estándar para redes neuronales profundas.
54
+ # The standard optimizer for deep neural networks.
55
55
  # ================================================================
56
56
  class Adam
57
57
  def initialize(params, lr: 0.001, beta1: 0.9, beta2: 0.999,
@@ -62,16 +62,16 @@ module GRX
62
62
  @beta2 = beta2
63
63
  @epsilon = epsilon
64
64
  @weight_decay = weight_decay
65
- @t = 0 # paso actual
65
+ @t = 0 # current step
66
66
 
67
- # Momentos de primer y segundo orden (inicializados en cero)
67
+ # First and second order moment vectors (zero-initialized)
68
68
  @m = params.map { |p| Tensor.zeros_like(p) }
69
69
  @v = params.map { |p| Tensor.zeros_like(p) }
70
70
  end
71
71
 
72
72
  def step
73
73
  @t += 1
74
- beta1t = @beta1 ** @t # beta1^t para corrección de bias
74
+ beta1t = @beta1 ** @t # beta1^t for bias correction
75
75
  beta2t = @beta2 ** @t
76
76
 
77
77
  @params.each_with_index do |param, i|
@@ -94,7 +94,7 @@ module GRX
94
94
  param.numel
95
95
  )
96
96
  else
97
- # Fallback Ruby puro
97
+ # Pure Ruby fallback
98
98
  p_data = param.to_a
99
99
  m_data = @m[i].to_a
100
100
  v_data = @v[i].to_a
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GRX
4
+ # ===================================================================
5
+ # Serialization — Native binary .grx format
6
+ #
7
+ # Binary layout:
8
+ # - 8 bytes: Magic header "GRX1\0\0\0\0"
9
+ # - 4 bytes: Number of parameter tensors (unsigned 32-bit big-endian)
10
+ # For each tensor:
11
+ # - 2 bytes: Rank (number of dimensions)
12
+ # - 4 bytes * rank: Dimensions of the shape (uint32 big-endian)
13
+ # - 8 bytes: Total numel (uint64 big-endian)
14
+ # - numel * 8 bytes: Direct binary packed IEEE 754 doubles
15
+ # ===================================================================
16
+ module Serialization
17
+ MAGIC = "GRX1\x00\x00\x00\x00".b
18
+
19
+ def self.save(model, path)
20
+ params = model.parameters
21
+ File.open(path, "wb") do |f|
22
+ f.write(MAGIC)
23
+ f.write([params.size].pack("N"))
24
+ params.each do |p|
25
+ shape = p.shape
26
+ f.write([shape.size].pack("n"))
27
+ f.write(shape.pack("N*"))
28
+ f.write([p.numel].pack("Q>"))
29
+ # Direct binary copy from native C buffer
30
+ bytes = if p.storage.ptr
31
+ p.storage.ptr[0, p.numel * 8]
32
+ else
33
+ p.to_a.pack("d*")
34
+ end
35
+ f.write(bytes)
36
+ end
37
+ end
38
+ path
39
+ end
40
+
41
+ def self.load(model, path)
42
+ params = model.parameters
43
+ File.open(path, "rb") do |f|
44
+ magic = f.read(8)
45
+ raise StorageError, "Invalid format: not a valid .grx binary file" unless magic == MAGIC
46
+ count = f.read(4).unpack1("N")
47
+ raise StorageError, "Parameter count mismatch: model has #{params.size}, file has #{count}" unless count == params.size
48
+
49
+ params.each_with_index do |p, idx|
50
+ rank = f.read(2).unpack1("n")
51
+ shape = f.read(rank * 4).unpack("N*")
52
+ numel = f.read(8).unpack1("Q>")
53
+ raise ShapeError, "Shape mismatch for parameter #{idx}: expected #{p.shape}, got #{shape}" unless shape == p.shape
54
+
55
+ bytes = f.read(numel * 8)
56
+ if p.storage.ptr
57
+ p.storage.ptr[0, bytes.bytesize] = bytes
58
+ else
59
+ p.storage.instance_variable_set(:@data, bytes.unpack("d*"))
60
+ end
61
+ end
62
+ end
63
+ model
64
+ end
65
+ end
66
+ end
data/lib/grx/storage.rb CHANGED
@@ -4,45 +4,37 @@ require "fiddle"
4
4
 
5
5
  module GRX
6
6
  # ===================================================================
7
- # Storage — Buffer de memoria nativa
7
+ # Storage — Native memory buffer
8
8
  #
9
- # Cuando CAPI está cargado:
10
- # @ptr → Fiddle::Pointer a un bloque de doubles alineado a 32 bytes
11
- # reservado con grx_alloc() (malloc alineado en C).
12
- # Los datos viven en el heap de C, NO en el GC de Ruby.
9
+ # When CAPI is loaded:
10
+ # @ptr → Fiddle::Pointer to 32-byte aligned doubles block
11
+ # allocated via grx_alloc() (C posix_memalign / _aligned_malloc).
12
+ # Data lives in C heap, NOT managed by Ruby GC.
13
13
  #
14
- # Cuando CAPI NO está cargado (fallback):
15
- # @data → Array de Ruby normal (lento pero siempre correcto).
16
- #
17
- # La separación entre los dos modos es transparente para Tensor.
14
+ # When CAPI is NOT loaded (fallback):
15
+ # @data → Standard Ruby Array (slow but correct).
18
16
  # ===================================================================
19
17
  class Storage
20
18
  attr_reader :size
21
-
22
- # ptr expuesto para que CAPI pueda leerlo directamente
23
19
  attr_reader :ptr
24
20
 
25
21
  def initialize(array_plano)
26
22
  @size = array_plano.size
27
23
 
28
24
  if CAPI::LOADED
29
- # --- MODO RÁPIDO: memoria C alineada ---
30
- # grx_alloc devuelve un double* alineado a 32 bytes
25
+ # Fast mode: aligned C memory
31
26
  @ptr = CAPI.grx_alloc(@size)
32
- raise StorageError, "grx_alloc falló (OOM)" if @ptr.null?
27
+ raise StorageError, "grx_alloc failed (OOM)" if @ptr.null?
33
28
 
34
- # Empaquetamos el Array de Ruby en el buffer C como doubles (little-endian)
35
- # Array#pack("d*") → String binaria de IEEE 754 doubles
29
+ # Pack Ruby Array into C buffer as IEEE 754 doubles
36
30
  bytes = array_plano.pack("d*")
37
31
  @ptr[0, bytes.bytesize] = bytes
38
32
 
39
- # Registramos un finalizer para liberar la memoria C cuando el objeto
40
- # Ruby sea recolectado por el GC. Usamos ObjectSpace para evitar
41
- # que el closure capture self (lo que impediría la recolección).
33
+ # Finalizer releases C memory upon Ruby GC collection
42
34
  ptr_to_free = @ptr
43
35
  ObjectSpace.define_finalizer(self, self.class.make_finalizer(ptr_to_free))
44
36
  else
45
- # --- MODO FALLBACK: Array de Ruby ---
37
+ # Fallback mode: Ruby Array
46
38
  @data = array_plano.map(&:to_f)
47
39
  @ptr = nil
48
40
  end
@@ -53,12 +45,11 @@ module GRX
53
45
  end
54
46
 
55
47
  # ------------------------------------------------------------------
56
- # Lectura / escriturausadas solo en modo fallback y por get()
57
- # Las operaciones masivas (add, mul, etc.) van directo por @ptr en C.
48
+ # Read / Writeused in fallback mode and by item/get()
49
+ # High-performance tensor ops operate directly on @ptr in C.
58
50
  # ------------------------------------------------------------------
59
51
  def read(indice)
60
52
  if CAPI::LOADED
61
- # Leemos 8 bytes desde el offset correcto y los desempaquetamos como double
62
53
  @ptr[indice * 8, 8].unpack1("d")
63
54
  else
64
55
  @data[indice]
@@ -73,7 +64,7 @@ module GRX
73
64
  end
74
65
  end
75
66
 
76
- # Vuelca todo el buffer a un Array de Ruby (para to_a, inspect, tests)
67
+ # Dumps entire buffer to a Ruby Array
77
68
  def to_ruby_array
78
69
  if CAPI::LOADED
79
70
  @ptr[0, @size * 8].unpack("d#{@size}")