Test-Time Compute › Code Implementation: Multiple Sampling with Majority Vote
Language AI HandbookAll chapters Test-Time Compute › Code Implementation: Multiple Sampling with Majority VoteOne section · Sections in this chapter6 / 10 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let's implement the core ideas from this chapter: generating multiple samples from a model, evaluating them, and selecting via majority vote. We will use a toy setting with a simulated model to keep the code runnable, but the patterns apply directly to production-scale systems.
Setting Up the Sampling Framework
We start by defining a simple model wrapper and the core sampling loop.
import random
# Simulate a model that answers math questions with a given per-question accuracy.
# In a real implementation, this would call an API or local inference endpoint.
class SimulatedModel:
"""Simulate a language model with known per-question accuracy."""
def __init__(self, true_accuracy: float, seed: int = 42):
self.true_accuracy = true_accuracy
self.rng = random.Random(seed)
def sample(self, question: str, n_samples: int = 1) -> list:
"""Generate n_samples independent answers, each correct with probability p."""
answers = []
for _ in range(n_samples):
if self.rng.random() < self.true_accuracy:
answers.append("correct")
else:
# Wrong answers are diverse; only one is correct.
wrong_options = ["wrong_a", "wrong_b", "wrong_c"]
answers.append(self.rng.choice(wrong_options))
return answersSimulatedModel class defined. This simulates a model with a fixed per-question accuracy. In a real system, 'sample' would call an LLM API.
The simulated model gives us reproducible experiments. Each call to sample is an
independent forward pass through the model, returning a list of answers. In a real
system, you would replace sample with an asynchronous batch request to the model's
API, enabling all candidates to be generated in parallel rather than sequentially.
The logic of majority voting and pass@k computation is identical whether samples are
generated locally or through a remote API.
Majority Voting Implementation
With candidate answers in hand, we implement majority voting to select the best response.
def majority_vote(answers: list) -> str:
"""Return the most common answer from a list of candidates."""
counter = Counter(answers)
return counter.most_common(1)[0][0]
def evaluate_majority_vote(
model, n_questions: int, n_samples: int, seed: int = 0
) -> float:
"""Estimate accuracy using majority vote over n_samples per question."""
rng = random.Random(seed)
correct_count = 0
for _ in range(n_questions):
candidates = model.sample("question", n_samples)
selected = majority_vote(candidates)
if selected == "correct":
correct_count += 1
return correct_count / n_questionsThe majority_vote function uses Python's Counter to find the most frequent answer
in time. For numerical answers in real math problems, you would want to normalize
the answer strings first, since the model might express the same number as "1/2", "0.5",
or "0.50". Standardizing the answer format before voting is an important practical step
that substantially improves the reliability of majority voting in production.
Theoretical pass@k
We also implement the exact pass@k formula for comparison with the approximation.
def pass_at_k_exact(n: int, c: int, k: int) -> float:
"""
Compute exact pass@k given n total samples and c correct ones.
Formula: pass@k = 1 - C(n-c, k) / C(n, k)
Args:
n: Total number of generated samples.
c: Number of correct samples among the n.
k: Number of samples submitted (the budget).
Returns:
Probability that at least one of the k selected samples is correct.
"""
if n - c < k:
return 1.0
return 1.0 - comb(n - c, k) / comb(n, k)
def expected_pass_at_k(
p: float, n: int, k: int, n_questions: int = 10000
) -> float:
"""Estimate E[pass@k] over many questions, each with P(correct) = p."""
rng = np.random.default_rng(42)
total = 0.0
for _ in range(n_questions):
c = int(rng.binomial(n, p))
total += pass_at_k_exact(n, c, k)
return total / n_questionsPer-sample accuracy: 10%
k pass@k (approx) pass@k (exact sim)
--------------------------------------------------
1 0.100 0.099
2 0.190 0.187
5 0.410 0.406
10 0.651 0.647
20 0.878 0.879
50 0.995 0.994
100 1.000 1.000Even with only 10% per-sample accuracy, generating 50 samples and checking any correct one yields over 99% pass@50. This is the core power of test-time compute for verifiable tasks. Notice that the approximation and the exact simulation agree closely, confirming that the simple formula captures the essential behavior.
The table also reveals the law of diminishing returns in action. Moving from to gives a gain of about 0.41 percentage points, but moving from to gives only about 0.01 percentage points. The first few samples do the heavy lifting; later samples mostly confirm what you already know.
Comparing Selection Strategies
Let's compare majority voting against random selection (baseline) and oracle selection (upper bound) to understand the practical gain from majority voting.
def random_selection(answers: list) -> str:
"""Randomly select one answer (baseline)."""
return random.choice(answers)
def oracle_selection(answers: list) -> str:
"""Select the correct answer if it exists (upper bound)."""
if "correct" in answers:
return "correct"
return random.choice(answers)
def compare_strategies(
model, n_questions: int = 2000, sample_sizes: list = None
) -> dict:
"""
Compare three selection strategies across varying sample sizes.
Returns dict mapping strategy name to list of accuracies.
"""
if sample_sizes is None:
sample_sizes = [1, 2, 4, 8, 16, 32, 64]
results = {"random": [], "majority_vote": [], "oracle": []}
for n in sample_sizes:
counts = {"random": 0, "majority_vote": 0, "oracle": 0}
for _ in range(n_questions):
answers = model.sample("q", n)
for strategy, fn in [
("random", random_selection),
("majority_vote", majority_vote),
("oracle", oracle_selection),
]:
if fn(answers) == "correct":
counts[strategy] += 1
for key in results:
results[key].append(counts[key] / n_questions)
return results
# Run comparison
model = SimulatedModel(true_accuracy=0.25, seed=99)
sample_sizes = [1, 2, 4, 8, 16, 32, 64]
strategy_results = compare_strategies(
model, n_questions=2000, sample_sizes=sample_sizes
)Accuracy by strategy and sample size (model accuracy = 25%)
N random majority_vote oracle
---------------------------------------------------------
1 0.265 0.265 0.265
2 0.254 0.247 0.441
4 0.260 0.245 0.685
8 0.262 0.248 0.893
16 0.263 0.242 0.990
32 0.265 0.256 1.000
64 0.254 0.249 1.000Majority voting closes a large portion of the gap between random selection and the oracle. At N=64 with a 25% per-sample accuracy, majority voting approaches the oracle bound, while random selection is capped at the model's base accuracy. This gap represents the practical value of having a good selection mechanism: it is not free compute, but it is value realized by combining generation with a reliable selector.
The oracle upper bound also reveals the ceiling on what any selection mechanism can achieve. Once coverage is high, the gap between majority vote and oracle narrows, confirming that the bottleneck has shifted from selection quality to coverage. At very large , even a random selector would approach the oracle because almost every subset of the sample pool contains a correct answer.