<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Efrain Garay</title>
    <description>The latest articles on DEV Community by Efrain Garay (@efraingaray).</description>
    <link>https://dev.to/efraingaray</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4086497%2Ff46e5349-16a6-4bd9-b4b4-7361b88f8578.jpg</url>
      <title>DEV Community: Efrain Garay</title>
      <link>https://dev.to/efraingaray</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/efraingaray"/>
    <language>en</language>
    <item>
      <title>ComfyUI vs Forge on a 16 GB GPU: a tie on SDXL, ComfyUI twice as fast when the model does not fit</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Sun, 18 Oct 2026 16:12:46 +0000</pubDate>
      <link>https://dev.to/efraingaray/comfyui-vs-forge-on-a-16-gb-gpu-a-tie-on-sdxl-comfyui-twice-as-fast-when-the-model-does-not-fit-5emf</link>
      <guid>https://dev.to/efraingaray/comfyui-vs-forge-on-a-16-gb-gpu-a-tie-on-sdxl-comfyui-twice-as-fast-when-the-model-does-not-fit-5emf</guid>
      <description>&lt;p&gt;I use ComfyUI for images and video on a 16 GB card and had never put it side by side with Forge on the same machine. So I measured ComfyUI, Forge Neo and the original Forge on an RTX 4070 Ti SUPER with the same files, seeds and settings, diffusers as a baseline, 36 paired images per model and the decision rule written down before the first run.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;ComfyUI&lt;/th&gt;
&lt;th&gt;Forge Neo&lt;/th&gt;
&lt;th&gt;Original Forge&lt;/th&gt;
&lt;th&gt;diffusers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SDXL (fits in 16 GB)&lt;/td&gt;
&lt;td&gt;6.71 s&lt;/td&gt;
&lt;td&gt;6.71 s&lt;/td&gt;
&lt;td&gt;6.76 s&lt;/td&gt;
&lt;td&gt;6.85 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Z-Image Turbo bf16 (does not fit)&lt;/td&gt;
&lt;td&gt;6.75 s&lt;/td&gt;
&lt;td&gt;13.39 s&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;15.81 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen-Image fp8 (does not fit)&lt;/td&gt;
&lt;td&gt;75.7 s&lt;/td&gt;
&lt;td&gt;96.0 s&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Median seconds per image, 36 images each.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;While the model fits, they tie.&lt;/strong&gt; With SDXL the three tools land within 1 % of each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When it does not fit, ComfyUI wins.&lt;/strong&gt; Twice as fast with Z-Image Turbo, and 21 % less time per image with Qwen-Image fp8. Paired bootstrap intervals exclude a tie in both cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The logs point to memory management.&lt;/strong&gt; ComfyUI v0.41 reports dynamic VRAM loading and «0 models unloaded»; Forge Neo logs three «Moving model(s)» lines per image, which add up to a median of 7.2 s with Z-Image, close to the 6.6 s gap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same seed, nearly the same image&lt;/strong&gt; for ComfyUI and Forge Neo. The original Forge has no engine for Z-Image or Qwen-Image; its last commit on main is from June 2025.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wan 2.2 A14B, one clip per tool as a curiosity:&lt;/strong&gt; 877 s against 932 s, not enough to call a winner.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The post also covers what broke before measuring (a ComfyUI cache that faked a 0.05 s first image, a Forge Neo refiner switch that counted noise instead of steps) and the threats to validity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the full measurements, the pre-registered sheet and the scripts → &lt;a href="https://efraingaray.com/en/blog/comfyui-vs-forge-16gb/" rel="noopener noreferrer"&gt;https://efraingaray.com/en/blog/comfyui-vs-forge-16gb/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>stablediffusion</category>
      <category>ai</category>
      <category>gpu</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>NVFP4 vs FP8 vs GGUF on a 16 GB Ada GPU: NVFP4 decodes 2.3x faster and ties BF16 on GSM8K</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Sun, 18 Oct 2026 01:56:53 +0000</pubDate>
      <link>https://dev.to/efraingaray/nvfp4-vs-fp8-vs-gguf-on-a-16-gb-ada-gpu-nvfp4-decodes-23x-faster-and-ties-bf16-on-gsm8k-4573</link>
      <guid>https://dev.to/efraingaray/nvfp4-vs-fp8-vs-gguf-on-a-16-gb-ada-gpu-nvfp4-decodes-23x-faster-and-ties-bf16-on-gsm8k-4573</guid>
      <description>&lt;p&gt;When you type «nvfp4 vs» into Google it completes with «fp8», «gguf», «q4_k_m» and «bf16». The usual answer compares different models quantized by different people. On October 16 NVIDIA released Nemotron Labs Dory 1.0 4B in BF16, NVFP4, MXFP8 and GGUF at once, so I measured the same model in every format on an RTX 4070 Ti SUPER (Ada, 16 GB, no FP4 hardware), with the rules written down before the first run.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Generate (tok/s, batch 1)&lt;/th&gt;
&lt;th&gt;Read 2048-token prompt (tok/s)&lt;/th&gt;
&lt;th&gt;Weights&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;51.0&lt;/td&gt;
&lt;td&gt;7,003&lt;/td&gt;
&lt;td&gt;7.69 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP8 (per tensor, W8A8)&lt;/td&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;78.5&lt;/td&gt;
&lt;td&gt;10,118&lt;/td&gt;
&lt;td&gt;4.67 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MXFP8&lt;/td&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;81.4&lt;/td&gt;
&lt;td&gt;6,061&lt;/td&gt;
&lt;td&gt;4.76 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4&lt;/td&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;116.8&lt;/td&gt;
&lt;td&gt;6,978&lt;/td&gt;
&lt;td&gt;3.28 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GGUF Q5_K_M&lt;/td&gt;
&lt;td&gt;llama.cpp&lt;/td&gt;
&lt;td&gt;58.8&lt;/td&gt;
&lt;td&gt;1,710&lt;/td&gt;
&lt;td&gt;3.42 GiB (file)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Among the vLLM formats, generation follows weight size.&lt;/strong&gt; NVFP4 was 2.3 times faster than BF16. On Ada it runs through a weight-only Marlin kernel: the weights stay small, the math stays in 16 bits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading a long prompt, FP8 wins&lt;/strong&gt;, because it is the only one of these formats whose multiplications Ada does in 8 bits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality, full GSM8K (1319 questions, 8192-token cap):&lt;/strong&gt; BF16 1249, FP8 1241, MXFP8 1250, NVFP4 1238. All three tie with BF16 under the ±3-point rule; their 95 % intervals stay within ±2 points.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token by token it is another story.&lt;/strong&gt; On fixed English and Spanish texts NVFP4 picks BF16's next token 89-90 % of the time, against 94-96 % for FP8 and MXFP8, while perplexity barely moves. MXFP8 is the most faithful to BF16.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The post also covers what broke before measuring (the llama.cpp converter rejects the public checkpoint, a prefix cache that inflated prefill, truncated reasoning being scored as answers) and a mistake the audit caught in my own first fidelity run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the full measurements, the pre-registered sheet and the scripts → &lt;a href="https://efraingaray.com/en/blog/nvfp4-vs-fp8-gguf-ada/" rel="noopener noreferrer"&gt;https://efraingaray.com/en/blog/nvfp4-vs-fp8-gguf-ada/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>machinelearning</category>
      <category>gpu</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>Kandinsky 6.0 on a 16 GB GPU: the official recipe runs out of memory, and only the 29B model passed my Spanish test</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Wed, 14 Oct 2026 18:27:33 +0000</pubDate>
      <link>https://dev.to/efraingaray/kandinsky-60-on-a-16-gb-gpu-the-official-recipe-runs-out-of-memory-and-only-the-29b-model-passed-2lel</link>
      <guid>https://dev.to/efraingaray/kandinsky-60-on-a-16-gb-gpu-the-official-recipe-runs-out-of-memory-and-only-the-29b-model-passed-2lel</guid>
      <description>&lt;p&gt;Kandinsky 6.0 generates five seconds of video &lt;strong&gt;with its audio&lt;/strong&gt;, voices included, from a text prompt (MIT license). I wanted to know two things: does it run on a 16 GB card, and can it speak Spanish?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory.&lt;/strong&gt; On a rented 16 GB RTX 4080, the README recipe (per-component CPU offload) ran out of memory with the distilled Lite model; the Qwen2.5-VL text encoder files alone add up to 15.45 GiB. An 8-bit text encoder and sequential offload failed too. What worked was a two-phase recipe: compute the prompt embeddings first, streaming the encoder through the GPU in groups of layers, then drop it and generate with a tiled video VAE. PyTorch allocated at most 11.24 GiB; a vertical clip took about 141 s; the process reached 45.9 GiB of system RAM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speech.&lt;/strong&gt; Eight scenes with one Spanish line each, two seeds, scored with faster-whisper large-v3 against the requested line. The rule, written before running: median word error ≤ 0.20 and Spanish detected in ≥ 12 of 16 clips.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Median Spanish WER&lt;/th&gt;
&lt;th&gt;Clips with WER ≤ 0.20&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lite distilled (3B, 10 steps)&lt;/td&gt;
&lt;td&gt;1.06&lt;/td&gt;
&lt;td&gt;1 of 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lite undistilled (50 steps, guidance 5)&lt;/td&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;td&gt;1 of 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro distilled (29B, 10 steps)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.15&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9 of 16&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only Pro met the rule, and its process reached 120 GiB of RAM at roughly 12 to 23 minutes per clip (RTX 3090 with PyTorch memory capped at 15.57 GiB, transformer streamed block by block). The data is compatible with an advantage of the bigger model, but it cannot separate size from distillation.&lt;/p&gt;

&lt;p&gt;The full write-up has the recipes, the component diagram, clips with what Whisper heard, the threats to validity (three external audits), and a demo: the small model's video with a replaced voice and the mouth redrawn by LatentSync 1.6.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the full measurements → &lt;a href="https://efraingaray.com/en/blog/kandinsky6-video-audio-16gb/" rel="noopener noreferrer"&gt;https://efraingaray.com/en/blog/kandinsky6-video-audio-16gb/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scripts, the pre-registration sheet and the raw JSONL: &lt;a href="https://gist.github.com/EfrainGaray/e38005fb2d7b8abe25045c2d1e0f1526" rel="noopener noreferrer"&gt;gist&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>gpu</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>Qwen-Image 2.1 on 16 GB of VRAM, quantized to NF4: a real benchmark against FLUX</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Sat, 10 Oct 2026 18:08:42 +0000</pubDate>
      <link>https://dev.to/efraingaray/qwen-image-21-on-16-gb-of-vram-quantized-to-nf4-a-real-benchmark-against-flux-4jk0</link>
      <guid>https://dev.to/efraingaray/qwen-image-21-on-16-gb-of-vram-quantized-to-nf4-a-real-benchmark-against-flux-4jk0</guid>
      <description>&lt;p&gt;On a 16 GB RTX 4070 Ti SUPER, Qwen-Image-2.1-Turbo does not fit in bf16 (the pipeline is 32.5 GB). Quantized to NF4 with bitsandbytes it does, and I ran it against the FLUX.1-dev Q6_K + pixel-art LoRA that made my blog covers: same prompts, same seeds, same card, every image checked by eye.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhp8hfsxwj1eyd7nkd25g.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhp8hfsxwj1eyd7nkd25g.jpg" alt="Same prompt, same seed: FLUX against Qwen NF4" width="800" height="1422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I measured&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speed:&lt;/strong&gt; 8.4 s per image for Qwen NF4 (8 steps) against 61 s for FLUX + LoRA (30 steps), median, 1344×768. About 7×.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text in the image:&lt;/strong&gt; 13 of 18 requested texts exact for Qwen NF4, 7 for FLUX. Unquantized Qwen on a 48 GB A6000 got 17.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cover prompts:&lt;/strong&gt; Qwen NF4 drew the requested scene in 11 of 15 covers; FLUX + LoRA in none (6 almost, 9 missing the main action or object).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory:&lt;/strong&gt; 11.9 GiB over the card's baseline for Qwen NF4 (nvidia-smi), 13.1 GiB for FLUX.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What broke on the way&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An 8-bit (LLM.int8) transformer returns a near-uniform purple field. NF4 works.&lt;/li&gt;
&lt;li&gt;Decoding the VAE in tiles (needed to fit) left faint vertical purple streaks. Fix: make the latents first, drop the transformer and the text encoder from the GPU, then decode the whole frame (0.77 s, 7.1 GiB peak).&lt;/li&gt;
&lt;li&gt;An automatic reader (gemma3:4b) accepted misspelled text, and an external audit caught five of my own verdicts. Every number in the post comes from the corrected image-by-image review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I lose: the LoRA's atmosphere. Qwen follows the scene but on a flatter background. I switched anyway: every cover on the blog is now generated by Qwen NF4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the full measurements, the side-by-side galleries and the code →&lt;/strong&gt; &lt;a href="https://efraingaray.com/en/blog/qwen-image-vs-flux/" rel="noopener noreferrer"&gt;https://efraingaray.com/en/blog/qwen-image-vs-flux/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scripts, prompts and the per-image review: &lt;a href="https://gist.github.com/EfrainGaray/427b5cc1875b888fd57e32bf8b886527" rel="noopener noreferrer"&gt;gist&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>stablediffusion</category>
      <category>machinelearning</category>
      <category>benchmarks</category>
      <category>python</category>
    </item>
    <item>
      <title>Strands Decider 2B vs LLM as a judge: a keyword regex tied the 2B</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Wed, 07 Oct 2026 18:15:59 +0000</pubDate>
      <link>https://dev.to/efraingaray/strands-decider-2b-vs-llm-as-a-judge-a-keyword-regex-tied-the-2b-i74</link>
      <guid>https://dev.to/efraingaray/strands-decider-2b-vs-llm-as-a-judge-a-keyword-regex-tied-the-2b-i74</guid>
      <description>&lt;p&gt;I wanted a cheap judge for my video pipeline: given a shot description, does it have a concrete subject, a visible action and an explicit camera decision, or is it generic? Strands Decider 2B is built for that kind of yes/no call, so I measured it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;75 shot descriptions&lt;/strong&gt;: 63 captured from my pipeline (22 from production, 41 from a weak model running the same step) and 12 adversarial, with the three features annotated before running any judge.&lt;/li&gt;
&lt;li&gt;With the &lt;code&gt;choice&lt;/code&gt; primitive the 2B reaches &lt;strong&gt;AUC 0.91 in 34 ms&lt;/strong&gt; per judgment. Claude Opus reaches &lt;strong&gt;0.99&lt;/strong&gt;. A regex that only looks for framing words reaches &lt;strong&gt;0.90&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;With its default primitive, &lt;code&gt;noul&lt;/code&gt;, it caught &lt;strong&gt;none of the 31 generic shots&lt;/strong&gt; at the 0.5 threshold. The v21 checkpoint does not improve it.&lt;/li&gt;
&lt;li&gt;Repeated scenes in a shot list, my real failure, are the easy part: the 2B, Opus and Haiku got &lt;strong&gt;13 of 13&lt;/strong&gt; lists right.&lt;/li&gt;
&lt;li&gt;Shots, labels, every judge's output and the scripts are public: a &lt;a href="https://efraingaray.com/en/datasets/juez-decision-75/" rel="noopener noreferrer"&gt;CC-BY-4.0 dataset&lt;/a&gt; and a gist that reproduces every figure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Read the full measurements → &lt;a href="https://efraingaray.com/en/blog/juez-decision-2b/" rel="noopener noreferrer"&gt;https://efraingaray.com/en/blog/juez-decision-2b/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>machinelearning</category>
      <category>benchmarks</category>
      <category>python</category>
    </item>
    <item>
      <title>Wild beats mold linking Rust 20 times out of 20, and in release the linker is no longer the bottleneck</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Mon, 28 Sep 2026 01:01:46 +0000</pubDate>
      <link>https://dev.to/efraingaray/wild-beats-mold-linking-rust-20-times-out-of-20-and-in-release-the-linker-is-no-longer-the-113n</link>
      <guid>https://dev.to/efraingaray/wild-beats-mold-linking-rust-20-times-out-of-20-and-in-release-the-linker-is-no-longer-the-113n</guid>
      <description>&lt;p&gt;I measured Wild 0.10.0, mold 2.42.1, rust-lld and GNU ld linking ripgrep and cargo.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wild won &lt;strong&gt;all 20 repetitions&lt;/strong&gt; of each project, by &lt;strong&gt;1.9 ms&lt;/strong&gt; and about &lt;strong&gt;8 ms&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;With either fast linker, the link is close to &lt;strong&gt;1% of the rebuild&lt;/strong&gt; — it stops being the bottleneck.&lt;/li&gt;
&lt;li&gt;On the way I almost published false numbers; the post shows the trap and how I caught it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Read the full measurements → &lt;a href="https://efraingaray.com/en/blog/wild-vs-mold-rust/" rel="noopener noreferrer"&gt;https://efraingaray.com/en/blog/wild-vs-mold-rust/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>linkers</category>
      <category>benchmarks</category>
      <category>performance</category>
    </item>
    <item>
      <title>TabPFN and TabICL against tuned XGBoost: the model that does not train won on fourteen tables out of fourteen</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Mon, 28 Sep 2026 01:01:10 +0000</pubDate>
      <link>https://dev.to/efraingaray/tabpfn-and-tabicl-against-tuned-xgboost-the-model-that-does-not-train-won-on-fourteen-tables-out-58bk</link>
      <guid>https://dev.to/efraingaray/tabpfn-and-tabicl-against-tuned-xgboost-the-model-that-does-not-train-won-on-fourteen-tables-out-58bk</guid>
      <description>&lt;p&gt;The claim behind TabPFN and TabICL: they predict on a table without ever training on it, and still beat tuned boosting.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Measured on &lt;strong&gt;14 datasets&lt;/strong&gt; from the Grinsztajn benchmark, same split and same clock for everyone.&lt;/li&gt;
&lt;li&gt;The model that &lt;strong&gt;does not train won on 14 out of 14&lt;/strong&gt; against tuned XGBoost.&lt;/li&gt;
&lt;li&gt;What it costs in latency and VRAM, and when I'd still reach for boosting — in the post.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Read the full measurements → &lt;a href="https://efraingaray.com/en/blog/tabpfn-vs-xgboost/" rel="noopener noreferrer"&gt;https://efraingaray.com/en/blog/tabpfn-vs-xgboost/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>benchmarks</category>
      <category>datascience</category>
      <category>python</category>
    </item>
    <item>
      <title>Rust Coreutils 0.12 vs GNU 9.12: almost everything works the same, starting a process costs nearly 3x more</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Mon, 28 Sep 2026 01:00:34 +0000</pubDate>
      <link>https://dev.to/efraingaray/rust-coreutils-012-vs-gnu-912-almost-everything-works-the-same-starting-a-process-costs-nearly-3i46</link>
      <guid>https://dev.to/efraingaray/rust-coreutils-012-vs-gnu-912-almost-everything-works-the-same-starting-a-process-costs-nearly-3i46</guid>
      <description>&lt;p&gt;I measured Rust Coreutils (uutils) 0.12 against GNU coreutils 9.12 in containers with limits: the full GNU test suite, 157 commands with their output compared byte for byte, and process start-up.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;uutils is &lt;strong&gt;slower in 91 of 157&lt;/strong&gt; cells.&lt;/li&gt;
&lt;li&gt;It takes &lt;strong&gt;2.8x longer&lt;/strong&gt; to launch 5,000 processes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mv&lt;/code&gt; between disks &lt;strong&gt;loses the timestamps&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where each one wins, the full command matrix, and whether it's ready to replace GNU on a real box — in the write-up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the full measurements → &lt;a href="https://efraingaray.com/en/blog/rust-coreutils-contra-gnu/" rel="noopener noreferrer"&gt;https://efraingaray.com/en/blog/rust-coreutils-contra-gnu/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>linux</category>
      <category>benchmarks</category>
      <category>performance</category>
    </item>
    <item>
      <title>Your architecture diagram is lying: I tested both ways of writing it as code</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Mon, 28 Sep 2026 00:59:58 +0000</pubDate>
      <link>https://dev.to/efraingaray/your-architecture-diagram-is-lying-i-tested-both-ways-of-writing-it-as-code-50ig</link>
      <guid>https://dev.to/efraingaray/your-architecture-diagram-is-lying-i-tested-both-ways-of-writing-it-as-code-50ig</guid>
      <description>&lt;p&gt;Most architecture diagrams in production are out of date — not through carelessness, but because they live in a PNG someone drew once while the code kept changing. Nobody opens a visual editor to move a box when they ship a microservice at 11pm.&lt;/p&gt;

&lt;p&gt;A closed C4 web editor made the rounds on Habr this week: no repo, nothing to self-host, nothing to measure. So I tested the two alternatives you &lt;em&gt;can&lt;/em&gt; run yourself, on a real system.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structurizr CLI&lt;/strong&gt; — generates all three C4 levels (context, containers, components) in &lt;strong&gt;1 second&lt;/strong&gt; from a single &lt;code&gt;.dsl&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C4-PlantUML&lt;/strong&gt; — reaches the same result with &lt;strong&gt;nothing to install&lt;/strong&gt;, at the cost of &lt;strong&gt;tripling the size of the model&lt;/strong&gt; you maintain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full write-up has the exact commands, the four review gates this unlocks (diff, PR review, versioning, CI), the side-by-side of both tools, and when I'd pick each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the full measurements → &lt;a href="https://efraingaray.com/en/blog/arquitectura-como-codigo/" rel="noopener noreferrer"&gt;https://efraingaray.com/en/blog/arquitectura-como-codigo/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>plantuml</category>
      <category>devops</category>
      <category>tools</category>
    </item>
    <item>
      <title>Don't put getaddrinfo on your proxy's hot path</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Thu, 10 Sep 2026 21:11:48 +0000</pubDate>
      <link>https://dev.to/efraingaray/dont-put-getaddrinfo-on-your-proxys-hot-path-i31</link>
      <guid>https://dev.to/efraingaray/dont-put-getaddrinfo-on-your-proxys-hot-path-i31</guid>
      <description>&lt;p&gt;I set out to benchmark Pingora 0.9.0 — the Rust proxy library Cloudflare runs at its edge — against nginx, in the environment I actually care about: a small pod with a CPU limit, two vCPUs, everything in containers so no number leans on the host.&lt;/p&gt;

&lt;p&gt;I wrote the simplest possible reverse proxy with Pingora, hit it, and got &lt;strong&gt;21k requests/sec against nginx's 126k in the same setup&lt;/strong&gt;. Six times slower.&lt;/p&gt;

&lt;p&gt;That number was a lie, and the fault was mine.&lt;/p&gt;

&lt;p&gt;Six times is too much — Cloudflare would not replace nginx with something 6x slower. So I isolated it. Not CPU throttling (&lt;code&gt;nr_throttled&lt;/code&gt; was zero). Not thread oversubscription (exactly two workers). Not connection reuse (both held ~130 upstream connections). The clue was latency: 4.78 ms in the container, 0.45 ms on the host with the backend at &lt;code&gt;127.0.0.1&lt;/code&gt;. The only thing that changed was how I named the backend.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;upstream_peer&lt;/code&gt; built the destination on &lt;strong&gt;every request&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// (&amp;amp;str, u16) → to_socket_addrs() → getaddrinfo, blocking, on the tokio worker&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;HttpPeer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s"&gt;"backend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;HttpPeer::new&lt;/code&gt; over a &lt;code&gt;(&amp;amp;str, u16)&lt;/code&gt; calls &lt;code&gt;getaddrinfo&lt;/code&gt; synchronously, inside the tokio worker thread. In a container, every request fired a DNS query to Docker's resolver and blocked the thread. On the host, an IP literal is just a parse — no syscall — so it barely showed.&lt;/p&gt;

&lt;p&gt;The fix is one line: resolve once at startup, keep the &lt;code&gt;SocketAddr&lt;/code&gt;, pass that on the hot path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"backend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.to_socket_addrs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// ...on every request, no lookup:&lt;/span&gt;
&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;HttpPeer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.addr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;())))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pingora went from 21k to &lt;strong&gt;89k req/s&lt;/strong&gt; in the same pod. The 6x was a &lt;code&gt;getaddrinfo&lt;/code&gt; per request, not the framework.&lt;/p&gt;

&lt;p&gt;The lesson isn't about Pingora. A microbenchmark punishes any clumsiness of whoever writes it, and a blocking name resolution hidden on the hot path disguises itself perfectly as "the framework is slow."&lt;/p&gt;

&lt;p&gt;With the proxy written properly, the &lt;strong&gt;real gap in a 2 vCPU pod is 1.49x&lt;/strong&gt; — and &lt;code&gt;perf&lt;/code&gt; pins it down to the cycle: Pingora runs 1.7x more instructions per request. I also found Pingora's thread default is 1 (a whole idle core if you don't set it), and that in a CPU-limited pod, &lt;em&gt;more&lt;/em&gt; threads than cores wrecks the p99 through CFS throttling.&lt;/p&gt;

&lt;p&gt;I documented the whole thing — the container setup, the full concurrency matrix with repetitions, the &lt;code&gt;perf&lt;/code&gt;/&lt;code&gt;strace&lt;/code&gt; profile, the flamegraph, and the thread-throttling numbers — with live diagrams and a reel, here:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://efraingaray.com/en/blog/pingora-090-getaddrinfo-hot-path/" rel="noopener noreferrer"&gt;The full write-up on efraingaray.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you benchmark a Rust proxy in a container: resolve upstreams by IP or once, and set threads equal to your CPU quota. Those two lines matter more than the framework you pick.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>programming</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>pg_anon caught 1 of my 8 PII columns. My schema isn't in English.</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Sun, 06 Sep 2026 03:20:27 +0000</pubDate>
      <link>https://dev.to/efraingaray/pganon-caught-1-of-my-8-pii-columns-my-schema-isnt-in-english-2e5d</link>
      <guid>https://dev.to/efraingaray/pganon-caught-1-of-my-8-pii-columns-my-schema-isnt-in-english-2e5d</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/TantorLabs/pg_anon" rel="noopener noreferrer"&gt;pg_anon&lt;/a&gt; found &lt;strong&gt;1&lt;/strong&gt; of the 8 personal-data columns in my PostgreSQL database. The one it caught was &lt;code&gt;email&lt;/code&gt;, and only because "email" is spelled the same in Spanish and English.&lt;/p&gt;

&lt;p&gt;The other seven — &lt;code&gt;nombre&lt;/code&gt;, &lt;code&gt;apellido&lt;/code&gt;, &lt;code&gt;telefono&lt;/code&gt;, &lt;code&gt;direccion&lt;/code&gt;, &lt;code&gt;fecha_nac&lt;/code&gt;, &lt;code&gt;tarjeta_ult4&lt;/code&gt; and &lt;code&gt;rut&lt;/code&gt; — walked straight through, unmasked.&lt;/p&gt;

&lt;p&gt;pg_anon is the open TantorLabs tool that masks personal data in PostgreSQL: it scans the database, flags the sensitive columns, and dumps a masked copy. Like &lt;code&gt;pg_dump&lt;/code&gt;, but covering the sensitive parts on the way out. Exactly what you want before handing a colleague a copy of production. So I fed it a Chilean database and watched it miss almost everything — no error, no warning. It finished successfully and handed me a dump with names and national IDs still in cleartext.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the scan actually does
&lt;/h2&gt;

&lt;p&gt;Two filters, in order. First it reads each column's &lt;strong&gt;name&lt;/strong&gt; against a set of regexes (&lt;code&gt;^email$&lt;/code&gt;, &lt;code&gt;^phone$&lt;/code&gt;, &lt;code&gt;^ssn$&lt;/code&gt;…). To the columns left over, it opens the &lt;strong&gt;data&lt;/strong&gt; and tries patterns on the value (an email's &lt;code&gt;@&lt;/code&gt;, a card's 16 digits). Whatever no filter catches passes through.&lt;/p&gt;

&lt;p&gt;The rules in the demo meta-dict it ships with are written for English schemas. Mine aren't.&lt;/p&gt;

&lt;h2&gt;
  
  
  1 of 8
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Holds&lt;/th&gt;
&lt;th&gt;Stock rules&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;email&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nombre&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;first name&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apellido&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;surname&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;telefono&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;phone&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;direccion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;address&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fecha_nac&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;birth date&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tarjeta_ult4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;card digits&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rut&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;national ID&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;email&lt;/code&gt; got caught by name (it's an English word) and confirmed by its &lt;code&gt;@&lt;/code&gt;. Everything else has a Spanish name no stock rule looks for.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;rut&lt;/code&gt; is the clearest miss. A RUT looks like &lt;code&gt;7917183-2&lt;/code&gt;: seven or eight digits, a dash, and a mod-11 check digit that &lt;strong&gt;can be the letter K&lt;/strong&gt;. The only national-ID rule pg_anon ships with is &lt;code&gt;ssn&lt;/code&gt;. It has no idea what a RUT is — and it won't know a &lt;code&gt;cpf&lt;/code&gt; (Brazil), &lt;code&gt;dni&lt;/code&gt; (Spain, Argentina), &lt;code&gt;curp&lt;/code&gt; (Mexico), &lt;code&gt;nif&lt;/code&gt; (Portugal) or &lt;code&gt;aadhaar&lt;/code&gt; (India) either. If your schema isn't American, the defaults miss your most sensitive column.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: a few lines of Spanish
&lt;/h2&gt;

&lt;p&gt;You teach it. Column names in your language, plus a content regex for the ID so it's caught even in a column &lt;em&gt;not&lt;/em&gt; named &lt;code&gt;rut&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rules&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^nombre$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^apellido$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^email$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^telefono$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^rut$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^direccion$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^fecha_nac$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^tarjeta_ult4$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data_regex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rules&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[\w.-]+@[\w-]+\.\w+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# email
&lt;/span&gt;      &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^(\+?56)?9\d{8}$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# Chilean mobile
&lt;/span&gt;      &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^\d{5,8}-[\dkK]$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# RUT with check digit
&lt;/span&gt;    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, recall went from &lt;strong&gt;1 of 8 to 8 of 8&lt;/strong&gt;, no false positives — it still left the non-personal columns alone. The point isn't "it went up when I wrote the rules" (obviously). It's how cheap the fix was, and that a masking tool ships blind to any schema that isn't English.&lt;/p&gt;

&lt;p&gt;I left the full Spanish meta-dict (with the RUT regex) in a public repo so nobody starts from scratch: &lt;strong&gt;&lt;a href="https://github.com/EfrainGaray/pg-anon-rules-es" rel="noopener noreferrer"&gt;pg-anon-rules-es&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;This is the short version. The full write-up covers what didn't fit: whether foreign keys survive the restore, the trap where a blanket SHA-256 keeps uniqueness but produces "emails" with no &lt;code&gt;@&lt;/code&gt;, and how much slower it runs than a plain &lt;code&gt;pg_dump&lt;/code&gt;. Numbers, diagrams and a 47-second breakdown: &lt;strong&gt;&lt;a href="https://efraingaray.com/en/blog/pg-anon-datos-personales" rel="noopener noreferrer"&gt;pg_anon found my database's emails, but not the RUT&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does the national ID look like where you are — and would a stock masking tool catch it?&lt;/strong&gt; Drop the format (or the regex) in the comments and I'll add it to the repo.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>security</category>
      <category>programming</category>
    </item>
    <item>
      <title>I translated DOOM from C to Rust in 23 seconds, then used AI for what it is good at</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Fri, 21 Aug 2026 21:58:13 +0000</pubDate>
      <link>https://dev.to/efraingaray/i-translated-doom-from-c-to-rust-in-23-seconds-then-used-ai-for-what-it-is-good-at-1j3a</link>
      <guid>https://dev.to/efraingaray/i-translated-doom-from-c-to-rust-in-23-seconds-then-used-ai-for-what-it-is-good-at-1j3a</guid>
      <description>&lt;p&gt;I needed to move a C program to Rust. My first instinct was the easy one: hand it to a model, chunk by chunk, and review the output.&lt;/p&gt;

&lt;p&gt;Then I checked whether a tool already existed. &lt;code&gt;c2rust&lt;/code&gt;, from Immunant, does exactly this. On the 73,095 lines of doomgeneric it took &lt;strong&gt;23.2 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The interesting part is not the speed. It is that the model still ended up doing real work, just nowhere near the translation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost was not the argument, and pretending otherwise would be selling smoke
&lt;/h2&gt;

&lt;p&gt;I measured the token cost of translating DOOM with a model using the &lt;code&gt;o200k_base&lt;/code&gt; tokenizer: 550,595 input tokens and 1,896,319 output. That is a few dollars. It is not a reason.&lt;/p&gt;

&lt;p&gt;What actually decides is three other things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reproducible.&lt;/strong&gt; Same input, same output file, byte for byte, every run. You can &lt;code&gt;diff&lt;/code&gt; it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Systematic errors.&lt;/strong&gt; The translation produced four bugs. Two were the same bug in 23 places. One class gets fixed once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All of them failed loudly.&lt;/strong&gt; Every one crashed at compile time or on the first frame. A scattered, silent error in one of 143,911 lines costs more to find than the whole translation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How you prove a translation did not change the program
&lt;/h2&gt;

&lt;p&gt;DOOM ships recorded demos. Every gameplay decision is a fixed-point calculation, so if one of them differs the playback desynchronizes and the tic count stops matching. That is the first gate, and it is free.&lt;/p&gt;

&lt;p&gt;The second gate: I hashed every frame the engine hands to the display, in both builds. &lt;strong&gt;11,113 frames, one differed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I nearly published that one frame as &lt;code&gt;c2rust&lt;/code&gt;'s single error. The control run is what saved me: I ran the original C binary against &lt;strong&gt;itself&lt;/strong&gt;. It differs from itself too, on the same pixel. Original DOOM reads one pixel of uninitialized memory, and the translation reproduced that faithfully.&lt;/p&gt;

&lt;p&gt;If you only ever compare against one run, you will report your own reference's noise as the other side's bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The translated Rust is not safe Rust
&lt;/h2&gt;

&lt;p&gt;Measuring &lt;strong&gt;lines inside &lt;code&gt;unsafe&lt;/code&gt; blocks&lt;/strong&gt;, the translated zlib puts 47% of the crate in &lt;code&gt;unsafe&lt;/code&gt;, against 31% for &lt;code&gt;zlib-rs&lt;/code&gt;, a rewrite done by people.&lt;/p&gt;

&lt;p&gt;Counting occurrences of the word &lt;code&gt;unsafe&lt;/code&gt; gives you the opposite answer, and it is a misleading metric: the translator wraps whole functions in a single block, so it scores &lt;em&gt;better&lt;/em&gt; on a count while being worse in reality.&lt;/p&gt;

&lt;p&gt;These are not comparable artifacts anyway. &lt;code&gt;zlib-rs&lt;/code&gt; is a rewrite, with redesigned structures and the type system doing work. An automatic translation cannot do that. Its goal is to not change the program.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the model actually earned its place
&lt;/h2&gt;

&lt;p&gt;Not translating. Generating detail that &lt;strong&gt;modulates on top of what the engine already computes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every texture goes through an upscaler, then gets stored as a one-channel map whose mean is exactly 128 per texel. The renderer keeps deciding color and lighting; the map only contributes variation &lt;em&gt;within&lt;/em&gt; the texel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;color_final = color_doom × detail / 128
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the mean is anchored at 128, the enhancement provably cannot shift the image. Averaged over any texel, it multiplies by 1.&lt;/p&gt;

&lt;p&gt;And that same map is a height map, so its gradient gives per-pixel relief for free, with no extra bytes, baked at pack time.&lt;/p&gt;

&lt;p&gt;That constraint, that the engine keeps deciding, is what made the result add instead of ruin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both versions are playable in your browser
&lt;/h2&gt;

&lt;p&gt;The 1993 C build and the Rust translation are compiled to WebAssembly and running side by side, plus the enhanced renderer at 640×400 with per-pixel lighting.&lt;/p&gt;

&lt;p&gt;The 60-second version of all of this, with the game running behind it:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/2mTM5vuS114"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;p&gt;The full write-up has the four translation bugs, the three portability ones, the &lt;code&gt;memset&lt;/code&gt; that silently cleared half the screen after widening a struct field, and how it went from 7215 to 3009 realtics without rewriting the game:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://efraingaray.com/en/blog/c-a-rust-doom/" rel="noopener noreferrer"&gt;I translated DOOM from C to Rust without writing a line, then used AI for what it is actually good at&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>programming</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
