Created
January 5, 2026 02:09
-
-
Save ruvnet/eea94075d68d958b0779b48880668335 to your computer and use it in GitHub Desktop.
PowerInfer-Style Activation Locality Inference Engine for Ruvector (SPARC Specification)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| PowerInfer-Style Activation Locality Inference Engine for Ruvector (SPARC Specification) | |
| Specification | |
| Goals and Motivation: The goal is to create a high-speed inference engine that exploits the activation locality in neural networks (especially transformers) to accelerate on-device inference while preserving accuracy. Modern large models exhibit a power-law distribution of neuron activations – a small subset of “hot” neurons are consistently high-activation across inputs, while the majority are “cold” and only occasionally activate . By focusing compute on the most active neurons and skipping or offloading the rest, we can dramatically reduce effective model size and latency. The engine will leverage this insight (as in PowerInfer ) to meet edge deployment constraints. Key performance targets include multi-fold speedups (2×–10×) over dense inference and significant memory savings (e.g. 40%+ lower RAM usage ) with minimal accuracy impact (<1% drop on benchmarks ). It should enable running larger models on resource-constrained environments (browsers, mobile CPUs) at near real-time speeds . | |
| Supported Model Classes: The engine will support multiple model types common in the Ruvector ecosystem, including: | |
| • LFM2-style embedding models: e.g. Liquid AI’s LFM2 transformer backbones used for retrieval. These models use efficient architectural elements (short-range gated convolutions, grouped-query attention) for fast edge inference . Our engine will integrate with LFM2 encoders to further exploit any internal gating (e.g. input-aware gated conv layers) for sparsity. | |
| • Sentence-transformer encoders: e.g. BERT or MiniLM variants for semantic search. These typically have hundreds of millions of parameters with GeLU/SiLU activations. The engine will handle their Transformer layers (self-attention + FFN), applying activation sparsity in the feed-forward blocks. Even without retraining, ~50–60% of FFN neurons can be skipped via thresholding with negligible effect  . | |
| • Llama-family decoder models (GGUF format): e.g. Llama2 or Mistral in quantized GGUF form (for RuvLLM). These use SwiGLU activation in feed-forward layers, which by default has less hard sparsity . The engine will support such models by either utilizing relufied sparse variants (like “ReLU Llama” or TurboSparse models with 80–90% of neurons inactive  ) or by applying dynamic sparsification at runtime (with careful error-budget thresholding  if using vanilla models). Full compatibility with the GGUF format is required – the engine must parse the model structure, weights (including quantized weight blocks), and Llama-specific components like rotary positional encodings and multiple attention heads. | |
| Latency, Throughput, and Memory Targets: The engine is designed for low-latency inference on CPU and WASM (browser/edge) environments. Target end-to-end latency for moderate-size models (e.g. ~300M parameter LFM2 or ~7B LLM) is on the order of tens of milliseconds per inference on a modern laptop or high-end mobile SoC. For example, the 350M LFM2-ColBERT should run as fast as a 150M model baseline (2.3× effective speedup) thanks to the sparse backbone . For LLM decoding, the system aims to approach double-digit token-per-second generation on consumer CPUs – e.g. ~10 tokens/s on a smartphone for a 7B TurboSparse model (matching reported 11 tps on mobile for a 47B sparsified model) . Memory footprint should be minimized: by only keeping frequently-used (“hot”) neuron weights in fast memory and not resident loading all weights at once, we target 1.5–2× reduction in RAM usage for large models . The engine will have a graceful degradation fallback – if sparsity predictions are disabled or if an input unexpectedly requires many neurons, it will run in dense mode to guarantee correctness (at cost of speed). Compatibility constraints include ensuring numerical reproducibility (within tolerance) relative to dense baselines and supporting quantized weights and SIMD alignment. The implementation must be pure Rust (no GPU needed) to compile to WebAssembly for browser/edge use; optional hooks for ARM NPUs or other accelerators should be included via feature flags, without being mandatory. | |
| Compatibility and Integration: The design will align with the Ruvector and RuvLLM frameworks. That means providing a Rust API that fits into Ruvector’s EmbeddingProvider interface (for vector database embedding generation) and RuvLLM’s inference orchestration (for text generation pipelines). The engine should operate in native Rust environments and WebAssembly, ensuring consistent results. All dependencies must be WASM-compatible (no OS-specific calls, no heavyweight native libs) and the code should allow no_std or similar mode if needed for certain embedded targets. We will also ensure the solution is modular – so it can execute on pure CPU or take advantage of an ARM NPU if present. For instance, on a device with a Neural Engine, one could offload the heavy matrix multiplications for “hot” neurons to the NPU while the CPU orchestrates sparsity logic, but if no NPU is available the CPU alone will handle everything. In summary, the specification demands a unified, portable sparse inference engine supporting multiple transformer architectures (encoders and decoders) with tunable sparsity for quality/performance trade-off, fully integrable into the Ruvector ecosystem. | |
| Pseudocode | |
| Below is a high-level pseudocode illustrating the core algorithms: a sparse feed-forward network (FFN) evaluation, the activation predictor, and the neuron planning logic. This is a simplified view focusing on the key steps per transformer layer: | |
| # Assume model is a sequence of layers (attention or FFN). | |
| # 'hot_neurons[layer]' is a precomputed set of always-active neuron indices for layer's FFN (if any). | |
| # 'predictor[layer](x)' produces a set of active neuron indices for the layer given input x. | |
| # For simplicity, we show a standard FFN; GLU variants are handled with minor adjustments. | |
| function forward_transformer(model, input_sequence): | |
| for layer in model.layers: | |
| if layer.type == 'MHAttention': | |
| # Standard multi-head attention (not sparsified here, though could prune heads if needed) | |
| output = attention_forward(layer, input_sequence) | |
| elif layer.type == 'FFN': | |
| output = sparse_ffn_forward(layer, input_sequence) | |
| input_sequence = output # feed to next layer | |
| return output | |
| function sparse_ffn_forward(layer, input_vector): | |
| # 1. Predictor phase: quickly estimate which neurons will be significantly activated | |
| pred_set = predictor[layer](input_vector) # dynamic set of indices likely to be active | |
| active_set = pred_set ∪ hot_neurons[layer] # always include statically hot neurons | |
| # 2. Compute only the active neurons of the first FFN linear layer (W1 * x + b1) | |
| hidden = [0] * layer.hidden_size | |
| for j in active_set: | |
| # Dot product of input with j-th weight vector (W1[j]) | |
| # Use optimized vector ops (SIMD) for this inner product | |
| z = dot_product(layer.W1[j], input_vector) + layer.b1[j] | |
| hidden[j] = activation_fn(z) # e.g. ReLU/GeLU (if GLU, compute paired neurons together) | |
| # Neurons not in active_set remain at 0 (implicitly skipped) | |
| # 3. Second linear layer: compute output using only contributions from active neurons | |
| output = [0] * layer.output_size # (same as model dim) | |
| for j in active_set: | |
| # Add contribution of neuron j: its weight column in W2 times hidden[j] | |
| # W2_j denotes the j-th column of W2 (or j-th row if W2 stored transposed) | |
| axpy(output, layer.W2[:, j], hidden[j]) | |
| # (axpy: fused multiply-add of a vector: output += W2[:,j] * hidden[j]) | |
| # 4. Add bias of second layer | |
| for i in range(layer.output_size): | |
| output[i] += layer.b2[i] | |
| return output | |
| In the above pseudocode, the predictor is critical. It provides a fast estimate of which FFN neurons will be “activated” (i.e. have non-negligible output) for the given input. One simple approach is to use a low-rank approximation of the weight matrix to project the input into a smaller space and identify likely large activation indices . For example, we can precompute a matrix $P$ of shape (r × d_model) and $Q$ of shape (d_hidden × r) such that $Q \cdot P \approx W_1$ (the FFN’s first-layer weight). Here r is a small rank (e.g. 5–10% of the hidden size). Then: | |
| function predictor[layer](input_vector): | |
| # Low-rank projection to estimate W1 * x (FFN pre-activation) | |
| v = P[layer] * input_vector # v is length r (compressed intermediate) | |
| approx_out = Q[layer] * v # approx_out is length d_hidden (approximate neuron outputs) | |
| # Determine active neurons based on a threshold τ or top-K fraction | |
| active_pred = { j | approx_out[j] > τ } # for GLU, consider paired indices together | |
| return active_pred | |
| This predictor essentially acts as a surrogate “gate” that very quickly computes a rough version of the FFN output using only a fraction of the work, then picks the neurons above a threshold τ. The threshold can be tuned per layer to balance sparsity and accuracy – for instance, choose τ such that dropping neurons below τ keeps output error under some budget (as in CETT: Cumulative Error Thresholding) . We can also choose a fixed number K of neurons to activate (e.g. the top 10% by approximate magnitude). The predictor matrices $P, Q$ can be derived offline: e.g. by performing a small SVD on $W_1$ or by a few steps of regression on sample data so that approx_out correlates with the true $W_1 x$. This method was used in sparse inference research to compute “gate approximations lightning-fast” (using 4–10% of hidden dims) . In practice, this process is extremely fast (much faster than computing all $d_{hidden}$ dot-products) and can run in parallel with other computations. | |
| Neuron Planner & Caching: In a real implementation, we maintain a neuron planner structure for each layer that stores hot_neurons[layer], predictor parameters, and caching info. Pseudocode for a simple planner setup: | |
| initialize_planner(model): | |
| for each layer in model: | |
| if layer.type == 'FFN': | |
| # Identify hot neurons via calibration (those with highest average activation) | |
| hot_neurons[layer] = find_hot_neurons(layer, calibration_data, fraction=0.05) | |
| # Precompute low-rank predictor matrices P,Q (or train small ML model for predictor) | |
| P[layer], Q[layer] = low_rank_approx(layer.W1, rank=r) | |
| # Optionally, allocate cache for recently used neuron weights | |
| cache[layer] = new NeuronCache(capacity = C_per_layer) | |
| The hot neuron set can be determined by running a few representative inputs (or using training data) and selecting the neurons that are consistently in the top activations. These will always be computed to avoid missing important features . The planner also sets up a cache of neuron weights to optimize memory access: when a neuron is activated once, its weight vector (from $W_1$ and corresponding column from $W_2$) can be kept in memory in a contiguous, dequantized form for reuse on subsequent inferences or subsequent tokens  . This is useful in iterative LLM decoding, where the same neurons tend to stay active across consecutive tokens (temporal locality) . The planner will update the cache each step by evicting rarely used weights and keeping recently active ones (“neuron-aware caching” similar to PowerInfer’s design ). | |
| Dynamic Execution Flow: During inference, the system interleaves predictor computation and actual sparse computation. For example, while we are computing layer L’s heavy multiply for active neurons, we can asynchronously start computing layer L+1’s predictor based on the output of layer L’s attention block (using the input from layer L that is already available) . This pipelining (sometimes called “look-ahead” or Déjà Vu execution) hides the predictor overhead and keeps all cores busy, further improving throughput. In pseudocode: | |
| # Pipelined execution sketch (assuming multi-threading for parallel predictor compute) | |
| for each layer L in model: | |
| if L is FFN: | |
| spawn_async task: pred_set_next = predictor[L+1](pred_input_next) | |
| output = sparse_ffn_forward(L, current_input) | |
| wait_for pred_set_next if any (ensure predictor result ready when needed) | |
| current_input = output | |
| pred_input_next = output # for next layer's predictor | |
| This way, the predictor for the next layer works on the previous layer’s output in parallel to the current layer doing its main computation, leveraging the observation that transformer layer inputs change slowly across layers due to residual connections (so using the previous layer’s output as a proxy for the next is valid) . Such parallelism, combined with the sparse skipping of neurons, has yielded 2×–6× speedups in research prototypes  and is integral to our engine’s design. | |
| Architecture | |
| High-Level Design: The engine will be implemented as a Rust library (crate) structured into clear components: a runtime for model execution, a planner for deciding neuron activity, and multiple backend implementations for different environments (native, WASM, etc.). The architecture cleanly separates model representation, sparsity planning, and hardware-specific optimizations. This separation allows the core logic to remain constant while swapping out low-level kernels for a given target. Key architectural elements include: | |
| • Model Representation Layer: A set of data structures to load and describe the neural network (Transformer) layers in a hardware-agnostic way. For example, Model contains a list of Layer enums (Attention or FFN). Each FFN layer object holds its weight matrices (W1, W2 – possibly in quantized form), biases, and metadata (hidden size, activation type). This layer will include parsers for model formats: | |
| • GGUF/GGML parser: to load Llama family models from GGUF files (reading tensor data, quantization scales, etc.). | |
| • Transformers/HuggingFace loader: for sentence transformers or LFM2 models (if provided via ONNX or custom format). | |
| • Common Model Trait defining forward-pass interface so that the runtime can treat all models uniformly. For instance, trait ModelRunner { fn forward(&self, input) -> output } implemented for each model type using the engine. | |
| • Activation Planner & Sparse Scheduler: This module (e.g. activation_plan.rs) encapsulates the logic of selecting neurons and planning computation. It contains the predictor parameters (matrices or thresholds) for each layer and implements the functions outlined in the pseudocode (e.g. predictor[layer] and manages hot_neurons). It also manages neuron clusters and caching: grouping neurons into clusters if needed for efficient processing. For example, if using cluster size of 16 neurons, the planner will round-up the active neuron indices to the nearest cluster boundaries and activate a whole cluster at once (to better utilize SIMD and memory alignment) . The planner is effectively the brains that, given an input for a layer, produces an execution plan: which neurons to compute, and on which device/backend (if multiple available). It will expose an API like plan_ffn(layer_index, input_vector) -> ActiveMask where ActiveMask could be a bitmask or index list of active neurons. | |
| • Runtime Execution Engine: The core engine (e.g. engine.rs) orchestrates the end-to-end inference. It traverses the model layers, uses the planner to get the active neuron plan for each FFN, and dispatches computations to the appropriate backend. It implements control flow for different layer types (e.g. calls optimized attention routines for attention layers, sparse FFN routines for FFN layers). The runtime will handle batching if needed (though initial focus is single inference or small batch, as typical in online serving). It also integrates the asynchronous predictor pipeline: e.g. using Rust threads or async tasks to overlap predictor computation with ongoing layer computation (taking advantage of Rust’s async or rayon threadpool for parallelism). The runtime includes the logic for fallback to dense: if for some reason the active set equals the full set (or above a threshold), it can call a dense GEMM kernel to compute the layer in one go (this is important for correctness and also for efficiency when sparsity is low – avoiding overhead). | |
| • Backends (Native, WASM, NPU): We design a Backend trait that defines low-level operations like matrix-vector multiply for a subset of rows, vector add, dequantize-and-multiply, etc. There will be multiple implementations: | |
| • Native CPU backend: optimized for x86-64/ARM CPU with SIMD. This will use Rust’s std::simd or explicit intrinsics to perform vectorized dot products and fused operations. It will also leverage multi-threading (using Rayon or manual thread spawning) to parallelize heavy operations across cores – for example, splitting the active neuron list among threads for the first linear layer multiply, or parallelizing across output dimensions for the second layer accumulation. Memory layout is tuned: we may store W1 in row-major (each neuron’s weights contiguous) and W2 in column-major (each neuron’s output weights contiguous) to facilitate fast gathering and accumulation . This backend will be used in native Rust (and Node.js via Neon or similar if Ruvector runs there). | |
| • WebAssembly backend: a variant that avoids any unsupported instructions and uses WebAssembly SIMD (128-bit) via Rust’s portable simd. This backend runs the same algorithm but within the WASM sandbox constraints (no threads unless web workers are used; limited SIMD width). It ensures the engine can run in browser contexts (for Ruvector’s WASM support ) albeit possibly at lower absolute speed. We will carefully manage memory to avoid heavy heap allocation in WASM and possibly use linear memory alignment tricks for coalesced access to weight data. | |
| • Optional NPU/Accelerator backend: An abstract backend that, if enabled, offloads certain computations to an external accelerator. For example, on an Android device with Hexagon DSP or smartphone NPU, we could use the Qualcomm NNAPI or Apple’s BNNS/ANE for large matrix multiplies. The planner could decide to send the dense part of computation (like all hot neurons as one block) to the NPU while CPU handles the rest. This requires writing thin FFI bindings or using existing crates (e.g. tract or tensorflow-lite delegate). This backend will remain behind a feature flag – the system is functional without it, but pluggable for future. | |
| • Rust Crate Organization: The project can be organized as a multi-crate workspace: | |
| • ruv_infer_core: core data structures, model definitions, planner, and algorithms (no platform-specific code). | |
| • ruv_infer_backend: containing sub-modules or cfg-target sections for native, wasm, npu implementations of the Backend trait. | |
| • We will also include integration crates or modules for Ruvector/RuvLLM: | |
| • e.g. ruv_infer_ruvector: that implements the EmbeddingProvider trait expected by Ruvector, calling into ruv_infer_core. | |
| • and ruv_infer_llm: hooking into RuvLLM’s pipeline (this might be part of RuvLLM directly or provided as utility to register our engine as a backend). | |
| All crates will be no_std-compatible or have minimal dependencies to ensure they compile to WASM and can be embedded easily. The Rust architecture emphasizes modularity: e.g. one could use the planner + backend alone for a custom model outside Ruvector, or use the model loader + runtime as a standalone lightweight inference library. | |
| Data Flow and Module Interaction: When an input (say a text query) comes in: | |
| 1. The Ruvector EmbeddingProvider (implemented by our library) loads the model (if not already loaded) via the Model Representation module. | |
| 2. The runtime engine is invoked with the input. It calls into the Activation Planner for each layer to get active neuron indices (using predictor and hot neuron info). | |
| 3. It then dispatches the actual math to the Backend. For example, for each active neuron cluster, the backend performs the dot-product of that cluster’s weights with the input vector using SIMD. It accumulates results into output buffers. This is repeated for all clusters in the active set. | |
| 4. After computing outputs, the runtime proceeds to the next layer until final output is obtained. The EmbeddingProvider then returns the embedding vector to the Ruvector DB (or, for RuvLLM, the token probabilities for generation). | |
| 5. During execution, the cache may be updated: recently used neuron weights remain in a ready state (e.g. in CPU L2 cache or a pre-allocated buffer) to speed up the next invocation. If running an LLM, the cache definitely retains active neuron weights between token steps, so that the next token’s forward pass can skip loading those weights from main memory or disk again  . | |
| 6. The design ensures thread-safety for the runtime (multiple queries can be processed in parallel if needed, using internal locks or separate planner instances per thread), though the first target is single-stream performance. | |
| Overall, the architecture marries concepts from recent sparse inference research (predictor-based neuron gating, weight caching, hybrid execution) with a robust, portable Rust implementation. By isolating hardware-specific code in backend implementations and keeping the planner abstract, we ensure the system can run anywhere – from a cloud VM to a web browser – and still benefit from activation locality optimizations. | |
| Refinement | |
| With the basic design in place, several refinements and optimizations will be applied to maximize performance and maintain accuracy: | |
| • Quantization Support: Many edge models use weight quantization to reduce memory. The engine will support quantized weights (int8, int4, etc. as in GGUF for Llama). We will implement fast dequantization on the fly for active neurons. For example, if weights are stored in 16-byte blocks for 32 int4 values with a shared scale, our backend will fetch the block for a given neuron, unpack the int4 values to a vector of floats, and dot it with the input (potentially using SIMD for the dequantization step too). Because we skip a large fraction of neurons, the overhead of dequantizing a subset of weights is small, and in many cases we can cache the dequantized vectors for hot neurons to avoid repeating that work. Compatibility with GGUF includes reading the quantization parameters per tensor and handling per-row or per-block scales appropriately. In case a model uses mixed-precision (e.g. some layers int8, some float16), the engine will incorporate those seamlessly (possibly by having different backend routines or branching inside them). Our design favors structured sparsity over unstructured – meaning we drop entire neurons (whole rows/columns of weights) rather than random individual weights. This aligns well with quantization because entire quantized blocks can be skipped if they belong to inactive neurons, and it avoids needing sparse matrix multiplication support. The result is simpler logic and more efficient memory access patterns. | |
| • GGUF and Model Format Compatibility: The refinement phase will ensure that nuances of each model type are handled. For GGUF Llama models, this means implementing the exact activation function (e.g. SiLU or GeLU) and the gated linear unit logic. If the model uses SwiGLU (which splits the hidden layer into two halves and multiplies them after activation), our planner will treat a pair of neurons (one from each half) as the basic unit for sparsity – effectively deciding to compute or skip both together, since the product of one “gate” neuron and one “value” neuron is what forms the output . We might refine the predictor to predict the product’s magnitude rather than each half separately. Additionally, position-wise operations like RMSNorm and positional embeddings (RoPE) will be integrated into the runtime (these are inexpensive compared to FFNs, so they can be executed fully without sparsification). For LFM2 models, any custom layer types (e.g. convolution layers) need implementation: the gated convolution could itself be a source of sparsity (if the gating yields small values for certain convolution channels, we could skip applying some filters). We will investigate if the LFM2 “input-aware gated conv” has inherent sparsity and if so, add a similar predictor for conv filters. Grouped-query attention in LFM2 likely just changes how attention weights are computed; it will be handled in the attention kernel without sparsification (since attention is not the primary bottleneck in these models). | |
| • Fallback to Dense Mode: To ensure robustness, the engine can dynamically fall back to a dense computation for any layer where the sparsity is not beneficial. For instance, during initialization we may detect that some small models (with hidden size, say, 256) don’t gain from sparse execution (the overhead might outweigh savings if only a few neurons are pruned). In those cases, the planner can mark the layer as “always dense”. Similarly, if the predictor ever returns an active set that is, say, >80% of neurons, it may be faster to just compute the full FFN normally. The runtime will check the active_set size and switch to a dense kernel (calling a BLAS routine or our own dense matmul) when appropriate. This guarantees that we never catastrophically slow down worst-case inputs. The threshold for fallback can be tuned or learned (the system could measure timings online and adjust). The dense path will use the same quantized weights (just multiplied fully) to ensure identical accuracy as original. | |
| • SIMD and Parallel Optimizations: We will refine low-level kernels to make use of data-parallel instructions extensively. Rust’s portable SIMD or explicit architecture-specific intrinsics (via cfg(target_arch)) will be used for key operations: | |
| • Dot products for selected neurons will be unrolled and vectorized (e.g. processing 8 or 16 float elements per step on AVX/NEON). We will align weight data to 32-byte boundaries to allow aligned loads. If the input dimension is large (e.g. 4096), we can also parallelize the dot product itself across lanes or threads. | |
| • The accumulation of outputs (the AXPY operation for W2) will be vectorized. Because we store W2 in column-major format, adding a scaled column to the output is a contiguous add for each neuron’s contribution . We can further fuse multiple neurons’ contributions if using cluster groups – e.g. process 4 active neurons together in a small matrix * vector multiply using AVX instructions. | |
| • WebAssembly SIMD: Since WASM supports 128-bit vectors (e.g. 4×32-bit floats), our WASM backend will use those for inner loops. We’ll carefully avoid any floats that could cause nondeterminism across platforms (WASM has well-defined float behavior, so it should match Rust’s). | |
| • Parallel threads: On native platforms, thread-level parallelism will be used for larger models. The refinement will include deciding the grain of parallel tasks – e.g., one strategy is to assign each thread a subset of active neurons to compute in the first layer, then combine results. Another is pipeline parallelism: one thread computes current layer while another pre-computes next layer’s predictor (as discussed). We will likely use a threadpool and post tasks for each layer’s compute and for each predictor, using synchronization to ensure correctness. The parallel predictor (“Déjà Vu”) approach can yield substantial throughput gains with 2+ cores , so it’s a priority in refinement testing. | |
| • Memory layout and cache locality: We will refine how weights are stored and accessed to minimize cache misses. For example, grouping active neurons contiguously means we can fetch a whole cache line of W1 containing several active neurons’ weights in one go. We may also compress or pack weight clusters for faster streaming. Inspired by PowerInfer-2, we might adjust cluster size based on hardware – e.g. on an x86 desktop with large caches, use larger clusters (32–64 neurons) to better use cache lines; on a small device with tight memory, use smaller clusters to reduce wasted compute . These cluster sizes can be empirically tuned. | |
| • Quality vs. Sparsity Trade-off: A critical refinement is the ability to tune how aggressive the sparsity is. We will provide configuration options or even an auto-tuning mechanism. For example: | |
| • A user can set a target percentage of neurons to activate per layer (say 50%). The predictor’s threshold τ will then be adjusted (during a calibration phase) to meet that target on average. | |
| • Alternatively, the user can set an allowable accuracy drop (error budget). Using methods like CETT (Cumulative Error of Tail Truncation), the system can choose thresholds per layer such that the neglected neurons contribute at most, say, 5% or 10% error to the layer’s output norm . This ensures bounded quality loss. Our implementation can use a small calibration dataset to compute these thresholds by binary search as in the CETT algorithm . We’ll document these options clearly. | |
| • We may implement multiple modes: e.g. conservative mode (minimal quality loss, maybe 30-40% sparsity), balanced (~60-70% sparsity with tiny loss), aggressive (~90% sparsity if using a model known to handle it, like TurboSparse). This allows the engine to be used flexibly depending on the scenario (e.g. aggressive for a personal assistant where some minor generation quirks are fine, conservative for an enterprise search where accuracy is paramount). | |
| • The trade-off tuning can even be dynamic: for instance, if the engine is integrated into an interactive application, it could start in high-accuracy mode and if response times are too slow, gradually increase sparsity until it meets latency targets. | |
| • Structured Sparsity and Grouping: We emphasize structured execution: dropping whole neurons or groups means the computation can skip entire vectors of weights. This is far more efficient than unstructured (random weight) sparsity, which would incur scatter/gather overhead. Our engine’s inner loops handle contiguous blocks of active weights, leveraging efficient memory moves. Furthermore, if certain patterns emerge (say, always the same subset of neurons active for a certain task), the planner could recognize and specialize for that. In refinement, we might add a mechanism to detect recurrent active sets and treat them as quasi-“expert paths”. This is analogous to a runtime mixture-of-experts where only one expert’s weights are loaded. Indeed, if the model were an MoE, our system naturally extends: each “expert FFN” could have its own predictor and we only activate one or two experts per input (this is supported as TurboSparse mentions using MoE expert sparsity too) . | |
| In summary, refinement will focus on squeezing maximum efficiency out of the approach (through SIMD, parallelism, caching) and minimizing any accuracy impact (through careful threshold calibration and support for models that are pre-trained for sparsity). We will benchmark different strategies during this phase to choose optimal parameters (thresholds, cluster sizes, etc.) for each model class. | |
| Completion | |
| Finally, we outline the steps for integrating this engine into the Ruvector ecosystem, along with testing and model-specific considerations: | |
| Integration into Ruvector (EmbeddingProvider): Ruvector’s vector database can call our engine to generate embeddings on-the-fly for new data or queries. We will implement the EmbeddingProvider interface (or equivalent mechanism) so that Ruvector can use local models instead of external APIs. For example, if Ruvector’s configuration specifies an embedding model (LFM2-ColBERT-350M or a sentence-transformer) for a certain index, our integration will allow Ruvector to load that model (possibly at startup) via our loader, and then on each query, pass the text through our forward_transformer function to get an embedding vector. This integration needs to handle multi-threaded requests (the DB might embed multiple items concurrently), so we’ll ensure our engine is thread-safe – e.g. by using per-thread model copies or a global model with internal locks around the planner state. In Node.js or WASM contexts, Ruvector can include our compiled module; we may expose a simple JS API (via wasm-bindgen or a Node native addon) such as initModel(path, format), embed(text). All of this will be documented for Ruvector users to seamlessly turn on the PowerInfer-style acceleration for their embeddings. The expected outcome is that Ruvector’s end-to-end pipelines (which might involve retrieving candidates via HNSW and reranking) become faster due to the reduced embedding time, enabling higher throughput of queries. | |
| Integration into RuvLLM Pipeline: RuvLLM is a toolkit for LLM interactions and continuous learning. We will integrate by providing a custom InferenceBackend that RuvLLM can call to generate text. For instance, if RuvLLM orchestrates a dialogue with a local Llama2 7B model, our engine will replace the default inference method (which might currently use llama.cpp or a naive implementation). We’ll ensure that features like token sampling, stopping criteria, etc., interface correctly – likely by outputting logits for each token step that RuvLLM can use in its sampler. The integration steps include: | |
| • Extending RuvLLM’s configuration to accept our engine (perhaps via a flag like use_power_infer: true or selecting a SparseEngine as the backend). | |
| • Wrapping our model loading and execution in whatever abstractions RuvLLM expects. This could mean implementing a trait, e.g. LLMModel with methods like load(model_path), infer(next_token_probabilities). | |
| • Handling streaming generation: RuvLLM may generate tokens one by one; our engine will maintain the internal state between calls (the transformer’s KV cache for attention, and our neuron cache for recently active neurons). We will test that we can resume generation seamlessly, using our cached active neuron set from the previous token to accelerate the next (which PowerInfer showed is beneficial due to neuron concentration across tokens ). | |
| • Ensuring any learning or fine-tuning hooks (like RuvLLM’s runtime LoRA updates via SONA) are compatible. This means if RuvLLM applies a LoRA patch to the model weights, we either re-run a quick calibration to update our predictor (since weights changed) or disable sparsity for that segment until recalibration is done. This ensures the self-learning aspect of RuvLLM isn’t hindered. | |
| Testing Plan: We will conduct comprehensive testing at multiple levels: | |
| 1. Unit Tests: for the predictor (e.g. verify that the approximate selection matches brute-force top-K on a set of sample vectors), for the sparse FFN kernel (verify that computing with all neurons or with all neurons marked active yields the same result as a dense reference within floating-point tolerance), for the model loaders (e.g. read a small GGUF and ensure shapes and values match expectations). | |
| 2. Integration Tests: where we run full models on known inputs and compare outputs to a ground-truth implementation. For example, take a sentence-transformer and run a few sentences through both our engine (with sparsity off and on) and a reference (PyTorch or onnxruntime) to ensure the embeddings are the same or have only minor differences when sparsity is on. For LLMs, we’ll compute perplexity on a validation corpus with our engine vs. baseline to quantify any accuracy loss due to sparsity gating. Our target is to stay within ~1% of baseline accuracy/perplexity , which is in line with reported negligible loss at high sparsity after fine-tuning. | |
| 3. Performance Benchmarks: We will measure inference latency and throughput in various scenarios: | |
| • Single-token and batch inference for LLMs (to measure tokens/sec and end-to-end latency). | |
| • End-to-end embedding generation time for a batch of, say, 100 sentences, comparing with and without sparsity. | |
| • Memory usage profiling: load a large model and measure RAM consumption with our hot/cold weight strategy versus loading the entire model dense. If we implement weight offloading (paging out cold weights), we’ll verify memory drops proportionally. | |
| We will benchmark on a variety of hardware: an x86 server CPU, a typical laptop (with AVX2), an ARM smartphone (using the WASM build or an Android NDK build). The expectation is significant speedups on all. For instance, we anticipate ~5× faster MLP throughput on CPU with caching and fused ops , and overall 2–4× model speedups depending on sparsity levels, consistent with literature  . | |
| 4. Edge Cases: Ensure the engine handles edge cases like very short inputs (where maybe all neurons are “hot” because of lack of information – then we should default to dense), or all-zero inputs, etc. Also test with different sequence lengths for LLMs, since longer sequences stress the attention cache but also perhaps allow more sparsity in later tokens. | |
| Benchmark Targets: As a concrete goal, we aim for the following performance milestones: | |
| • For a 350M parameter LFM2 retriever, achieve embedding generation in ~5-10 ms per sentence on a modern desktop CPU (which would be on par with or faster than a 100M model baseline) – leveraging the model’s own optimizations plus ~50% neuron sparsity on top. This aligns with Liquid AI’s claim of LFM2 being as fast as models 2.3× smaller , and we strive to maintain or exceed that even with our overhead. | |
| • For a 7B LLaMA2 model (quantized) running on a consumer CPU, target ~50–100 ms per token, which would be a 2–3× speedup over llama.cpp baseline. If using a TurboSparse-7B model (with ~90% sparsity), we target an even bigger gain, potentially 5×, approaching ~10–15 tokens/sec generation on CPU . On a smartphone-class device, we expect a lower absolute rate, but still aim to run a 7B model at a few tokens/sec, where previously it might be <1 token/sec without these optimizations. | |
| • Memory-wise, for a 7B model, demonstrate that we can run with, say, only 4GB of RAM available by offloading ~50% of the model to flash (cold neurons) while maintaining generation speed close to fully in-RAM inference. PowerInfer-2 has shown that offloading half the FFN weights to disk still maintains state-of-art speed on phones  , so we intend to replicate that capability in our context (especially relevant if deploying on devices with limited RAM). | |
| Model-Specific Adjustments: Finally, we highlight any custom adjustments needed per supported model class: | |
| • LFM2 Models: We will work closely with the specifics of LFM2’s architecture. The “short-range input-aware gated conv” layers mean that there is a gating mechanism per convolution filter. If possible, we will apply a similar sparsity trick: e.g. use the gating values to skip convolution filters whose gate is nearly zero for the input. This essentially turns off certain CNN channels dynamically, further saving compute. If the gating is a smooth function, we might introduce a small threshold to decide “off/on”. We will validate this doesn’t hurt accuracy. Additionally, grouped-query attention might allow splitting the attention computation across groups – we ensure our attention implementation for LFM2 is correct and possibly optimize by reusing attention weights across groups if applicable. Because LFM2 is designed for efficiency, our engine’s overhead should be minimal here; we primarily ensure compatibility and let LFM2’s own speed optimizations (convolutions, etc.) work in tandem with our neuron-skipping. | |
| • Sentence-Transformers: These models (e.g. SBERT) often have moderate size and sometimes already use mean pooling, etc. Our main focus here is on their FFN layers. We will likely not fine-tune these models to ReLU (as that would be another project), so we rely on runtime thresholding. We might use a relatively conservative threshold (to guarantee identical embeddings up to high precision) for these if high accuracy is needed (since they’re used in search, even a slight change can affect results). However, if acceptable, we’ll allow ~50% of neurons to be dropped on average, which prior work showed can be done with minimal impact . We will test on retrieval benchmarks (e.g. compare top-10 nearest neighbor results with and without sparsity) to confirm no significant change in recall. If any degradation is found, we might dial back sparsity or exclude the final layers from sparsification (so the output embedding remains very precise). | |
| • LLaMA/GGUF Models: These require the most adjustments due to smooth activations. For best results, we will support loading models that are pre-sparsified (e.g. the “TurboSparse-Mistral-7B” checkpoint  or others from HuggingFace ). Those models use a custom dReLU activation to yield ~97% of neurons zeroed out , which our engine will naturally leverage (most neurons will simply never be selected by the predictor). If the user provides a standard LLaMA model, we will by default apply a moderate CETT-based thresholding (error budget ~0.2 of output norm, as per research) which gave >60% sparsity with negligible accuracy change . We’ll document that for even higher speed, users can fine-tune or use relu-activated versions (e.g. ReLU-Llama or ProSparse models that reach ~80% sparsity without quality loss  – our engine is compatible with these formats too). Additionally, we handle the GLU doubling of hidden size: our implementation ensures that if a neuron’s “gate” part is predicted inactive, its corresponding “value” part is skipped as well (since a zero gate nullifies the value anyway). This effectively halves the work in many cases, as a lot of gates might be near zero for any given token in vanilla LLaMA (though not exactly zero, we will approximate). We also pay attention to LLaMA’s rotary embeddings and ensure our attention mechanism reproduces those accurately (these are not sparsified). Quantization in GGUF (like Q4_K, Q8) will be thoroughly tested because any mistake in scale application would hurt model quality – our refinement includes verifying that quantized inference (with no sparsity) matches the dequantized baseline first, then adding sparsity. | |
| By completing these integration and testing steps, we will deliver a robust, production-ready activation locality inference engine. It will reflect the state-of-the-art techniques from PowerInfer and TurboSparse research – such as splitting hot/cold neurons across compute resources, predictor-based sparse computation, and weight caching   – but implemented within Ruvector’s Rust-based, edge-focused paradigm. This engine will enable Ruvector and RuvLLM to run larger models faster and more efficiently on everyday hardware, bringing cutting-edge sparse inference to practical AI deployments. All results and improvements will be documented, and citations to techniques like Deja Vu predictors, Relufication, and structured sparsity have been incorporated to guide future enhancements and ensure the design is grounded in current research.   |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment