Module: Toy::MRI

Defined in:
lib/toy/mri.rb

Overview

MRI-side runtime support for the Spinel FFI DSL.

Defined Under Namespace

Classes: NativeCallError, ShimCollisionError

Constant Summary collapse

NATIVE_LIB =

The Stage B shared library (CPU ggml + tinynn, self-contained —‘make libtinynn_shared`). Repo-relative: this file lives at <root>/lib/toy/mri.rb, the artifact at <root>/tinynn/.

File.expand_path("../../tinynn/libtinynn_ggml_shared.so", __dir__)
INT_ARRAY_FMT =

native long == int64_t on LP64

"l!*"
FLOAT_ARRAY_FMT =

C double

"d*"
OFF_STATE_NATIVES =

The deliberate NON-RAISING declarations of the STUB ARM ONLY —every other declared function raises NativeCallError there. In the NATIVE arm this table is BYPASSED ENTIRELY: tnn_null_ptr and the trace family bind through Fiddle like everything else (real calls, real off-state values from tinynn_trace.c, real NULL → 0 from tnn_null_ptr). Two families, both with an exact, documented pure-value semantic (no computation is being masked):

  1. ‘tnn_null_ptr` — returns a typed NULL `void *`; exists only as a Spinel type-inference workaround (`:ptr` ivars seed with it instead of `nil`, else post-85a4670 inference boxes them as sp_RbVal — see lib/toy/ffi/tinynn.rb FFNFFICache#initialize). Under MRI the honest equivalent of a typed NULL handle is `nil`. Without this, `TransformerLM.new` — the PURE-RUBY teaching model, the whole point of Stage A — raises at construction despite never executing an FFI branch.

  2. The trace OFF-state (tinynn/tinynn_trace.c): until ‘tnn_trace_open` succeeds, natively begin→0, end/mark→nothing, active→0, op_capture_active→0. Under MRI tracing can never open (`tnn_trace_open` RAISES — a state change we cannot honor, so it fails loud, as does tnn_trace_set_op_capture), so the off-state values ARE the native semantics, forever. Without this, every Mat op raises — Mat#matmul et al. are unconditionally instrumented with tnn_trace_begin/end.

{
  tnn_null_ptr:                nil,
  tnn_trace_begin:             0,
  tnn_trace_end:               nil,
  tnn_trace_mark:              nil,
  tnn_trace_active:            0,
  tnn_trace_op_capture_active: 0,
}.freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.cflagsObject (readonly)

Public registry — the Fiddle arm binds against exactly these.



62
63
64
# File 'lib/toy/mri.rb', line 62

def cflags
  @cflags
end

.declarationsObject (readonly)

Public registry — the Fiddle arm binds against exactly these.



62
63
64
# File 'lib/toy/mri.rb', line 62

def declarations
  @declarations
end

.libsObject (readonly)

Public registry — the Fiddle arm binds against exactly these.



62
63
64
# File 'lib/toy/mri.rb', line 62

def libs
  @libs
end

.native_handleObject (readonly)

Public registry — the Fiddle arm binds against exactly these.



62
63
64
# File 'lib/toy/mri.rb', line 62

def native_handle
  @native_handle
end

Class Method Details

.fiddle_type(t) ⇒ Object

Spinel’s FFI lowering, pinned by probing generated C (a699cf9):

:ptr → void*            (Ruby side: integer-valued handle)
:int → int              :long → long          :size_t → size_t
:double → double        :str → const char*    :void → void
:int_array   → const int64_t* (Spinel Array<Integer> storage;
               == native long on LP64 → pack("l!*"))
:float_array → const double*  (Array<Float> storage — NOT
               float; the C side narrows where needed → pack("d*"))

The tinynn headers declare exactly these widths per family (verified: int64_t*/long* for the 2 int_array sigs, double* for the 6 float_array sigs).



110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/toy/mri.rb', line 110

def fiddle_type(t)
  case t
  when :ptr, :str, :int_array, :float_array then Fiddle::TYPE_VOIDP
  when :int    then Fiddle::TYPE_INT
  when :long   then Fiddle::TYPE_LONG
  when :size_t then Fiddle::TYPE_SIZE_T
  when :double then Fiddle::TYPE_DOUBLE
  when :void   then Fiddle::TYPE_VOID
  else
    raise NativeCallError,
          "unknown FFI type #{t.inspect} in a declaration — extend " \
          "Toy::MRI.fiddle_type in lockstep with Spinel's DSL (toy#71)"
  end
end

.native?Boolean

True when the Stage B native arm is live (shared lib dlopened).

Returns:

  • (Boolean)


65
66
67
# File 'lib/toy/mri.rb', line 65

def native?
  !@native_handle.nil?
end

.native_impl(mod, name, argtypes, rettype) ⇒ Object

Build the native (Fiddle-backed) implementation for one declaration. Spinel’s array specs are ZERO-COPY (the C side reads AND writes the Ruby array’s storage); Fiddle can’t share storage with an MRI Array, so we mirror the semantics: pack → call →unpack → Array#replace IN PLACE on the caller’s array. That keeps output-array conventions (pre-sized Array filled by C, e.g. tnn_read_i32_file, tnn_download_to_f64_array) working unchanged.



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/toy/mri.rb', line 135

def native_impl(mod, name, argtypes, rettype)
  sym =
    begin
      @native_handle[name.to_s]
    rescue Fiddle::DLError
      nil
    end
  if sym.nil?
    # Loud, named, at the CALL site (a require-time raise would
    # take down the whole surface for one stale artifact).
    return lambda do |*_args|
      raise NativeCallError,
            "native call `#{name}` (declared in #{mod}) is missing from " \
            "#{File.basename(NATIVE_LIB)} — stale artifact? rebuild with " \
            "`make libtinynn_shared` (toy#71)"
    end
  end
  fn = Fiddle::Function.new(sym,
                            argtypes.map { |t| fiddle_type(t) },
                            fiddle_type(rettype))
  lambda do |*args|
    if args.length != argtypes.length
      raise ArgumentError,
            "#{mod}.#{name}: #{args.length} args for " \
            "#{argtypes.length}-arg native declaration"
    end
    arrays = nil
    cargs = Array.new(args.length)
    i = 0
    while i < args.length
      a = args[i]
      cargs[i] =
        case argtypes[i]
        when :ptr then a.nil? ? 0 : a
        when :int, :long, :size_t then Integer(a)
        when :double then Float(a)
        when :str then a # String buffer → const char*
        when :int_array
          buf = a.pack(INT_ARRAY_FMT)
          (arrays ||= []) << [a, buf, INT_ARRAY_FMT]
          buf
        when :float_array
          buf = a.pack(FLOAT_ARRAY_FMT)
          (arrays ||= []) << [a, buf, FLOAT_ARRAY_FMT]
          buf
        end
      i += 1
    end
    r = fn.call(*cargs)
    arrays&.each { |orig, buf, fmt| orig.replace(buf.unpack(fmt)) }
    case rettype
    when :ptr  then r.to_i
    when :str  then r.null? ? nil : r.to_s
    when :void then nil
    else r
    end
  end
end

.record_cflags(mod, flags) ⇒ Object



74
75
76
77
# File 'lib/toy/mri.rb', line 74

def record_cflags(mod, flags)
  (@cflags[mod] ||= []) << flags
  nil
end

.record_func(mod, name, argtypes, rettype) ⇒ Object



79
80
81
82
# File 'lib/toy/mri.rb', line 79

def record_func(mod, name, argtypes, rettype)
  (@declarations[mod] ||= []) << [name.to_sym, argtypes, rettype]
  nil
end

.record_lib(mod, name) ⇒ Object



69
70
71
72
# File 'lib/toy/mri.rb', line 69

def record_lib(mod, name)
  (@libs[mod] ||= []) << name
  nil
end

.try_open_native!Object

── Stage B: the Fiddle binder ──────────────────────────────────Called once at load. Honors TOY_MRI_NATIVE=0 (explicit stub-arm opt-out, silent); otherwise dlopens the shared lib when present, or hints (one stderr line) at the make target when absent.



88
89
90
91
92
93
94
95
96
97
# File 'lib/toy/mri.rb', line 88

def try_open_native!
  return if ENV["TOY_MRI_NATIVE"] == "0"
  if File.file?(NATIVE_LIB)
    require "fiddle"
    @native_handle = Fiddle.dlopen(NATIVE_LIB)
  else
    warn "toy/mri: stub arm (native calls raise) — `make libtinynn_shared` " \
         "builds tinynn/libtinynn_ggml_shared.so for real MRI compute (toy#71)"
  end
end