Skip to main content
← Blog

Accelerating Model Inference with On‑the‑Fly Snapshot Compression

Contents

    Checkpoint/restore is already being applied to model serving: Dynamo Snapshot1 uses CRIU and cuda-checkpoint to fast-start GPU workers in Kubernetes; Cloudburst2 uses warmed SGLang snapshots to avoid repeated server initialization; and SwapServeLLM3 uses transparent GPU checkpointing to hot-swap models as demand changes.

    Memory compression has been a topic the CRIU community has been exploring over the past few years. A straightforward approach has been to compress the checkpoint data after it is written to disk (as a compressed tar file). This simple approach is inefficient because it requires saving the full uncompressed checkpoint, reading it back for compression, and then writing the compressed snapshot. Although this overhead may be acceptable for applications with small memory footprints, the additional disk I/O, required temporary storage, and compression time become increasingly costly for memory-intensive workloads, such as LLM inference engines.

    We set out to address these challenges by designing a solution that integrates compression directly into CRIU’s checkpoint and restore pipeline. Instead of performing compression as a post-processing step, our approach compresses memory pages as they are extracted from the target process and writes them directly into the checkpoint image. Similarly, the memory pages are decompressed on the fly as they are being restored. This streaming approach eliminates the need for intermediate storage of uncompressed checkpoint data, reducing both I/O overhead and storage requirements.

    Earlier compression approaches

    “This could (or should) be implemented in the future” was my answer back in 20184 when a user asked whether CRIU compressed its image files.

    Compressing memory in JVM checkpoints

    OpenJDK’s Coordinated Restore at Checkpoint (CRaC) project uses CRIU to capture an initialized Java Virtual Machine (JVM), allowing Java applications to restart without repeating their initialization work. A CRIU fork added image compression that runs after the dump: it compresses complete pages-*.img files with LZ4 and replaces the original raw images.

    Before restore, the fork expands those images into temporary files under /tmp, then restores the process from the raw copies. This reduces retained checkpoint size, but adds full-image I/O, temporary storage, and decompression to the restore path. Those costs grow with the process’s memory footprint, making the approach less practical for applications with hundreds of gigabytes of memory state.

    Representing zero-filled memory without storing data

    The proposed --skip-zero-pages option took a narrower approach. During checkpoint, CRIU would detect pages containing only zero bytes and omit their payloads from the page image. During restore, it would fill the omitted pages with zeroes instead of reading their data from storage.

    This is particularly useful for Java applications because the JVM often allocates large memory regions without immediately using every page. The proposal did not land upstream, but it demonstrated that CRIU could reduce both stored bytes and page-image I/O by representing zero-filled pages without always writing a full payload.

    Compressing checkpoint data before end-to-end encryption

    Compression also reduces the work required for end-to-end encryption. Container checkpoints include process memory that may contain cryptographic keys, access tokens, passwords, and other confidential data. Compressing that memory before encryption reduces the bytes that must be encrypted, transferred, and stored without moving the confidentiality boundary. CRIUsec, presented at Linux Plumbers Conference 2023,5 integrated encryption of CRIU image files and memory pages into the checkpoint/restore pipeline rather than a separate file-processing pass. We later presented its use in Kubernetes clusters at CloudNativeSecurityCon 20246 and described the design in an APSys 2024 paper,7 which identified integrated compression as an important next step.

    Reducing LLM inference cold starts

    Reusing initialized runtime state

    An LLM replica is not ready after its model weights have merely arrived. Python must import packages, the inference engine must create memory pools, kernels may be compiled or autotuned, and CUDA graphs may be captured. A cold start includes that initialization and ends only when the new replica can serve a valid request.

    Checkpoint/restore removes most of that repeated work from the cold-start path. The platform initializes the server once, captures its warmed state, and later restores new replicas from that checkpoint. Initialization becomes data: the checkpoint must be staged, read, and reconstructed quickly enough for the replica to become useful.

    Restoring from compressed memory

    CRIU stores the process’s anonymous and shared memory in pages-*.img. With NVIDIA’s current checkpoint path, GPU memory is first brought into system memory, and CRIU saves the resulting host mappings. CRIU’s compressor therefore operates on host pages rather than reading device memory itself. GPU-aware restore still requires the appropriate vendor support and CRIU plugin.8 For an inference server, these memory images can be many gigabytes.

    Compressing those images reduces the bytes that must be stored, transferred, and read during scale-out. That can shorten a cold start when moving snapshot data costs more than LZ4 decompression. It can regress when storage is already fast and decompression becomes the bottleneck, so a smaller checkpoint alone does not prove a faster replica start.

    Compressing a completed checkpoint archive with gzip or zstd saves capacity, but adds a separate pass around CRIU’s memory path and may require compressed and raw artifacts to coexist. CRIU’s integrated path instead compresses pages before they reach storage and decompresses them while restoring the process’s memory mappings.

    Optimizing cold-start latency

    CRIU memory restore is only one interval within an LLM cold start. A production timeline begins when the platform launches a replica and ends when that replica returns its first valid inference response. Between those points, the platform may fetch or import the checkpoint, create the container, restore CPU memory, reconstruct GPU state, wait for service health, and submit the first request.

    The figure below isolates the CRIU memory-image portion of that path:

    CRIU Data Path

    One transform during checkpoint, three paths during restore

    Follow page data into the checkpoint, then use the controls to inspect how restore handles each stored representation.

    CRIU compressed memory checkpoint and restore pipelineDuring checkpoint, process memory passes through the page pipe and block classifier. Non-zero block bytes go to pages image files while block sizes and geometry go to pagemap metadata. During restore, CRIU validates the metadata, plans bounded batches, and reads only stored bytes. A zero block reads no payload, a raw block reads its full in-memory block size, and an LZ4 block reads fewer stored bytes before decompression. Every path reconstructs the same block size in the restored process memory. CHECKPOINT RESTORE 1 Process memory process mappings 2 Page pipe pages + runs 3 Classify / compress zero · raw · LZ4 4 pages-*.img raw or LZ4 memory blocks total stored bytes read N read S 3 Plan bounded batch classify · split · destinations pages-*.img stored block byte stream 4 Read stored bytes sum(block_sizes) ZERO S = 0 read 0 · fill N RAW S = N read N · copy N LZ4 0 < S < N read S · decompress N 5 Restored memory N-byte block Per block: S = stored bytes · N = bytes in memory metadata / control image read memory write

    Checkpoint

    1. Process memorycheckpointed mappings
    2. Page pipepages + runs
    3. Classify / compresszero · raw · LZ4
    4. pages-*.imgraw or LZ4 memory blocks

    Restore

    1. Inventory + pagemapmode · S · block geometry
    2. Validate image layoutcounts · sizes · totals · bounds
    3. Plan bounded batchclassify · split · destinations
    4. No payload read

      ZeroS = 0 · fill N

      Payload read

      pages-*.imgstored block byte stream Read stored bytestotal for the planned range
      RawS = N · copy N LZ40 < S < N · decompress N
    5. Restored process memoryN bytes from every path

    Reading the full flow

    Metadata directs reads from the pages image

    For stored size S and in-memory block size N, restore reads 0 bytes for zero, N for raw, or S for LZ4. Every path writes N bytes into restored process memory.

    Dashed arrows show metadata and control, indigo arrows show reads from the page image, and teal arrows show writes into restored process memory. For each block, S is the stored byte count and N is the number of bytes written to restored process memory. Select Checkpoint or Restore to trace a complete phase. Select ZERO, RAW, or LZ4 to follow one restore path. Motion represents operation order, not duration, throughput, or data volume. The checkpoint trace uses a non-zero block: CRIU writes the payload before it flushes the completed pagemap entry. A zero block writes no payload. If every block in an entry remains raw, CRIU may omit its block metadata and retain the ordinary contiguous-page path.

    Those boundaries should be measured separately with one clock. Compare compressed and raw checkpoints with the same model placement, cache state, runtime configuration, and concurrency. Record readiness and first response as different endpoints: a successful health probe does not prove that the complete inference path is ready. Include checkpoint transfer or archive import whenever the production path includes it.

    A smaller memory image is therefore a mechanism, not the cold-start result. Compression is a win only when the complete launch-to-response interval falls. Saved storage I/O can otherwise be offset by decompression CPU or by time in unaffected layers. The evaluation below reports the OCI runtime and CRIU memory-restore phases separately and treats application cold start as distinct context.

    Evaluation

    The latest results come from a baseline compression comparison and a parallel block-size comparison on an AMD EPYC 9335 host with an NVIDIA RTX PRO 6000 Blackwell Server Edition. Both benchmark sets ran SGLang under Podman with a development build of CRIU, one warm-up, and five measured trials per condition. Podman’s outer archive compression was disabled. The compressed condition used acceleration 1. The baseline compared compression disabled with 256 KiB blocks and serial decompression. The parallel comparison tested 4 KiB, 256 KiB, 512 KiB, and 1 MiB blocks with a 16-thread restore limit.

    Restore latency across the serving stack

    The baseline comparison contrasts CRIU page-image compression disabled with 256 KiB LZ4 blocks and serial decompression. Every value is the median of five measured trials after one warm-up. The complete container-restore and TTFT intervals include archive import. The OCI runtime and CRIU figures isolate the nested restore layers reported by Podman's --print-stats output.

    Checkpoint size and container restore latency
    ModelCompression offLZ4, 256 KiBSavedRestore latency: offRestore latency: LZ4
    Qwen 3.5 4B22.35 GiB12.17 GiB45.5%42.04 s22.68 s
    Qwen 3.5 9B31.49 GiB21.21 GiB32.6%65.79 s50.27 s
    Qwen 3.5 27B65.01 GiB54.68 GiB15.9%152.62 s134.54 s
    Qwen 3.6 35B-A3B79.39 GiB68.77 GiB13.4%200.00 s181.29 s
    Gemma 4 26B-A4B57.72 GiB50.53 GiB12.5%127.33 s118.26 s
    Container restore latency. LZ4 reduced checkpoint size and complete container restore time for every accepted model. This interval includes archive import, storage preparation, and container reconstruction.
    OCI runtime data
    ModelCompression offLZ4, 256 KiBChange
    Qwen 3.5 4B4.40 s6.70 s+52.3%
    Qwen 3.5 9B5.80 s8.11 s+39.9%
    Qwen 3.5 27B28.86 s27.56 s−4.5%
    Qwen 3.6 35B-A3B41.50 s35.66 s−14.1%
    Gemma 4 26B-A4B25.02 s22.80 s−8.9%
    OCI runtime. Serial LZ4 decompression increases the runtime interval for Qwen 4B and 9B. Reading fewer bytes wins for the three larger memory images.
    CRIU memory restore data
    ModelCompression offLZ4, 256 KiBChange
    Qwen 3.5 4B2.62 s4.88 s+85.7%
    Qwen 3.5 9B3.25 s5.73 s+76.2%
    Qwen 3.5 27B23.25 s21.84 s−6.1%
    Qwen 3.6 35B-A3B32.33 s29.39 s−9.1%
    Gemma 4 26B-A4B19.60 s17.71 s−9.6%
    CRIU memory restore. CRIU's internal restore_time is contained within the OCI runtime. It exposes the same crossover between decompression CPU and reduced page image I/O.

    At the complete container-restore boundary, 256 KiB LZ4 reduced restore time for all five accepted models by 7.1% to 46.1%. The nested phases varied by model. For Qwen 4B and 9B, serial decompression increased both the OCI runtime and CRIU memory-restore durations, but the smaller checkpoint still reduced container restore latency. For Qwen 27B, Qwen 35B-A3B, and Gemma 26B-A4B, LZ4 reduced both nested durations because the saved image I/O exceeded the decompression cost.

    Cold-start TTFT in context

    Cold-start TTFT, OCI runtime, and checkpoint size
    ModelCold-start TTFTCompression offLZ4, 256 KiB
    OCI runtimeCheckpoint sizeOCI runtimeCheckpoint size
    Qwen 3.5 27B121.90 s28.86 s65.01 GiB27.56 s54.68 GiB
    Qwen 3.6 35B-A3B151.74 s41.50 s79.39 GiB35.66 s68.77 GiB
    Gemma 4 26B-A4B120.36 s25.02 s57.72 GiB22.80 s50.53 GiB
    Unlike request-only TTFT, cold-start TTFT includes startup. It spans container launch through the first generated token and pools ten observations across both compression runs. OCI runtime is a nested five-observation interval that excludes archive import, application recovery, and inference. Checkpoint-size labels are annotations. Model files were prefetched, and the host page cache remained warm.

    Qwen 4B and 9B reached the first token in 61.56 and 63.10 seconds, respectively. They are omitted from the chart to keep its focus on larger memory images.

    Optimizing block size for parallel decompression

    Larger blocks give LZ4 more history and amortize per-block metadata and codec calls, but they make partial reads coarser and can expose fewer independent jobs to restore workers. This comparison holds the model and 16-thread limit constant while varying only the block size. Each value is the median of five accepted Qwen 3.5 4B observations.

    Qwen 3.5 4B with at most 16 restore threads
    Metric4 KiB256 KiB512 KiB1 MiB
    Checkpoint size (GiB)12.95812.17312.15812.152, lowest measured value
    Checkpoint time (s)58.5754.1952.4951.30, lowest measured value
    Container restore latency (s)25.9122.3923.0519.82, lowest measured value
    OCI runtime (s)6.135.055.104.97, lowest measured value
    CRIU memory restore (s)4.253.163.313.11, lowest measured value
    Restore-inclusive TTFT (s)27.1423.6224.2921.05, lowest measured value
    Bold marks each row's lowest median. The 1 MiB result suggests that this checkpoint still provided enough independent work for the restore threads, so coarser blocks had not yet reduced useful parallelism.

    Increasing the block from 256 KiB to 1 MiB reduced checkpoint size from 12.173 to 12.152 GiB, only 0.17%. CRIU accepts blocks up to 4 MiB, but this benchmark did not measure 2 MiB or 4 MiB. Measurements at those two sizes are needed to determine whether larger blocks reduce runtime overhead or expose too few independent decompression jobs.

    At 256 KiB, the 16-thread run recorded 3.16 seconds for the CRIU memory restore phase and 5.05 seconds for the OCI runtime, compared with 4.88 and 6.70 seconds in the serial baseline. Container restore latency decreased by only 0.29 seconds, from 22.68 to 22.39 seconds. Because these measurements came from separate benchmark sets rather than a controlled worker-count comparison, they suggest an effect from parallel decompression but do not isolate it.

    The benchmark validates a deterministic response before and after every successful restore. All 50 measured baseline observations and all 20 measurements in the complete Qwen 4B block-size comparison passed. This is a correctness and readiness check, not a throughput, tail-latency, or concurrent-load result.

    Compression improves restore latency only when the time avoided by reading fewer image bytes exceeds the additional decompression work.

    On an I/O-bound image path, the reduction in read time can dominate. On tmpfs, a hot page cache, or very high-bandwidth local storage, decompression may instead become the bottleneck. The benchmark metadata did not record the storage path, so these results locate the trade-off for that host rather than proving which storage regime produced it.

    Compressed LZ4 blocks use buffered I/O, so --image-io-mode direct does not make the compressed path use direct I/O. Deployments that depend on direct I/O should benchmark the compressed path separately.9

    On-the-fly memory compression

    At Linux Plumbers Conference 2025,10 we presented the first prototype of CRIU-LZ4. In contrast to previous approaches, which compressed checkpoint images as a post-processing step, CRIU-LZ4 integrated LZ4 directly into the checkpoint/restore pipeline by extending CRIU’s pagemap metadata and image format to support compressed memory. The prototype also demonstrated that it was possible to checkpoint and restore a workload occupying approximately 75% of the available memory entirely within an in-memory filesystem, illustrating that built-in compression can make memory-intensive checkpoints practical even without backing storage. During the Q&A,11 Andrei Vagin suggested retaining compression only when it provides a worthwhile reduction. That review shifted the design from compressing every page to choosing the most useful representation for each block, as described below.

    Using LZ4 with restorer context

    The restorer context is a minimal, self-contained execution environment that CRIU copies into a temporary location before the final stage of process restoration. It executes outside the target process’s address space, allowing CRIU to recreate the original memory mappings and transfer control without overwriting its own execution state. CRIU compiles this context as position-independent code (PIE) without access to the dynamic loader or shared libraries, so it cannot simply link against liblz4. Any decompression logic executed there must therefore be built into the restorer blob or be completely self-contained.

    An earlier prototype used a helper process, but review showed that compressed pages could be restored through the normal page reader before PIE.12 The restore path premaps eligible private VMAs containing real LZ4 blocks and decompresses them through the normal page reader before PIE. Raw and zero ranges can retain delayed PIE restore. Hugetlb and external-plugin mappings, which cannot use the generic premap path, are kept raw or zero.

    Encoding memory blocks

    CRIU now offers two ways to divide memory into LZ4 blocks:

    • --compress encodes each system page independently. On the x86 benchmark host, that means one 4 KiB LZ4 block per page.
    • --compress-block SIZE groups consecutive pages and encodes the group as one block. The size must be page-aligned and no larger than 4 MiB.

    Per-page mode preserves fine-grained access and works across the widest set of CRIU paths. Region mode gives LZ4 a larger history, so it can find repetitions that cross page boundaries and amortize the call and metadata overhead. The price is coarser partial reads: asking for one page can require decoding its whole region.

    For either mode, every block has one of three representations in the pagemap schema:

    compressed_sizeMeaningPayload in pages-*.img
    0the block is all zeroesno bytes
    uncompressed block sizeraw fallbackthe original bytes
    any smaller valueLZ4that many compressed bytes

    The writer first detects an all-zero block. Otherwise, it runs LZ4 and keeps the result only when it is smaller than seven eighths of the original. A weakly compressible page is stored raw, avoiding decode work for a marginal saving. If every block in a pagemap entry falls back to raw, CRIU drops the compression metadata for that entry and leaves restore’s ordinary contiguous fast path available. The final dump path therefore avoids expanding the page payload for incompressible data, although the compression attempt, alignment, and surrounding metadata are not free.

    The metadata has to solve two different problems. compressed_size[] locates individual blocks by describing their variable lengths. total_compressed_size lets the reader skip a complete pagemap entry without summing the array. region_pages says whether one array element describes one page or a group of pages. The inventory records the overall mode and region size, so restore detects the representation automatically.

    Restoring incremental checkpoint chains

    An incremental checkpoint stores only pages that changed since its parent. During restore, an inherited range is passed to the parent reader, which can recurse through further generations. Each reader interprets its own pagemap, so ordinary, ZERO, RAW, and LZ4 entries can coexist in one chain without expanding the checkpoints first. The readers share one encoded-read context, keeping decompression batches within the same restore-wide working-set limit.

    Large blocks can amplify sparse parent reads because restoring one inherited page may require decompressing its complete block. CRIU batches copy-on-write comparison reads in groups of at most 256 pages and aligns them to block boundaries when possible. If a request still covers only part of an LZ4 block, the parent reader caches that block. Later slices can then be copied from the cache instead of reading and decompressing the same parent block again.

    Restoring variable-length blocks

    CRIU does not simply read pages-*.img from beginning to end. It reconstructs private mappings, shared memory, memfd regions, copy-on-write relationships, and pages inherited from earlier checkpoints. A request can start in the middle of a pagemap entry, and a region can cross destination iovec boundaries.

    The reader handles this in four steps:

    1. Resolve the requested blocks and validate their counts, sizes, offsets, and expected decoded length before those values drive allocations.
    2. Read one packed payload for a bounded batch.
    3. Copy raw blocks and fill simple zero runs directly, then describe eligible zero and LZ4 blocks as independent jobs.
    4. Decode into the final mappings, using a scratch buffer only when one region spans multiple destination vectors.

    The implementation keeps encoded input batches to at most 32 MiB. At most two such working sets can be active: while workers decode one batch, the caller may prefetch the next payload if it can acquire the second slot without waiting. The details are in the final encoded reader and prefetch path.

    Decompressing blocks in parallel

    --decompress-threads N controls the aggregate worker concurrency used for LZ4 blocks and eligible large zero fills:

    • 1, the default, keeps each decode serial. Separate private, shared-memory, and memfd requests may still make progress independently.
    • 0 asks CRIU to choose a width from its CPU affinity, the number and decoded size of blocks in the batch, and the shared restore CPU budget.
    • A value greater than one is an upper bound, not a promise to start that many threads. CRIU reduces impossible requests to the available CPU count.

    Small batches remain serial. Parallel work needs enough decoded data per active thread to amortize scheduling, and the encoded staging-memory bound is independent of the thread count. These constraints make 0 safe to experiment with, but they do not make it the correct default for a serving node. Eight cores spent shortening one restore may delay eight other replicas or the control plane.

    Automatic width follows sched_getaffinity(), not a cgroup’s CFS CPU quota. For a quota-constrained container with a broad affinity mask, use a cpuset or an explicit cap. The shared worker budget is also local to one CRIU restore. An orchestrator still has to limit aggregate CPU demand across simultaneous restores.

    The baseline comparison used the serial default, while the block-size comparison fixed the upper bound at 16 threads. Because neither benchmark varied the worker count while holding the other variables constant, the results do not isolate the effect of parallel decompression. I would start with 1 under concurrent scale-out and benchmark 0, 2, 4, and 8 only when spare CPU is available and decompression is visible in the restore profile.

    Offline memory compression

    CRIT can convert an existing checkpoint after the process has been dumped. This is not generic archive compression: CRIT understands CRIU’s image format. It rewrites every task and shared-memory pages-*.img/pagemap-*.img pair and updates inventory.img. The result uses the same version 1.2 block format as on-the-fly compression and can be restored directly, without first expanding a temporary copy.

    The conversion requires the Python lz4 package:

    python3 -m pip install lz4
    crit compress checkpoint/
    crit decompress checkpoint/
    

    crit compress validates each pagemap/page-image pair and applies the ZERO, RAW, and LZ4 rules to each present 4 KiB page. It does not regroup an existing image into larger blocks. Non-zero pages in hugetlb and plugin-managed VMAs stay raw because those restore paths cannot consume LZ4 blocks.

    crit decompress accepts page-sized and multi-page blocks, validates the complete image set, and expands each entry into an ordinary contiguous page payload. Both commands stage and synchronize replacements before changing live names, then roll back the complete image set on failure. Originals remain as .bak hard links by default. --in-place omits persistent backups. The operational behavior is documented in the CRIT manual.

    Configuring memory compression

    Memory compression will be included in CRIU 4.3. Until that release, both checkpoint and restore hosts need an LZ4-enabled build from the criu-dev branch. The public options and limits are in the CRIU manual.

    -c is the short form of --compress. It selects one LZ4 block per system page and supports page-server and image-streaming workflows. --compress-block SIZE selects larger blocks for local images. If both options are present, the last one wins.

    First check that CRIU was built with LZ4 support:

    criu check --feature compress
    

    For a local process checkpoint, my starting configuration is:

    mkdir -p checkpoint
    criu dump --tree "$PID" --images-dir checkpoint --compress-block 256K
    criu restore --images-dir checkpoint
    

    The compression choice is stored in inventory.img. The dump flag should not be repeated on restore. Restore-only concurrency can be selected independently:

    criu restore --images-dir checkpoint --decompress-threads 0
    

    For an OCI runtime that invokes CRIU through runc, put the equivalent settings in runc’s CRIU configuration:

    # /etc/criu/runc.conf
    compress-block 262144
    decompress-threads 1
    

    Use compress instead of compress-block in runc.conf to select page-sized compression.

    This is the relevant integration point for Podman and for Kubernetes nodes where the CRI runtime delegates checkpoint and restore to runc. Kubernetes does not expose these CRIU tuning options through its checkpoint API, so provision the configuration on every participating node and verify that its runtime uses the expected runc and CRIU installation.

    Conclusion

    CRIU can now make the compression trade where pages enter and leave the memory image, without creating a second artifact or making a second pass over the checkpoint. ZERO, RAW, and LZ4 representations avoid storing bytes that do not help, while premap restore and bounded parallel decompression keep that choice inside CRIU’s existing memory path.

    The measurements also show that a smaller image does not guarantee lower restore latency. Serial decompression slowed CRIU for the smaller models, while the larger models benefited inside the runtime and the 16-thread Qwen 4B comparison favored 1 MiB blocks on the measured host. Treat any default as a starting point: larger blocks suit local, I/O-bound images, while page-sized blocks preserve page-server, streaming, and lazy-page compatibility. On memory-speed storage, compression may not help. Change the block size, worker budget, or LZ4 acceleration only after profiling the production restore path.

    Acknowledgements

    Thanks to Andrei Vagin and Alexander Mikhalitsyn for their detailed review and design feedback. I am grateful to Viktória Spišaková and Adrian Reber for our long-standing collaboration, including on the work that led to the CRIU-LZ4 prototype and EuroMLSys paper. I also thank my PhD supervisors, Rodrigo Bruno and Wes Armour, for their guidance and support throughout this research. Thanks to Fergus Finn, Debosmit Ray, and the teams at Doubleword and DevZero for their support in bringing this work to production environments.