TurboQuant 0.3.0

dotnet add package TurboQuant --version 0.3.0
                    
NuGet\Install-Package TurboQuant -Version 0.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TurboQuant" Version="0.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TurboQuant" Version="0.3.0" />
                    
Directory.Packages.props
<PackageReference Include="TurboQuant" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TurboQuant --version 0.3.0
                    
#r "nuget: TurboQuant, 0.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TurboQuant@0.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TurboQuant&version=0.3.0
                    
Install as a Cake Addin
#tool nuget:?package=TurboQuant&version=0.3.0
                    
Install as a Cake Tool

TurboQuant

Near-optimal vector quantization for .NET 8+

Compress embeddings to 2-4 bits with <0.5% quality loss. Zero dependencies. AOT-ready.

NuGet Build License .NET AOT


Implements Algorithm 1 from TurboQuant: Online Vector Quantization with Near-optimal Distortion Rate (Zandieh et al., ICLR 2026). Lloyd-Max quantization on the exact Beta(d/2, 1/2) distribution with randomized rotation.

768-dim float32 embedding  ──>  TurboQuant 4-bit  ──>  512 bytes
       3072 bytes                                       6x smaller
                                                        cosine > 0.995

Install

dotnet add package TurboQuant

30-second Example

using TurboQuant;

var quantizer = TurboQuantBuilder.Create(dim: 768).WithBits(4).BuildMSE();

var packed = quantizer.Quantize(embedding);        // float[768] -> PackedVector
var restored = quantizer.Dequantize(packed);       // PackedVector -> float[768]
var score = quantizer.ApproxSimilarity(a, b);      // LUT-based, zero allocation

// Zero-alloc dequantize into existing buffer
Span<float> buffer = stackalloc float[768];
quantizer.Dequantize(packed, buffer);

// Serialize for storage
var bytes = packed.ToBytes();
var loaded = PackedVector.FromBytes(bytes);

Why TurboQuant?

TurboQuant Product Quantization Scalar Quantization
Quality (4-bit) cosine > 0.995 cosine ~ 0.96 cosine ~ 0.98
Similarity search LUT, zero alloc Asymmetric dist Full decompress
Needs training data? No Yes (codebook training) No
Online / streaming? Yes No (batch only) Yes
Theory-backed MSE? Yes (proven optimal) No No

Performance

Measured on Apple M5 (ARM64), on battery, .NET 10.0.10, Release, BenchmarkDotNet 0.15.8 on 2026-10-17. The comparison used the implementation before these changes, three warmup iterations and five measurement iterations, with statistics disabled and variable input vectors. System power history recorded battery use before the run started; results should be compared under the same power conditions.

Operation Before After Change
Quantize, 768d, 4-bit, random rotation 92.27 us 82.85 us 10% less time
Quantize, 768d, 4-bit, Hadamard 28.02 us 17.31 us 38% less time
Quantize, 768d, 2-bit, Hadamard 15.44 us 7.27 us 53% less time
Similarity scan, 100K documents, 4-bit 127.16 ms 63.10 ms 2.0x throughput
Similarity scan, 100K documents, 3-bit 544.91 ms 65.35 ms 8.3x throughput
Similarity scan, 100K documents, 2-bit 430.02 ms 30.91 ms 13.9x throughput
Copy keys and values, 4096 tokens × 128d 4.32 MB allocated 0 B allocated reusable output buffers

Similarity scans used 768d inputs with Hadamard padding to 1024 coordinates. The KV comparison used random rotation for both versions; runtime stayed approximately 16 ms because the same full decompression is performed. Smaller range reads avoid decompressing unrelated entries. Numbers describe this CPU and workload, rather than guarantees for other architectures.

SIMD operations use Vector<T> for automatic ISA selection (NEON on ARM, SSE/AVX2/AVX-512 on x86) with dual accumulators to hide data-dependency latency.

ApproxSimilarity scans packed groups directly at all bit widths, without intermediate index arrays. The product tables are initialized lazily and shared per codebook: 256 KiB for 2/4-bit byte groups, 16 KiB for 3-bit coordinate pairs, plus small squared-norm tables. Zero vectors return zero similarity, and unused packing bits are ignored.

Quantize selects centroids and packs bytes in one pass. QuantizeBatch reuses scratch storage across the batch; QuantizeBatchParallel reuses it per worker and processes small batches sequentially. Each result still allocates its own packed byte array. Batch input length must be a multiple of the dimension.

Per-vector timing and distortion statistics are enabled by default. Use .WithStatistics(false) when diagnostics are unnecessary; this applies to MSE quantizers, asymmetric quantizers and KV caches.

Compression Quality

Validated against the paper's theoretical bounds:

Bits Ratio D_mse Cosine Paper Upper Bound
4-bit ~6-8x ⇐ 0.0095 > 0.995 0.011
3-bit ~8-10x ⇐ 0.035 > 0.98 0.043
2-bit ~12-16x ⇐ 0.118 > 0.93 0.170

Ratios depend on whether the dimension is a power of 2 (no padding needed) or not.


Use Cases

Embedding Compression (OpenAI, Ollama, Azure AI)

var embedding = await embeddingModel.GenerateAsync(["your text"]);
var vector = embedding[0].Vector.ToArray();

var quantizer = TurboQuantBuilder.Create(dim: 768).WithBits(4).BuildMSE();
var packed = quantizer.Quantize(vector);

// Store packed vectors in your DB, cache, or memory
// Search directly on compressed data (zero allocation):
var score = quantizer.ApproxSimilarity(queryPacked, docPacked);

100K embeddings (768d): 292 MB → 37 MB

RAG Pipeline: Compressed Vector Store

var quantizer = TurboQuantBuilder.Create(dim: 768).WithBits(4).BuildMSE();
var store = new Dictionary<string, PackedVector>();

// Index
foreach (var doc in documents)
{
    var emb = await generator.GenerateAsync([doc.Text]);
    store[doc.Id] = quantizer.Quantize(emb[0].Vector.ToArray());
}

// Search (LUT-based, zero allocation)
var queryPacked = quantizer.Quantize(queryEmbedding);
var topK = store
    .Select(kv => (kv.Key, Score: quantizer.ApproxSimilarity(queryPacked, kv.Value)))
    .OrderByDescending(x => x.Score)
    .Take(10);

LLM KV Cache: Extend Context Length

var (cache, window) = TurboQuantBuilder
    .Create(dim: 128)             // head dimension
    .WithKeyBits(4)               // keys: 4-bit (attention precision)
    .WithValueBits(2)             // values: 2-bit (more aggressive)
    .WithHadamardRotation()      // fast path for the power-of-two head dimension
    .WithStatistics(false)
    .WithResidualWindow(tokens: 128)
    .BuildKVCache();

// Append during generation. Recent tokens stay in float32,
// older tokens are automatically compressed
window.Append(keyVector, valueVector);

// Retrieve for attention computation
// Allocate once, then reuse these buffers while the context size stays the same.
var keys = new float[window.TotalCount * cache.Dimension];
var values = new float[keys.Length];
window.CopyKeysTo(keys);       // contiguous rows: recent exact, older decompressed
window.CopyValuesTo(values);

// Read only the range needed by the caller, including across the residual boundary.
window.CopyKeysTo(startIndex: 0, count: Math.Min(32, window.TotalCount), destination: keys);

The compressed cache also provides CopyKeysTo / CopyValuesTo, single-entry span dequantization, and allocation-free GetKey(index) / GetValue(index) access to packed vectors. The existing GetAllKeys / GetAllValues APIs return independent snapshots. The residual window uses preallocated circular buffers; a zero-sized window compresses immediately. TrimToWindow releases old packed arrays without shifting retained entries. Cache mutation requires external synchronization when shared between threads.

32 layers x 32 heads x 4K tokens x 128d: 32 GB → ~6 GB

Microsoft Agent Framework: Compressed RAG

using Microsoft.Agents.AI;
using Microsoft.Agents.AI.ChatCompletion;
using TurboQuant;

// Compressed document store
var quantizer = TurboQuantBuilder.Create(dim: 768).WithBits(4).BuildMSE();
var docs = new Dictionary<string, (PackedVector Vec, string Title, string Text, string Link)>();

// Index documents (once)
foreach (var doc in documents)
{
    var embedding = await embeddingModel.GenerateAsync(doc.Text);
    docs[doc.Id] = (quantizer.Quantize(embedding), doc.Title, doc.Text, doc.Link);
}

// Search adapter: quantize query, find top matches via ApproxSimilarity
Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchAdapter(
    string query, CancellationToken ct)
{
    var queryEmbedding = embeddingModel.Generate(query);
    var queryPacked = quantizer.Quantize(queryEmbedding);

    var results = docs.Values
        .Select(d => (d, Score: quantizer.ApproxSimilarity(queryPacked, d.Vec)))
        .OrderByDescending(x => x.Score)
        .Take(5)
        .Select(x => new TextSearchProvider.TextSearchResult
        {
            Text = x.d.Text,
            SourceName = x.d.Title,
            SourceLink = x.d.Link
        });

    return Task.FromResult(results);
}

// Create agent with compressed RAG
var agent = azureOpenAIClient
    .GetChatClient(deploymentName)
    .AsAIAgent(new ChatClientAgentOptions
    {
        ChatOptions = new() { Instructions = "Answer questions using the provided context." },
        AIContextProviders = [new TextSearchProvider(SearchAdapter, new()
        {
            SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke
        })]
    });

100K documents (768d): 292 MB → 37 MB of packed payload and norms in the search adapter.

Semantic Kernel: Compressed Memory

public class CompressedMemoryStore : IMemoryStore
{
    private readonly TurboQuantMSE _quantizer;
    private readonly ConcurrentDictionary<string,
        ConcurrentDictionary<string, (PackedVector Vec, MemoryRecord Rec)>> _collections = new();

    public CompressedMemoryStore(int dim) =>
        _quantizer = TurboQuantBuilder.Create(dim).WithBits(4).BuildMSE();

    public Task<string> UpsertAsync(string collection, MemoryRecord record, CancellationToken ct)
    {
        var store = _collections.GetOrAdd(collection, _ => new());
        var packed = _quantizer.Quantize(record.Embedding.Vector.ToArray());
        store[record.Metadata.Id] = (packed, record);
        return Task.FromResult(record.Metadata.Id);
    }

    public Task<MemoryRecord?> GetNearestMatchAsync(
        string collection, ReadOnlyMemory<float> query, double minScore, CancellationToken ct)
    {
        if (!_collections.TryGetValue(collection, out var store))
            return Task.FromResult<MemoryRecord?>(null);

        var qPacked = _quantizer.Quantize(query.Span);
        return Task.FromResult(store.Values
            .Select(d => (d.Rec, Score: (double)_quantizer.ApproxSimilarity(qPacked, d.Vec)))
            .Where(x => x.Score >= minScore)
            .MaxBy(x => x.Score).Rec);
    }
}

ML.NET Pipeline

var quantizer = TurboQuantBuilder.Create(dim: 384).WithBits(4).BuildMSE();

var pipeline = mlContext.Transforms.CustomMapping<Input, Output>(
    (input, output) =>
    {
        var packed = quantizer.Quantize(input.Features);
        output.Compressed = quantizer.Dequantize(packed);
    },
    contractName: "TurboQuant");

ONNX Runtime: Post-Inference Compression

using var session = new InferenceSession("embedding_model.onnx");
var quantizer = TurboQuantBuilder.Create(dim: 768).WithBits(4).BuildMSE();

using var results = session.Run(inputs);
var outputTensor = results.First().AsEnumerable<float>().ToArray();

var compressed = new PackedVector[batchSize];
quantizer.QuantizeBatch(outputTensor, compressed);

Serialization

// Save
var bytes = packed.ToBytes();
File.WriteAllBytes("vector.bin", bytes);

// Load
var loaded = PackedVector.FromBytes(File.ReadAllBytes("vector.bin"));
var restored = quantizer.Dequantize(loaded);

Rotation Strategies

TurboQuant defaults to random orthogonal rotation (QR decomposition of a Gaussian matrix), which matches the paper exactly and works with any dimension without overhead.

For power-of-2 dimensions (128, 256, 512, 1024), the Hadamard rotation is an excellent alternative: identical quality, O(d log d) time, and O(d) memory instead of O(d^2).

For non-power-of-2 dimensions, Hadamard pads internally to the next power of 2. This adds 33% for 384, 768 and 1536 dimensions, and up to nearly 100% for arbitrary dimensions. A Trace.TraceWarning is emitted when padding is needed.

Quantizers created through the builder or the seeded MSE constructor share immutable rotations for the same dimension, seed and rotation strategy while those rotations remain in use. Weak references allow large matrices to be collected after their quantizers are released. Explicitly supplied custom rotations are used as provided.

// Default: random orthogonal (paper-correct, any dimension)
var quantizer = TurboQuantBuilder.Create(dim: 768).WithBits(4).BuildMSE();

// Fast path for power-of-2 dimensions (recommended for KV cache with head dim 64/128)
var fast = TurboQuantBuilder.Create(dim: 1024).WithBits(4).WithHadamardRotation().BuildMSE();
Random Orthogonal (default) Hadamard
Time O(d^2) O(d log d)
Memory O(d^2) per distinct rotation O(d) per distinct rotation
Packed size Exact (d coordinates) Exact if power-of-2, padded otherwise
D_mse = theoretical = theoretical (power-of-2), better (non-power-of-2)

Best practice: if your embedding model supports configurable dimensions (e.g. OpenAI, nomic), choose a power-of-2 dimension (512, 1024) to get the best of both strategies.

Note on seed correlation: the rotation matrix is generated from a random seed. If your test vectors are generated from the same seed (e.g. new Random(42) for both), the rotation and data become correlated, causing degraded quality. This does not happen in practice (embeddings come from a model, not from Random), but use distinct seeds in synthetic benchmarks.

How It Works

                    ┌──────────────┐
   float[768]  ──>  │  Normalize   │ store ||x||
                    └──────┬───────┘
                           v
                    ┌──────────────┐
                    │   Rotate     │ random orthogonal (default) or Hadamard
                    └──────┬───────┘
                           v
                    ┌──────────────┐
                    │  Quantize    │ per-coordinate Lloyd-Max on Beta(d/2, 1/2)
                    └──────┬───────┘
                           v
                    ┌──────────────┐
                    │  Bit-Pack    │ 4-bit: 2 values/byte, 3-bit: 8 values/3 bytes
                    └──────┬───────┘
                           v
                    PackedVector (data + norm)

The rotation ensures each coordinate is approximately Beta-distributed regardless of input, enabling provably near-optimal scalar quantization. D_mse is within factor sqrt(3)pi/2 ~ 2.7x of the information-theoretic lower bound (Theorem 3 in the paper).

ApproxSimilarity computes cosine similarity directly from precomputed products and squared norms of packed groups. It supports 2/3/4-bit data with zero scratch allocation after the shared lookup tables are initialized. Packed metadata must match the quantizer; serialized vectors must be read with the original rotation configuration.

Project Layout

src/TurboQuant/
  Core/
    Rotation/       RandomRotation (default, paper-correct), HadamardRotation (fast, power-of-2)
    Codebook/       BetaCodebook (Lloyd-Max on exact Beta distribution, with LUT)
    Packing/        BitPacker (2/3/4-bit), PackedVector (serializable, immutable)
    Quantizers/     TurboQuantMSE, TurboQuantAsymmetric
    Simd/           DotProductSimd (Vector<T> auto-ISA: NEON/SSE/AVX2/AVX-512)
  Cache/            KVCache, ResidualWindow
  Diagnostics/      CompressionStats (thread-safe), QuantizationBenchmark
  TurboQuantBuilder.cs

Build & Test

dotnet build TurboQuantNet.slnx
dotnet test
dotnet run --project samples/TurboQuant.Demo
dotnet run -c Release --project benchmarks/TurboQuant.Benchmarks

The benchmark runner accepts filters and job options. For example:

dotnet run -c Release --project benchmarks/TurboQuant.Benchmarks -- --list flat
dotnet run -c Release --project benchmarks/TurboQuant.Benchmarks -- --filter '*StatisticsBenchmarks*' --job short
dotnet run -c Release --project benchmarks/TurboQuant.Benchmarks -- --filter '*CacheReadBenchmarks*' --job short

The suite covers all bit widths, both rotations, variable input vectors, buffer-based dequantization, sequential/parallel batches, statistics overhead, rotation reuse and KV cache reads/appends. Run on each target CPU architecture before making throughput claims.

License

MIT. Based on the paper by Zandieh, Daliri, Hadian & Mirrokni (Google Research / NYU / Google DeepMind).

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0 100 10/17/2026
0.2.0 500 4/12/2026
0.1.1 140 4/3/2026