Memory Optimization

4 posts

google3 min readCurated summary

TurboQuant: Redefining AI efficiency with extreme compression

TurboQuant is a quantization framework designed to dramatically reduce memory use in large language models and vector search without sacrificing accuracy. It combines PolarQuant’s efficient vector compression with QJL’s one-bit residual correction to eliminate the overhead found in traditional quantization. Experiments show that it can compress KV caches to 3 bits, reduce memory by at least 6×, and accelerate attention-logit computation by up to 8×. ## The Memory Challenge in AI - High-dimensional vectors power language understanding, image features, vector search, and model attention. - These vectors consume substantial memory, particularly in the key-value (KV) cache used to store frequently accessed attention information. - Traditional vector quantization reduces vector size but often requires full-precision scaling or normalization constants for each block. - This metadata can add one or two bits per value, undermining the benefits of compression. ## TurboQuant’s Two-Stage Approach - TurboQuant first applies a random rotation to simplify the geometry of the data. - PolarQuant then compresses the transformed vectors using a standard quantizer, dedicating most bits to the vector’s primary information. - A remaining single bit is used by QJL to encode residual error. - QJL removes bias from the initial compression, improving the accuracy of attention-score calculations. - The approach requires no model training or fine-tuning. ## QJL: One-Bit Error Correction - QJL builds on the Johnson-Lindenstrauss Transform, which preserves important distances and relationships in high-dimensional data. - It represents each transformed value using only its sign: +1 or −1. - A specialized estimator combines low-precision stored data with a high-precision query. - This preserves accurate attention scores while introducing effectively zero memory overhead. ## PolarQuant: Compression Without Metadata Overhead - PolarQuant converts vectors from Cartesian coordinates into polar coordinates. - Instead of separately storing coordinate values, it represents vectors through: - A radius, capturing magnitude or signal strength - Angles, capturing direction and semantic structure - Because angular values follow a predictable, concentrated distribution, PolarQuant avoids expensive per-block normalization constants. - It recursively groups coordinate pairs and transforms their radii until the vector becomes one final radius plus a collection of angles. - This produces a compact representation with fixed, predictable boundaries. ## Experimental Results - The methods were tested on LongBench, Needle In A Haystack, ZeroSCROLLS, RULER, and L-Eval using Gemma and Mistral models. - TurboQuant achieved strong dot-product distortion and recall results while minimizing KV-cache memory. - On needle-in-a-haystack tasks, TurboQuant maintained perfect downstream performance while reducing KV memory by at least 6×. - PolarQuant was also nearly lossless on these tasks. - TurboQuant compressed KV caches to 3 bits without accuracy degradation. - Quantized models ran faster than the original uncompressed models. - On H100 GPUs, 4-bit TurboQuant delivered up to an 8× speedup for attention-logit computation compared with 32-bit keys. - The method has negligible runtime overhead and is relatively simple to implement. TurboQuant is presented as a practical way to make long-context LLMs and large-scale vector search more memory-efficient. Its combination of metadata-free PolarQuant compression and one-bit QJL correction is especially promising for deployments constrained by KV-cache capacity, latency, or GPU memory.

Read original(opens in new tab)
datadog3 min readCurated summary

How Go 1.24's Swiss Tables saved us hundreds of gigabytes

Go 1.24 initially caused a Go runtime regression that increased RSS across Datadog services, but some high-traffic workloads ultimately used substantially less memory. The reduction came from Go 1.24’s Swiss Tables map implementation, which made a large, mostly read-only routing cache more compact. Profiling also revealed opportunities to reduce memory further by removing redundant data from the cached values. ## The Unexpected Memory Reduction - Datadog observed roughly **500 MiB less live heap** in the `shardRoutingCache` map after upgrading to Go 1.24. - With `GOGC=100`, that translated to approximately **1 GiB less total memory usage**. - Even after accounting for an expected **400 MiB RSS increase** from the `mallocgc` regression, the service achieved a net reduction of about **600 MiB**. - The improvement was most visible in high-traffic environments because they contained larger routing caches. ## The `shardRoutingCache` Data Structure - The cache maps routing keys to shard information: ```go map[string]Response ``` - Each `Response` contains: - `ShardID int32` - `ShardType` - `RoutingKey string` - `LastModified *time.Time` - The map is populated mainly during service startup by querying a database. - It is rarely modified afterward, making its memory layout and initial allocation particularly important. - The routing key is stored both as the map key and again inside the value, creating potential redundancy. ## Estimating Memory per Entry - On a 64-bit system, a map key’s string header occupies **16 bytes**. - The value requires approximately: - 4 bytes for `ShardID` - 8 bytes for `ShardType` - 16 bytes for the `RoutingKey` string header - 8 bytes for the `LastModified` pointer - The value totals 36 bytes before alignment, or roughly **40 bytes with padding**. - Including the key header, each key-value pair requires about **56 bytes**, excluding the separately allocated string and `time.Time` data. ## Go 1.23 Bucket-Based Maps - Go 1.23 maps used hash tables organized into an array of buckets. - The number of buckets was always a power of two, and each bucket contained **eight slots**. - Reads and writes required scanning the slots in the selected bucket to find a matching key or an empty position. - When a bucket filled, Go added linked overflow buckets, which increased memory usage and made lookups more expensive. - Map growth occurred when the average load factor exceeded **13/16, or 6.5 of 8 slots**. - The map then allocated twice as many buckets. - To avoid a large latency spike, growth was incremental: old and new bucket arrays coexisted while entries were gradually moved during subsequent writes. ## Why Workload Shape Matters - The routing cache is populated in a startup-heavy phase and then primarily read. - Such a workload benefits from a compact map representation because it does not need frequent insertions or growth. - Differences in cache size and traffic patterns explain why the memory improvement was significant in some environments but not uniform across the fleet. Go 1.24’s Swiss Tables implementation can substantially reduce memory usage for large, stable maps, even when another runtime change causes RSS growth. Teams should profile real production heaps after Go upgrades and inspect large structs for duplicated strings, unnecessary pointers, and other avoidable per-entry overhead.

Read original(opens in new tab)
figma2 min readCurated summary

Supporting Faster File Load Times with Memory Optimizations in Rust | Figma Blog

Figma improved server-side file loading by reducing the memory overhead of its Rust data structures. Replacing per-node `BTreeMap`s with compact sorted vectors made deserialization faster and cut memory usage for large files by nearly 25%, despite worse theoretical operation complexity. The team also explored packing field IDs into unused pointer bits, potentially storing the same information in fewer bytes. ## Smaller, Memory-Efficient Maps - Figma files consist of nodes, each represented by properties such as type, parent, position, and dimensions. - Nodes were stored as `BTreeMap<u16, u64 pointer>` structures because ordered iteration was required for serialization. - Profiling showed these maps consumed more than 60% of a file’s memory, even though they stored metadata rather than large data payloads. - The schema contains fewer than 200 possible fields, and nodes typically contain only a subset of them—about 60 properties on average. - Figma replaced each `BTreeMap` with a sorted flat vector of `(field ID, pointer)` pairs. - Although vectors have theoretically slower insertion, lookup, and editing, their compact linear layout is more cache-friendly and faster during deserialization. - The deployed change reduced memory usage by nearly 25% for large files and improved file-loading performance. ## Saving More Memory with Bit Stuffing - The team also investigated storing the field ID inside the pointer itself. - While pointers are nominally 64 bits, x86 systems currently use only the lower 48 bits for memory addresses, leaving 16 bits available. - Figma’s field IDs require exactly 16 bits, allowing a single `u64` to contain both: - A 16-bit field ID - A 48-bit memory pointer - This representation could eliminate the separate field-ID storage and further reduce memory overhead. - The approach had not yet been productionized because relying on unused pointer bits is architecture-dependent and could change in the future. Figma’s results demonstrate that practical memory layout and CPU cache behavior can outweigh Big O complexity. For compact, bounded data structures, flat vectors—and carefully considered bit packing—can deliver substantial improvements in both memory efficiency and load speed.

Read original(opens in new tab)
figma2 min readCurated summary

Improving Performance with Incremental Frame Loading | Figma Blog

Figma redesigned its prototype player because loading entire documents caused slow startup times, high memory use, and frequent mobile crashes. Its incremental frame loading strategy loads only the currently visible frame and nearby reachable frames, then fetches more as users navigate. This improves time to interactive while reducing the amount of prototype data held in memory. ## Problems with Full-Document Loading - The original system loaded the entire prototype document into memory before displaying the starting screen. - As Figma files grew to include more pages, design systems, and component variants, prototypes became substantially larger. - Mobile devices, particularly iPhones, often exceeded their memory limits, causing the operating system to terminate Figma. - Large prototypes could take minutes to load, creating both performance and stability problems. ## Loading Frames Incrementally - Figma defined “incremental frame loading” as loading only the prototype content needed at a given moment. - The initial load includes: - The first frame. - Frames immediately reachable through prototype interactions. - When a user navigates to another frame, Figma loads that frame’s adjacent destinations. - Previously loaded frames remain available so users can navigate backward without reloading them. - This approach reduces both startup time and peak memory consumption. ## Adapting Multiplayer Document Sync - Implementing partial loading required extending Figma’s real-time multiplayer system to synchronize only selected portions of a document. - The system had to support querying specific subtrees instead of always synchronizing the complete file. - A client sends a `query` identifying a document node. - The server returns a `reply` containing the requested subtree, including its ancestors and descendants. - Later modifications to the subscribed content are delivered through `changes` messages. ## Handling Dynamic Document Changes - The protocol continues to work while designers edit the source file in real time. - Changes to subscribed nodes are sent directly to clients. - If a node is moved under a subscribed node, the client is informed about the newly available node. - If a subscribed node is moved outside the client’s subscribed descendants, the client receives a removal update. - This keeps the partially loaded prototype consistent with the live document while avoiding unnecessary data transfer. Figma’s solution combines navigation-aware preloading with fine-grained real-time synchronization. Loading only the frames users need provides a practical way to make large prototypes faster and more reliable, especially on memory-constrained mobile devices.

Read original(opens in new tab)