grx-tensor 0.1.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 ADDED
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GRX
4
+ module Optim
5
+ # ================================================================
6
+ # SGD — Stochastic Gradient Descent (con momentum opcional)
7
+ # ================================================================
8
+ class SGD
9
+ def initialize(params, lr: 0.01, momentum: 0.0, weight_decay: 0.0)
10
+ @params = params
11
+ @lr = lr
12
+ @momentum = momentum
13
+ @weight_decay = weight_decay
14
+ # Buffer de velocidad para momentum
15
+ @velocity = params.map { |p| Tensor.zeros_like(p) }
16
+ end
17
+
18
+ def step
19
+ @params.each_with_index do |param, i|
20
+ next unless param.grad
21
+
22
+ grad = param.grad
23
+
24
+ # L2 regularización (weight decay)
25
+ if @weight_decay > 0
26
+ grad = grad + param.scale(@weight_decay)
27
+ end
28
+
29
+ if @momentum > 0
30
+ # v = momentum*v + grad
31
+ @velocity[i] = @velocity[i].scale(@momentum) + grad
32
+ grad = @velocity[i]
33
+ end
34
+
35
+ if CAPI::LOADED
36
+ CAPI.grx_sgd_step(param.storage.ptr, grad.storage.ptr, @lr, param.numel)
37
+ else
38
+ # Fallback Ruby
39
+ param_data = param.to_a
40
+ grad_data = grad.to_a
41
+ param_data.each_with_index { |v, j| param_data[j] = v - @lr * grad_data[j] }
42
+ param.storage.instance_variable_set(:@data, param_data)
43
+ end
44
+ end
45
+ end
46
+
47
+ def zero_grad
48
+ @params.each(&:zero_grad!)
49
+ end
50
+ end
51
+
52
+ # ================================================================
53
+ # Adam — Adaptive Moment Estimation (Kingma & Ba, 2015)
54
+ # El optimizador estándar para redes neuronales profundas.
55
+ # ================================================================
56
+ class Adam
57
+ def initialize(params, lr: 0.001, beta1: 0.9, beta2: 0.999,
58
+ epsilon: 1e-8, weight_decay: 0.0)
59
+ @params = params
60
+ @lr = lr
61
+ @beta1 = beta1
62
+ @beta2 = beta2
63
+ @epsilon = epsilon
64
+ @weight_decay = weight_decay
65
+ @t = 0 # paso actual
66
+
67
+ # Momentos de primer y segundo orden (inicializados en cero)
68
+ @m = params.map { |p| Tensor.zeros_like(p) }
69
+ @v = params.map { |p| Tensor.zeros_like(p) }
70
+ end
71
+
72
+ def step
73
+ @t += 1
74
+ beta1t = @beta1 ** @t # beta1^t para corrección de bias
75
+ beta2t = @beta2 ** @t
76
+
77
+ @params.each_with_index do |param, i|
78
+ next unless param.grad
79
+
80
+ grad = param.grad
81
+
82
+ if @weight_decay > 0
83
+ grad = grad + param.scale(@weight_decay)
84
+ end
85
+
86
+ if CAPI::LOADED
87
+ CAPI.grx_adam_step(
88
+ param.storage.ptr,
89
+ @m[i].storage.ptr,
90
+ @v[i].storage.ptr,
91
+ grad.storage.ptr,
92
+ @lr, @beta1, @beta2, @epsilon,
93
+ beta1t, beta2t,
94
+ param.numel
95
+ )
96
+ else
97
+ # Fallback Ruby puro
98
+ p_data = param.to_a
99
+ m_data = @m[i].to_a
100
+ v_data = @v[i].to_a
101
+ g_data = grad.to_a
102
+ p_data.each_with_index do |_, j|
103
+ m_data[j] = @beta1 * m_data[j] + (1 - @beta1) * g_data[j]
104
+ v_data[j] = @beta2 * v_data[j] + (1 - @beta2) * g_data[j] ** 2
105
+ mh = m_data[j] / (1 - beta1t)
106
+ vh = v_data[j] / (1 - beta2t)
107
+ p_data[j] -= @lr * mh / (Math.sqrt(vh) + @epsilon)
108
+ end
109
+ param.storage.instance_variable_set(:@data, p_data)
110
+ @m[i].storage.instance_variable_set(:@data, m_data)
111
+ @v[i].storage.instance_variable_set(:@data, v_data)
112
+ end
113
+ end
114
+ end
115
+
116
+ def zero_grad
117
+ @params.each(&:zero_grad!)
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fiddle"
4
+
5
+ module GRX
6
+ # ===================================================================
7
+ # Storage — Buffer de memoria nativa
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.
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.
18
+ # ===================================================================
19
+ class Storage
20
+ attr_reader :size
21
+
22
+ # ptr expuesto para que CAPI pueda leerlo directamente
23
+ attr_reader :ptr
24
+
25
+ def initialize(array_plano)
26
+ @size = array_plano.size
27
+
28
+ if CAPI::LOADED
29
+ # --- MODO RÁPIDO: memoria C alineada ---
30
+ # grx_alloc devuelve un double* alineado a 32 bytes
31
+ @ptr = CAPI.grx_alloc(@size)
32
+ raise StorageError, "grx_alloc falló (OOM)" if @ptr.null?
33
+
34
+ # Empaquetamos el Array de Ruby en el buffer C como doubles (little-endian)
35
+ # Array#pack("d*") → String binaria de IEEE 754 doubles
36
+ bytes = array_plano.pack("d*")
37
+ @ptr[0, bytes.bytesize] = bytes
38
+
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).
42
+ ptr_to_free = @ptr
43
+ ObjectSpace.define_finalizer(self, self.class.make_finalizer(ptr_to_free))
44
+ else
45
+ # --- MODO FALLBACK: Array de Ruby ---
46
+ @data = array_plano.map(&:to_f)
47
+ @ptr = nil
48
+ end
49
+ end
50
+
51
+ def self.make_finalizer(ptr)
52
+ proc { CAPI.grx_free(ptr) }
53
+ end
54
+
55
+ # ------------------------------------------------------------------
56
+ # Lectura / escritura — usadas solo en modo fallback y por get()
57
+ # Las operaciones masivas (add, mul, etc.) van directo por @ptr en C.
58
+ # ------------------------------------------------------------------
59
+ def read(indice)
60
+ if CAPI::LOADED
61
+ # Leemos 8 bytes desde el offset correcto y los desempaquetamos como double
62
+ @ptr[indice * 8, 8].unpack1("d")
63
+ else
64
+ @data[indice]
65
+ end
66
+ end
67
+
68
+ def write(indice, valor)
69
+ if CAPI::LOADED
70
+ @ptr[indice * 8, 8] = [valor.to_f].pack("d")
71
+ else
72
+ @data[indice] = valor.to_f
73
+ end
74
+ end
75
+
76
+ # Vuelca todo el buffer a un Array de Ruby (para to_a, inspect, tests)
77
+ def to_ruby_array
78
+ if CAPI::LOADED
79
+ @ptr[0, @size * 8].unpack("d#{@size}")
80
+ else
81
+ @data.dup
82
+ end
83
+ end
84
+ end
85
+ end