README.md
September 22, 2026 · View on GitHub
SHAInet - A neural network in pure Crystal
SHAInet (Super Human Artificial Intelligence Network) is a neural network library written in pure Crystal. Originally created for biologically inspired neural network research, it has evolved into a general-purpose library for training and running neural networks, with a focus on simplicity and ease of use.
Features
- CPU and GPU (CUDA) support
- Multiple layer types and activation functions
- Various training algorithms (SGD, Adam, iRprop+, etc.)
- Streaming data support for large datasets
- HuggingFace model import via SafeTensors (no Python required)
- LLM inference: GPT-2, LLaMA, Mistral, Qwen2, Qwen3, Qwen3-MoE, and Qwen3.5 (Qwen3.5's Gated DeltaNet hybrid stack runs on the GPU; a 27B k-quant GGUF fits a 16 GB card with the trunk placed automatically -- see below)
- Direct GGUF loading (Ollama blobs or
.gguffiles) with weights dequantized inside the GEMV/GEMM kernels, and automatic layer placement -- no environment variables required. Q4_K/Q6_K plus the i-quants IQ4_XS, IQ3_S and IQ3_XXS run on dedicated kernels; the lower-bit types (IQ2_S/XS/XXS, Q2_K, IQ1_M) load too, so mixed-precision files like ISTA-DASLab's GSQ-RCO builds work as shipped - KV-cache decoding, Q8/Q4 weight quantization, and MoE expert offload (run large Mixture-of-Experts models on small GPUs)
Installation
Add to your shard.yml:
dependencies:
shainet:
github: NeuraLegion/shainet
GPU Acceleration (Optional)
- Install the CUDA Toolkit and ensure
libcudart.soandlibcublas.soare in yourLD_LIBRARY_PATH. - SHAInet will auto-detect CUDA and use GPU acceleration if available.
- For cuDNN support, ensure
libcudnn.sois also in yourLD_LIBRARY_PATH. - Compile the project with
-Denable_cuda
Check CUDA availability:
require "shainet"
puts "CUDA available: #{SHAInet::CUDA.available?}"
puts "CUDA version: #{SHAInet::CUDA.version || "unknown"}"
Optimized GPU Setup
For best performance (especially with transformers):
git clone https://github.com/NeuraLegion/shainet.git
cd shainet
make install
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(pwd)
make test
To build kernels manually:
./build_cuda_kernels.sh
RTX 30/40 Series (Ampere/Ada) Note
These GPUs use TF32 tensor cores by default for FP32 matrix multiply, which
reduces mantissa precision from 23 to 10 bits. For LLM inference this can cause
non-deterministic token generation. SHAInet sets
CUBLAS_MATH_DISALLOW_REDUCED_PRECISION_REDUCTION automatically, but for full
FP32 precision also set:
export NVIDIA_TF32_OVERRIDE=0
Device management
Layers such as LayerNorm allocate workspace matrices on the first forward pass
and reuse them across iterations. Call to_gpu! or to_cpu! only when
switching devices. Repeated calls without a device change keep the existing
workspaces to avoid unnecessary allocations.
Usage
See examples/ for more.
XOR Example
require "shainet"
data = [
[[0, 0], [0]],
[[1, 0], [1]],
[[0, 1], [1]],
[[1, 1], [0]],
]
net = SHAInet::Network.new
net.add_layer(:input, 2)
net.add_layer(:hidden, 2)
net.add_layer(:output, 1)
net.fully_connect
net.train(data: data,
training_type: :sgdm,
cost_function: :mse,
epochs: 5000,
log_each: 1000)
puts net.run([0, 1])
Load a GGUF model (Ollama blob or .gguf file)
Point HFLoader.load at an Ollama model name, a blob, or a .gguf file. Q4_K and
Q6_K weights are kept quantized and dequantized inside the GEMV/GEMM kernels, and
layer placement is automatic — no environment variables are needed:
net = SHAInet::HFLoader.load("qwen3.5:27b") # Ollama model name
net = SHAInet::HFLoader.load("/path/to/model.gguf") # or a file
net.use_kv_cache = true
tokenizer = SHAInet::GGUF.extract_tokenizer("qwen3.5:27b")
The loader sums the real GGUF tensor sizes, measures free VRAM, and places as many
layers on the card as fit, leaving the rest resident in host RAM. On a 16 GB card a
27B Q4_K/Q6_K model places 52 of 64 layers plus lm_head.
A mixed-precision i-quant build of the same 27B model fits entirely on the same
card -- ISTA-DASLab's Qwen3.8-27B-GSQ-RCO-IQ3_S (11.8 GB) places all 64 layers with
nothing left on the host. That matters more than the file size suggests: eliminating
host-resident layers removes the per-token PCIe streaming that otherwise dominates
decode. Measured on one 16 GB card, same prompt and same greedy output: 85 ms/token
all-resident against 127 ms for the Q4_K build at 52 of 64 layers.
Files like these assign a quantization type per tensor rather than one type for the whole model, so a single file mixes eight of them. Those carrying most of the weight (IQ4_XS, IQ3_S, IQ3_XXS) have dedicated GEMV kernels; the low-bit tail is converted once at load time.
Weights left on the host are handled differently depending on batch size, because
the two regimes are bound by different resources. Generating one token is
memory-bound — the CPU streams those weights at full DDR5 bandwidth, so it keeps
up. Prefill at hundreds of rows is compute-bound instead, where a CPU is about two
orders of magnitude off the GPU, so past 32 rows the quantized weight is uploaded
to the card and the matmul runs there. This mirrors llama.cpp's offload_op
scheduler hook and uses its 32-row threshold; override with
SHAINET_HOST_OFFLOAD_MIN_ROWS, or disable with SHAINET_HOST_OFFLOAD_OP=0.
For full fp32 determinism SHAInet sets CUBLAS_PEDANTIC_MATH on its cuBLAS
handles, so TF32 tensor cores are not used for SGEMM and NVIDIA_TF32_OVERRIDE=0
is not required. Set SHAINET_CUBLAS_TF32=1 to allow TF32 back.
Load a HuggingFace Model (SafeTensors)
Load models directly from HuggingFace SafeTensors — no Python, no PyTorch, just
pure Crystal binary parsing. HFLoader.load auto-detects the architecture from
the model's config.json:
require "shainet"
# Auto-detects the architecture (gpt2 / llama / mistral / qwen2 / qwen3 / qwen3_moe / qwen3_5)
net = SHAInet::HFLoader.load("/path/to/model-dir")
# Optionally quantize weights to int8 at load time (Q8):
net = SHAInet::HFLoader.load("/path/to/model-dir", quantize: true, bits: 8)
Supported architectures: GPT-2, LLaMA, Mistral, Qwen2, Qwen3, Qwen3-MoE, Qwen3.5. Supported tensor dtypes: F16, BF16, F32, F64.
For a full chat loop (tokenizer, KV-cache decoding, sampling) see
examples/llama_chat.cr; for a tool-using coding agent built on Network#run
see examples/agent.cr.
Iris Classification
data = SHAInet::Data.new_with_csv_input_target("iris.csv", 0..3, 4)
train, test = data.split(0.67)
iris = SHAInet::Network.new
iris.add_layer(:input, 4)
iris.add_layer(:hidden, 5)
iris.add_layer(:output, 3)
iris.fully_connect
iris.train_batch(
data: train,
training_type: :adam,
cost_function: :mse,
epochs: 2000,
log_each: 100)
puts iris.test(test)
Streaming Data
Efficiently train on large datasets:
stream = SHAInet::StreamingData.new(
"data.txt",
shuffle: true,
chunk_size: 1024,
gpu_batches: true)
net = SHAInet::Network.new
net.add_layer(:input, 2, :memory, SHAInet.sigmoid)
net.add_layer(:hidden, 3, :memory, SHAInet.sigmoid)
net.add_layer(:output, 1, :memory, SHAInet.sigmoid)
net.fully_connect
net.train(
data: stream,
training_type: :sgdm,
epochs: 5000,
mini_batch_size: 2,
log_each: 1000)
Advanced
-
Run a real LLaMA model:
crystal run examples/llama_chat.cr -Denable_cuda(auto-downloads Llama-3.2-1B-Instruct, chats with KV cache + GPU). -
Quantized inference (Q8_0): call
net.quantize!after loading to run with int8 weights + per-32-block fp32 scales (dequant-in-kernel GEMV). Cuts weight VRAM ~4x (1B model: ~5GB fp32 → ~1.3GB) and speeds up memory-bound decode.llama_chat.crquantizes by default on GPU; setSHAINET_FP32=1to keep fp32. Benchmark/eval both paths withexamples/q8_eval.cr. -
4-bit quantization (Q4) + MoE expert offload: run large Mixture-of-Experts models on small GPUs by keeping experts in host RAM (pinned) and streaming them to the GPU on demand, backed by a hot-expert LRU cache. Controlled via environment variables:
SHAINET_Q4=1— 4-bit weight quantizationSHAINET_MOE_OFFLOAD=1— offload MoE experts to host RAMSHAINET_DENSE_OFFLOAD=1— offload the dense weights too (attention projections, dense FFN, lm_head). RequiresSHAINET_Q4=1. This is what takes dense models off the VRAM budget, so a far larger dense model fits. Unlike experts, dense weights are touched on every token, so there is no sparsity to amortize the transfer: expect slower decode. Pair it withSHAINET_EXPERT_CACHE_MB— left unbounded the hot cache promotes the dense weights straight back onto the card and you can end up using more VRAM than not offloading (measured on Qwen3-0.6B: 586 MB with the default budget vs 483 MB not offloading vs 228 MB with the cache disabled, at 51 ms/step resident and 80 ms/step streamed, with identical greedy output throughout).SHAINET_EXPERT_CACHE_MB=<N>— VRAM budget for the hot-weight cache (0disables)
For example, Qwen3-Coder-30B-A3B (30B params, ~3B active) runs on a 16 GB GPU.
-
Tool-using coding agent:
examples/agent.cris a small CLI coding agent (file tools + shell, context management, streaming UI) built entirely onNetwork#run. Run it against a local instruct model, e.g.SHAINET_Q4=1 SHAINET_MOE_OFFLOAD=1 crystal run examples/agent.cr -Denable_cuda -- /path/to/model-dir. -
OpenAI-compatible API server:
examples/openai_server.crserves a loaded model over a subset of the OpenAI REST API (POST /v1/chat/completionswith streaming SSE,GET /v1/models), so existing OpenAI clients (theopenaiPython package,curl, LangChain, …) can use it as a drop-in local endpoint. Built entirely onNetwork#run. Example:crystal run examples/openai_server.cr -Denable_cuda -- /path/to/model-dir 8080. Binds to127.0.0.1with no authentication by default — setSHAINET_API_KEYto require anAuthorization: Bearer <key>header before exposing it. -
See
examples/babylm_transformer.crfor training a transformer language model. -
Use
SHAInet::SafeTensors::Fileto read any.safetensorsfile directly.
SafeTensors API
# Low-level tensor access
sf = SHAInet::SafeTensors::File.new("model.safetensors")
sf.tensor_names # => ["transformer.wte.weight", ...]
info = sf.tensors["transformer.wte.weight"]
info.dtype # => F32
info.shape # => [50257, 768]
matrix = sf.read_matrix("transformer.wte.weight") # => SimpleMatrix
data = sf.read_f64("transformer.h.0.ln_1.weight") # => Array(Float64)
sf.close
Autograd
a = SHAInet::SimpleMatrix.tensor(1, 2)
a[0, 0] = SHAInet::Autograd::Tensor.new(2.0)
a[0, 1] = SHAInet::Autograd::Tensor.new(3.0)
w = SHAInet::SimpleMatrix.tensor(2, 1)
w[0, 0] = SHAInet::Autograd::Tensor.new(4.0)
w[1, 0] = SHAInet::Autograd::Tensor.new(5.0)
out = a * w
out[0, 0].as(SHAInet::Autograd::Tensor).backward
Contributing
- Fork https://github.com/NeuraLegion/shainet
- Create a feature branch
- Commit and push your changes
- Open a Pull Request
Contributors
- ArtLinkov - creator, maintainer
- bararchy - creator, maintainer
- drujensen - contributor
- hugoabonizio - contributor
- Rémy Marronnier - contributor
- psikoz - logo design