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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/GUIA_PRINCIPIANTES.md +763 -0
- data/README.es.md +270 -0
- data/README.md +178 -375
- data/grx-tensor.gemspec +4 -2
- data/lib/grx/c_api.rb +41 -37
- data/lib/grx/data.rb +87 -0
- data/lib/grx/loss.rb +55 -25
- data/lib/grx/nn.rb +153 -36
- data/lib/grx/optim.rb +9 -9
- data/lib/grx/serialization.rb +66 -0
- data/lib/grx/storage.rb +15 -24
- data/lib/grx/tensor.rb +198 -65
- data/lib/grx/utils.rb +28 -0
- data/lib/grx/version.rb +1 -1
- data/lib/grx.rb +14 -3
- metadata +8 -3
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 (
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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 #
|
|
65
|
+
@t = 0 # current step
|
|
66
66
|
|
|
67
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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 —
|
|
7
|
+
# Storage — Native memory buffer
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
# @ptr → Fiddle::Pointer
|
|
11
|
-
#
|
|
12
|
-
#
|
|
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
|
-
#
|
|
15
|
-
# @data →
|
|
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
|
-
#
|
|
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
|
|
27
|
+
raise StorageError, "grx_alloc failed (OOM)" if @ptr.null?
|
|
33
28
|
|
|
34
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
57
|
-
#
|
|
48
|
+
# Read / Write — used 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
|
-
#
|
|
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}")
|