Test-Time Compute › Code Implementation: Multiple Sampling with Majority Vote

Michael BrenndoerferMarch 27, 20266 min read
Language AI HandbookAll chapters
Part 66 of 67Frontier Methods of 2025
Chapter 502Test-Time Compute › Code Implementation: Multiple Sampling with Majority Vote
Reading section 6 of 10 · Code Implementation: Multiple Sampling with Majority Vote · 6 minSection 6 of 10 · 6 min

Book contents

Section 6 of 10 · 6 min read

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.

In[8]:
Code
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 answers
Out[9]:
Console
SimulatedModel 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 NN 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.

In[10]:
Code
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_questions

The majority_vote function uses Python's Counter to find the most frequent answer in O(N)O(N) 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.

In[11]:
Code
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_questions
Out[12]:
Console
Per-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.000

Even 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 1−(1−p)k1 - (1-p)^k captures the essential behavior.

The table also reveals the law of diminishing returns in action. Moving from k=1k = 1 to k=5k = 5 gives a gain of about 0.41 percentage points, but moving from k=50k = 50 to k=100k = 100 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.

In[13]:
Code
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
)
Out[14]:
Console
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.000

Majority 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 NN, even a random selector would approach the oracle because almost every subset of the sample pool contains a correct answer.