<?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: tauridev</title>
    <description>The latest articles on DEV Community by tauridev (@tauridev).</description>
    <link>https://dev.to/tauridev</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%2F4019232%2F9ef8616e-3149-4666-96a2-5340e2d2e3a3.png</url>
      <title>DEV Community: tauridev</title>
      <link>https://dev.to/tauridev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tauridev"/>
    <language>en</language>
    <item>
      <title>I had a local LLM review the same code 51 times: majority voting picks persistence, not correctness</title>
      <dc:creator>tauridev</dc:creator>
      <pubDate>Tue, 07 Jul 2026 11:48:35 +0000</pubDate>
      <link>https://dev.to/tauridev/i-had-a-local-llm-review-the-same-code-51-times-majority-voting-picks-persistence-not-correctness-4g15</link>
      <guid>https://dev.to/tauridev/i-had-a-local-llm-review-the-same-code-51-times-majority-voting-picks-persistence-not-correctness-4g15</guid>
      <description>&lt;p&gt;If you use an LLM to review code — or run it several times and keep "whatever most runs agree on" — this experiment suggests the votes you're counting measure &lt;strong&gt;persistence&lt;/strong&gt;, not &lt;strong&gt;correctness&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Setup: Qwen2.5-Coder 1.5B / 7B (Apache-2.0) via Ollama 0.31.1, single RTX 4060 Ti, Q4_K_M quantization. N=51 repeated audits of the same small Python files with predeclared seeded defects (temp-0 baseline: N=11 per condition). The per-run records (normalized findings plus a SHA-256 of each raw response), ground truth, and aggregation code are public — every number quoted in this article regenerates deterministically from the frozen records via an independent verification script.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fully pinned = deterministic (on this rig).&lt;/strong&gt; At temperature 0 with a fixed GPU and quantization, the finding sets were identical across runs — Jaccard similarity &lt;strong&gt;1.00&lt;/strong&gt; in all four conditions (N=11 each). In a probe, six repeats within one seed and six different seeds all produced &lt;em&gt;byte-identical&lt;/em&gt; output: our temp-0 configuration also forces &lt;code&gt;top_k=1&lt;/code&gt; (greedy decoding in Ollama/llama.cpp), so the seed had no observable effect. "LLMs are always nondeterministic" is folklore, at least under fully pinned local conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sampling at temperature 0.7 breaks agreement.&lt;/strong&gt; On the defect-seeded file — 51 runs, one seed per run (0–50) — run-to-run Jaccard dropped to &lt;strong&gt;0.52&lt;/strong&gt; (7B) and &lt;strong&gt;0.07&lt;/strong&gt; (1.5B). For 7B, allowing ±2 lines of tolerance recovers 0.52 → &lt;strong&gt;0.80&lt;/strong&gt;: over half of the disagreement is consistent with &lt;em&gt;the same bug pointed at a different line&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The core finding: majority voting selects persistence, not correctness.&lt;/strong&gt; A real resource leak that the model detected in &lt;strong&gt;45 of 51 runs&lt;/strong&gt; split its votes across two adjacent lines (26 + 19 — no run reported both, so those really are 45 distinct runs). Under the strict line+category key, the majority threshold of 26 barely keeps one line; one vote higher, at k=27, a defect found 45 times is gone. Meanwhile, two "grey" pattern warnings (&lt;code&gt;eval&lt;/code&gt; and &lt;code&gt;shell=True&lt;/code&gt; on &lt;em&gt;constants&lt;/em&gt;, from the decoy file) survived at 33/51 and 28/51, because the model points at exactly the same place every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest caveats up front:&lt;/strong&gt; on the defect-seeded file, against predeclared ground truth with category + ±2-line matching, the 7B's per-run recall was only 0.31 — but precision was 0.98. Within this benchmark: when it speaks, it's right, and it misses a lot. The 1.5B was too noisy to carry any claim (it hit 25 of the file's 32 lines at least once across 51 runs), so it serves as a contrast group, not evidence.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠ &lt;strong&gt;Reproducibility scope:&lt;/strong&gt; the &lt;em&gt;existence and rough magnitude&lt;/em&gt; of the variance reproduces; exact numbers are rig-dependent (GPU / quantization / inference-engine version). The repo ships frozen per-run records — normalized findings plus a hash of each raw response; the raw response bodies themselves were not retained — and readers re-run only the deterministic aggregation layer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why measure this
&lt;/h2&gt;

&lt;p&gt;"LLM-as-a-judge" is everywhere, and its known weakness is nondeterminism. Prior work measures &lt;em&gt;score&lt;/em&gt; stability — &lt;a href="https://arxiv.org/abs/2502.20747" rel="noopener noreferrer"&gt;Klishevich et al. (arXiv:2502.20747)&lt;/a&gt; measured scalar-verdict consistency over 70 commits at temp 0, and a Japanese industrial blog measured same-score rates on cloud APIs. What I couldn't find measured anywhere: whether &lt;strong&gt;the set of findings itself&lt;/strong&gt; — &lt;em&gt;which&lt;/em&gt; issues, on &lt;em&gt;which&lt;/em&gt; lines — is stable across runs, on a local model, free to reproduce, with run records frozen and shipped. That's the gap this fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Models:&lt;/strong&gt; &lt;code&gt;Qwen2.5-Coder&lt;/code&gt; 1.5B and 7B (Apache-2.0), run locally via Ollama. Chosen because they're free, local, and require no API key — anyone can rerun generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sampling design:&lt;/strong&gt; temp-0 conditions use a &lt;strong&gt;fixed seed&lt;/strong&gt; and force &lt;code&gt;top_k=1, top_p=1.0&lt;/code&gt;; temp-0.7 conditions use &lt;strong&gt;one seed per run (0, 1, …, 50)&lt;/strong&gt; with Ollama's default sampler otherwise (&lt;code&gt;top_k=40, top_p=0.9&lt;/code&gt;; not swept). The temp-0.7 numbers therefore measure &lt;strong&gt;across-seed sampling variability at temperature 0.7&lt;/strong&gt; — not same-seed runtime nondeterminism (with a fixed seed, repeats would be trivially identical).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output format:&lt;/strong&gt; Ollama structured outputs (JSON-schema-enforced). Each finding is &lt;code&gt;{line_number, category, severity, description}&lt;/code&gt;, category constrained to an enum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity of a finding:&lt;/strong&gt; the composite key &lt;code&gt;(line_number, category)&lt;/code&gt;. Paraphrases are ignored; two findings are "the same" iff they name the same place and problem type. I deliberately did &lt;em&gt;not&lt;/em&gt; use embedding similarity — it happily merges "missing null check on line 12" with "missing null check on line 45".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target files (4):&lt;/strong&gt; &lt;code&gt;target_a&lt;/code&gt; (7 seeded defects), &lt;code&gt;clean&lt;/code&gt; (no defects — false-positive baseline), &lt;code&gt;decoy&lt;/code&gt; (looks dangerous, low actual harm — this classification gets revised later, see pitfall #4), &lt;code&gt;easy&lt;/code&gt; (6 obvious defects).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ground truth:&lt;/strong&gt; predeclared in a &lt;code&gt;gt.csv&lt;/code&gt; &lt;em&gt;outside&lt;/em&gt; the code before measurement. No hints in the code or the prompt — I learned this one the hard way (pitfall #1).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-layer reproducibility:&lt;/strong&gt; layer 1 (generation, nondeterministic) ran once and its per-run records are frozen in the repo; layer 2 (aggregation, Python stdlib only) is fully deterministic and is what you reproduce.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inference&lt;/td&gt;
&lt;td&gt;Ollama 0.31.1&lt;/td&gt;
&lt;td&gt;local, free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Models&lt;/td&gt;
&lt;td&gt;Qwen2.5-Coder 1.5B / 7B&lt;/td&gt;
&lt;td&gt;Q4_K_M (verified via &lt;code&gt;ollama show&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;NVIDIA RTX 4060 Ti (CUDA)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ollama ps&lt;/code&gt; reported 100% GPU offload during runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregation&lt;/td&gt;
&lt;td&gt;Python 3.12, stdlib only&lt;/td&gt;
&lt;td&gt;layer 2 fully deterministic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Measured: 2026-07. Per-cell provenance (model quantization/size/family, Ollama version, prompt/schema/GT hashes, backend) is recorded in the frozen records.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I measured
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reliability = Jaccard between runs (no ground truth involved):&lt;/strong&gt; &lt;code&gt;matched findings / union&lt;/code&gt;, computed with &lt;strong&gt;optimal bipartite matching&lt;/strong&gt; — naive greedy matching gives order-dependent values (a bug I actually shipped and had to fix). Reported as the mean over all run pairs within a condition, with a &lt;strong&gt;line-tolerance window w&lt;/strong&gt; (±0 strict / ±2 / ±5) reported alongside — this doubles as the sensitivity analysis for the identity key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validity = precision / recall / false positives against GT.&lt;/strong&gt; Unless noted, recall/precision are per-run means on &lt;code&gt;target_a&lt;/code&gt;, category-strict, line ±2. Precision excludes empty runs by definition — and there were none on &lt;code&gt;target_a&lt;/code&gt; (0/51); recall averages over all runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Majority voting = keep findings that appear in ≥ k of N runs.&lt;/strong&gt; Examined on two axes: threshold k × key granularity w.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncertainty:&lt;/strong&gt; 95% CIs from bootstrap resampling &lt;strong&gt;at the run level&lt;/strong&gt;. One honest structural caveat: 51 audits of the &lt;em&gt;same&lt;/em&gt; file are pseudo-replication — nothing here generalizes across codebases, and I don't claim it does.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Result 1: pin everything, and it's deterministic
&lt;/h2&gt;

&lt;p&gt;All four temp-0 conditions (2 models × {defect-seeded, easy} files, N=11): Jaccard &lt;strong&gt;1.00&lt;/strong&gt; at every tolerance level, zero variance across runs. In a separate probe on one condition, six repeats within a single seed and six different seeds (1, 2, 3, 101, 202, 303) produced byte-identical outputs — our temp-0 configuration also forces &lt;code&gt;top_k=1&lt;/code&gt;, i.e. greedy decoding in Ollama/llama.cpp, so the seed had no observable effect there. Folklore says "LLMs always wobble"; under fully pinned local conditions on this rig, they didn't.&lt;/p&gt;

&lt;p&gt;📐 The honest asterisk on "deterministic"&lt;/p&gt;

&lt;p&gt;In a separate spike I did observe a single category flip at temp 0 — only ever on the &lt;em&gt;first&lt;/em&gt; run after model load, which also had the longest latency (cold-cache/warm-up signature). With a warm-up run added, the residual disappeared. I can't pin the mechanism (GPU parallel-reduction ordering is the usual suspect; cold cache isn't ruled out), but it isn't the seed. This is consistent with &lt;a href="https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/" rel="noopener noreferrer"&gt;Thinking Machines' "Defeating Nondeterminism in LLM Inference"&lt;/a&gt;, which attributes serving-time nondeterminism primarily to &lt;strong&gt;batch-size variation&lt;/strong&gt; — a local single-request setup is effectively fixed-batch, which is consistent with the 1.00 here. The same mechanism would also predict the temp-0 flutter Klishevich et al. saw on &lt;em&gt;cloud APIs&lt;/em&gt; (variable batching), though they discuss other candidate causes as well — read this as consistency with one proposed mechanism, not a proven attribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 2: sample at temperature 0.7, and it falls apart
&lt;/h2&gt;

&lt;p&gt;People raise temperature precisely to widen candidate coverage across runs. At temp 0.7 on the defect-seeded file (one seed per run, 0–50), run-to-run Jaccard was &lt;strong&gt;0.52&lt;/strong&gt; [0.47, 0.59] for 7B and &lt;strong&gt;0.07&lt;/strong&gt; [0.04, 0.11] for 1.5B.&lt;/p&gt;

&lt;p&gt;Two distinct failure modes hide in those numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Line drift (7B):&lt;/strong&gt; 0.52 recovers to &lt;strong&gt;0.80&lt;/strong&gt; when the identity key tolerates ±2 lines. Over half of the disagreement is consistent with the same bug pointed at a different line — though tolerance matching can also create accidental matches, so treat ±2 as an upper bound on "drift".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Category flutter (1.5B, and &lt;code&gt;easy&lt;/code&gt;):&lt;/strong&gt; on the easy file, 7B's line-only recall is 0.81 but category-strict recall is 0.42 — the model keeps finding the right &lt;em&gt;place&lt;/em&gt; and labeling it a different &lt;em&gt;problem&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;recall (category-strict)&lt;/th&gt;
&lt;th&gt;recall (line-only)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;target_a&lt;/td&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;0.31&lt;/td&gt;
&lt;td&gt;0.31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;target_a&lt;/td&gt;
&lt;td&gt;1.5B&lt;/td&gt;
&lt;td&gt;0.05&lt;/td&gt;
&lt;td&gt;0.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;easy&lt;/td&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;0.42&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(For 7B on &lt;code&gt;target_a&lt;/code&gt; the two recalls coincide — those misses are genuine non-detections, not drift.)&lt;/p&gt;

&lt;p&gt;And here's where I hit my first big mistake: my early recall numbers were great — because I had left &lt;code&gt;# DEFECT: SQL injection&lt;/code&gt; annotations in the target code as notes to myself, and the whole file goes into the prompt. &lt;strong&gt;The model was reading my answer key.&lt;/strong&gt; Deleting the labels cratered recall and deepened the run-to-run variance. Details in the pitfall log below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 3 (the core): majority voting rewards persistence
&lt;/h2&gt;

&lt;p&gt;Majority vote = "keep findings that appear in ≥26 of 51 runs." Here is what that actually selects.&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%2Fa2nangb04fjob6nvez8o.png" 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%2Fa2nangb04fjob6nvez8o.png" alt="Votes per finding across 51 audits (7B, temp 0.7). Grey-smell warnings survive above the majority line; the real ResourceLeak bug splits 26/19 across two lines and straddles the threshold." width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure: votes per finding (blue = predeclared seeded defects, red = persistent findings outside GT, dashed = majority threshold 26/51). Counts are pooled from two target files run under identical conditions: the blue bars come from the defect-seeded file, the red bars from the decoy file.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mechanism.&lt;/strong&gt; With &lt;code&gt;(line, category)&lt;/code&gt; as the identity key, the same bug pointed at a neighboring line becomes &lt;em&gt;a different ballot&lt;/em&gt;. The model detected the ResourceLeak in &lt;strong&gt;45/51 runs&lt;/strong&gt; — but the votes landed 26 on line 16 and 19 on line 17 (no single run reported both lines, so 26+19 really is 45 distinct runs). Loosen the question to "did it say ResourceLeak at all, anywhere?" and it's 49/51 — scattered across &lt;strong&gt;six&lt;/strong&gt; different lines (12, 15, 16, 17, 18, 21). The vote count never sees that. Watch the threshold:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threshold k&lt;/th&gt;
&lt;th&gt;line 16 (26 votes)&lt;/th&gt;
&lt;th&gt;line 17 (19 votes)&lt;/th&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;k=19&lt;/td&gt;
&lt;td&gt;kept&lt;/td&gt;
&lt;td&gt;kept&lt;/td&gt;
&lt;td&gt;both survive (duplicated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;k=26 (majority)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;kept&lt;/td&gt;
&lt;td&gt;dropped&lt;/td&gt;
&lt;td&gt;survives by one vote&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;k=27&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;dropped&lt;/td&gt;
&lt;td&gt;dropped&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;a defect reported in 45/51 runs is gone&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Under the exact majority rule I predeclared (k=26) the leak &lt;em&gt;barely&lt;/em&gt; survives on one line; the point is not that majority voting killed it, but that &lt;strong&gt;whether it survives is governed by how the votes split and where you draw the line — not by the model's detection ability&lt;/strong&gt;. And it's not one anecdote: &lt;strong&gt;2 of the 5 defects that 7B detected at least once (~40%) showed line drift between runs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two contrasts sharpen the picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unsplit real bugs sail through:&lt;/strong&gt; the SQL injection on line 9 got &lt;strong&gt;51/51&lt;/strong&gt; unanimous votes. Voting doesn't kill strong detections — it kills &lt;em&gt;wobbly-keyed&lt;/em&gt; ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grey findings survive:&lt;/strong&gt; warnings against &lt;code&gt;eval("2 + 2")&lt;/code&gt; (33/51) and &lt;code&gt;subprocess.run(..., shell=True)&lt;/code&gt; with a constant string (28/51) — both low-harm on constants, both from the decoy file — persist because the model reacts to the &lt;em&gt;pattern&lt;/em&gt; and points at exactly the same place every time. (Bonus wobble: it labeled the latter &lt;code&gt;CodeInjection&lt;/code&gt; rather than &lt;code&gt;CommandInjection&lt;/code&gt; — categories flutter too.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;clean&lt;/code&gt; file adds a neat corollary: 7B stayed silent in 37/51 runs (72.5%), and when it &lt;em&gt;did&lt;/em&gt; complain, it complained about something different almost every time — non-empty clean runs agree at just &lt;strong&gt;0.12&lt;/strong&gt;. (Counting silent–silent pairs as agreement 1.0 the unconditional mean is 0.53; I report the conditional number because silence agreeing with silence isn't informative here.) Inconsistent false alarms are exactly what majority voting removes. Consistent grey warnings are exactly what it cannot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Threshold × key-granularity sensitivity&lt;/strong&gt; (majority k=26):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File (7B)&lt;/th&gt;
&lt;th&gt;w ±0&lt;/th&gt;
&lt;th&gt;±2&lt;/th&gt;
&lt;th&gt;±5&lt;/th&gt;
&lt;th&gt;Reading&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FP &lt;strong&gt;0&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;scattered false alarms die (as intended)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;decoy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 persistent&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;consistent grey findings survive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;target_a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TP 2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3 *&lt;/td&gt;
&lt;td&gt;* the +1 is a double-credit artifact (below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;easy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TP 2 / FP 1&lt;/td&gt;
&lt;td&gt;3 / 2&lt;/td&gt;
&lt;td&gt;3 / 1 †&lt;/td&gt;
&lt;td&gt;looser keys rescue split votes — at a price&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;† The &lt;code&gt;easy&lt;/code&gt; false-positive count is non-monotonic in w (1→2→1) because widening the window changes how findings cluster — adjacent clusters merge or get absorbed. This is a property of line-window clustering, disclosed rather than hidden.&lt;/p&gt;
&lt;/blockquote&gt;

📐 Why the ±5 "rescue" is double-counting, not detection

At w ±5, the ResourceLeak cluster (mostly line 16) becomes wide enough to overlap **two distinct** ground-truth defects — a connection leak (lines 6–11) and a file-handle leak (lines 15–17) — so one cluster gets credited to both (the aggregation deliberately allows multi-credit; that design choice is exactly what this row exposes). To be precise: no run put a ResourceLeak vote inside the connection-leak lines themselves (6–11); the nearest was a single vote at line 12 — one line outside, and far below any threshold. So the "+1 true positive" at ±5 is a windowing artifact, not a detection. Loosening keys rescues split votes, but the rescue is not free: on `easy` it temporarily raises false positives (1→2 at ±2), and here it manufactures a detection that never cleared any threshold. No free lunch.



&lt;p&gt;&lt;strong&gt;So: majority voting filters for &lt;em&gt;salience of the pattern response&lt;/em&gt;, not truth.&lt;/strong&gt; Persistent findings survive whether they're real bugs or grey smells; real bugs whose expression wobbles can fall. "Persistent" and "correct" are different properties, and the vote measures the first one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Honest scope note: in this sample the core claim rests mainly on one drifting real bug plus two persistent grey findings — read it as an &lt;strong&gt;observed mechanism&lt;/strong&gt;, not a general law. It is &lt;em&gt;not&lt;/em&gt; an argument against aggregation per se — this is also not the &lt;a href="https://arxiv.org/abs/2203.11171" rel="noopener noreferrer"&gt;self-consistency&lt;/a&gt; setting, which votes over &lt;em&gt;normalized&lt;/em&gt; final answers; the failure mode here lives precisely in the un-normalized key. Change the key (category-only, AST node, defect region) and the behavior changes — which is itself the point: threshold × key granularity is a design decision with no free lunch.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;What I actually do now:&lt;/strong&gt; treat multi-run LLM review as a &lt;strong&gt;candidate pool&lt;/strong&gt;, not an auto-verdict. Prioritize high-frequency findings for review, &lt;em&gt;don't&lt;/em&gt; discard low-frequency findings in severe categories, and let tests, static analysis, and a human's understanding of the spec make the final call.&lt;/p&gt;
&lt;h2&gt;
  
  
  Everything I got wrong (measurement-instrument failures included)
&lt;/h2&gt;

&lt;p&gt;The most useful part of this experiment was watching my own tooling and my own memory fail. Five mistakes, in the order I made them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;My answer key leaked into the prompt.&lt;/strong&gt; I'd annotated defect lines with &lt;code&gt;# DEFECT: ...&lt;/code&gt; comments as notes to myself — and the audit prompt includes the whole file. High recall was the model reading my answers. Removing the labels cratered recall and deepened variance. &lt;em&gt;Never let the benchmark's answers be visible to the system under test.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I thought I was benchmarking CPU inference. It was 100% GPU.&lt;/strong&gt; I had designed a thread-count variable (&lt;code&gt;num_thread&lt;/code&gt; 1 vs 4) — then &lt;code&gt;ollama ps&lt;/code&gt; showed 100% GPU offload, where &lt;code&gt;num_thread&lt;/code&gt; is a no-op. My thread axis had measured nothing. &lt;em&gt;Record your accelerator provenance instead of assuming it — the act of recording is what exposed the error.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I broke my own measuring instrument while "fixing" it.&lt;/strong&gt; A one-line "optimization" to a clustering helper introduced a classic append-then-clear aliasing bug that silently corrupted the majority-vote true/false-positive counts. I caught it only because a hand count said 2 where the tool said 3. Every number quoted in this article's text is now recomputed by an independent script (&lt;code&gt;verify_independent.py&lt;/code&gt;) that bypasses the aggregation code entirely; the full threshold×tolerance table and the bootstrap CIs still come from the aggregation pipeline. &lt;em&gt;Fixes need verification most of all.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My "false positives" were real smells.&lt;/strong&gt; I'd built &lt;code&gt;decoy&lt;/code&gt; assuming its &lt;code&gt;eval&lt;/code&gt;/&lt;code&gt;shell=True&lt;/code&gt;-on-constants patterns were harmless, so surviving warnings counted as false positives. But Bandit and friends flag exactly these. The finding got &lt;em&gt;stronger&lt;/em&gt; through the correction: the claim isn't "false positives survive voting" — it's "&lt;strong&gt;persistence survives voting, regardless of truth&lt;/strong&gt;", and I renamed those findings "persistent findings outside GT".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My own memory fabricated a result.&lt;/strong&gt; An early draft said "the &lt;code&gt;md5&lt;/code&gt; warning also survived the vote." The frozen records say md5 got &lt;strong&gt;0 votes in 51 runs&lt;/strong&gt;. My memory had synthesized it from the &lt;em&gt;impression&lt;/em&gt; of pitfall #4. Caught in pre-publication log reconciliation. &lt;em&gt;The experimenter's memory is not a primary source either — recheck the frozen records every time you write a number.&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is "don't trust LLM output" applied symmetrically: to the model, to my own instruments, and to my own claims.&lt;/p&gt;
&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Small sample: N=51, two model sizes, one rig (RTX 4060 Ti / CUDA / Q4_K_M / Ollama 0.31.1). CPU, other GPUs, other quantizations may behave differently.&lt;/li&gt;
&lt;li&gt;The temp-0.7 condition varies the seed per run (by design — that's what sampling the distribution means here), so it measures across-seed variability at that temperature, not a pure temperature effect with everything else held.&lt;/li&gt;
&lt;li&gt;The core claim rests on few instances (one drifting bug, two grey findings). Mechanistically coherent, statistically thin.&lt;/li&gt;
&lt;li&gt;"Smaller models wobble more" is a hypothesis from a 2-point comparison — capability and variance are confounded.&lt;/li&gt;
&lt;li&gt;Category-strict recall is low; this is not a benchmark of absolute audit quality. Some ground-truth entries depend on runtime contracts (e.g., whether a config loader may return None) — they are predeclared seeded defects under this benchmark's stated assumptions, not universally adjudicated bugs.&lt;/li&gt;
&lt;li&gt;Structured output (JSON-schema-constrained decoding) modifies the logit distribution; the variance measured here is variance &lt;em&gt;under&lt;/em&gt; that constraint. Free-form review text may behave differently.&lt;/li&gt;
&lt;li&gt;No causal claims. Scope: free, local, small models, this corpus.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Generation is nondeterministic and &lt;em&gt;not&lt;/em&gt; the reproduction target; the frozen per-run records are. The deterministic layer regenerates every table in this article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/axiom-pro/llm-audit-nondeterminism
&lt;span class="nb"&gt;cd &lt;/span&gt;llm-audit-nondeterminism

&lt;span class="c"&gt;# Layer 2 (deterministic, no LLM needed): regenerate all numbers from frozen records&lt;/span&gt;
python scripts/aggregate.py            &lt;span class="c"&gt;# -&amp;gt; results/m2_summary.json&lt;/span&gt;
python scripts/verify_independent.py   &lt;span class="c"&gt;# independent recomputation of every number quoted in the text — should print ALL PASS&lt;/span&gt;
python scripts/make_figures.py         &lt;span class="c"&gt;# -&amp;gt; figures/nofreelunch.png (needs Matplotlib)&lt;/span&gt;

&lt;span class="c"&gt;# Layer 1 (optional, needs Ollama + ~5GB VRAM for 7B Q4_K_M):&lt;/span&gt;
&lt;span class="c"&gt;#   regenerating gives you a *different* distribution — that's the point&lt;/span&gt;
&lt;span class="c"&gt;#   OLLAMA_EXE=/path/to/ollama bash scripts/run_m2.sh&lt;/span&gt;
&lt;span class="c"&gt;#   python scripts/seed_probe.py       # confirm seeds have no observable effect at temp 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Numerical aggregation and verification need Python 3.12 and only the standard library; figure regeneration additionally needs Matplotlib. A few seconds total. (On Windows, set &lt;code&gt;PYTHONUTF8=1&lt;/code&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pin everything and local LLM review was deterministic on this rig&lt;/strong&gt; — including deterministically repeating the same misses. Determinism ≠ correctness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One review run is a sample, not a verdict.&lt;/strong&gt; At practical temperatures the finding set moves between runs, mostly by line drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Majority voting measures agreement strength, not truth.&lt;/strong&gt; Use multi-run review as a candidate generator; adjudicate with tests, static analysis, and humans.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you remember one sentence, please don't make it "LLM code review is useless" — that's not what the data says. Make it: &lt;strong&gt;multi-run review is a decent candidate generator; what's unsafe is automatic adjudication over a brittle vote key.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All per-run records (normalized findings + response hashes), ground truth, aggregation code, and the independent verification script: &lt;a href="https://github.com/axiom-pro/llm-audit-nondeterminism" rel="noopener noreferrer"&gt;github.com/axiom-pro/llm-audit-nondeterminism&lt;/a&gt;. Anything not measured (other GPUs, quantizations, larger models) is listed as not measured.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is an English adaptation of &lt;a href="https://zenn.dev/tauridev/articles/llm-audit-nondeterminism" rel="noopener noreferrer"&gt;my Japanese article on Zenn&lt;/a&gt; — written by me in Japanese, restructured and translated with AI assistance, human-reviewed. If you spot an error, comments and issues are open; I'll verify against the frozen records and correct with a changelog.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About me:&lt;/strong&gt; I build developer tools in Rust/Tauri and measure AI-assisted development practices with frozen records and independent recomputation — GitHub: &lt;a href="https://github.com/axiom-pro" rel="noopener noreferrer"&gt;axiom-pro&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>codereview</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
