VKVlad KulikovPublic / lectures
RL for LLM12 / 13

Module 12. RL infrastructure for LLMs: memory, rollouts, and asynchrony

Try each exercise first. Expand worked answers or project guidance when needed; their open state is shared between RU/EN.

Download course files (.zip) 382.1 KB

The archive preserves the source folder structure, both languages, and supporting files.

Logical roles and processes

In one experiment, a single process alternates among generation, reward computation, and an optimizer step. In another, four services perform these operations.

  1. Name the four logical roles in the RL loop.

  2. Must they be four separate processes?

  3. Which data invariant must survive every placement?

  4. Why are an answer string and one reward number an insufficient rollout contract?

Show answer and solution

The four roles are:

  1. the trainer, which computes losses, gradients, and optimizer steps;

  2. the rollout engine, which samples policy actions and owns the KV cache;

  3. the verifier or environment, which produces an outcome and operational status;

  4. the orchestrator, which selects prompts, assembles groups, and manages queues, versions, and recovery.

They need not be separate processes. One GPU may alternate among all roles, while one logical role may span many workers on a cluster.

The invariant is that each training record unambiguously associates actions with the behavior policy that produced them and reward with the verifier or environment version that produced it.

A string and scalar reward omit tokenization, the policy-token mask, behavior log-probabilities, weight version, decoding parameters, stop reason, and reward provenance. The loss and the failure cannot be reconstructed reliably.

Two independent axes

A system uses a separate vLLM server. The trainer requests one batch, waits for the complete batch, trains, synchronizes weights, and only then requests the next batch.

  1. Is placement colocated or disaggregated?

  2. Is the logical algorithm synchronous or asynchronous?

  3. Why is “a separate server automatically makes the algorithm asynchronous” false?

  4. Give one example of colocation with buffering and one example of disaggregation with a strict barrier.

Show answer and solution
  1. Placement is disaggregated because generation runs on a separate server.

  2. The logical protocol is synchronous because every batch ends at a barrier.

  3. Asynchrony is defined by production and consumption order, not by a network address. A separate server can serve a strictly sequential loop.

  4. Colocation with buffering: one graphics processing unit (GPU) alternates between rollout and training while the CPU prepares the next prompt batch. Disaggregation with a barrier: a dedicated vLLM pool generates a batch, then waits until training and weight loading finish.

Memory profile

A colleague says, “Adam always needs 16 bytes per parameter, so no other accounting is required.”

  1. Explain which objects may make up these 16 bytes.

  2. Name at least four reasons the actual profile can differ.

  3. Why does dividing logical model-state memory by the number of Fully Sharded Data Parallel (FSDP) participants not give the exact peak?

  4. What does LoRA change, and what does it not remove?

Show answer and solution

A common 16-byte profile is:

  • 2 bytes for a bfloat16 parameter;
  • 2 bytes for a bfloat16 gradient;
  • 8 bytes for two float32 Adam moments;
  • 4 bytes for a separate float32 master copy.

The profile changes with gradient precision, whether a master copy exists, optimizer type, state quantization, CPU offload, adapter-only training, sharding, and implementation details.

Dividing by the FSDP group size ignores replicated objects, temporary module gathering, communication buffers, activations, logits, allocator fragmentation, and transfer phases.

LoRA reduces parameters with gradients and optimizer state. It does not remove base weights, forward activations, the KV cache, temporary buffers, or the requirement to publish a consistent adapter version to the rollout engine.

KV cache and rollout length

The Group Relative Policy Optimization (GRPO) group size is increased from 8 to 32 with the same maximum completion length.

  1. How does useful key–value cache (KV-cache) memory change if all sequences are active concurrently?

  2. Why may observed memory growth differ from the ideal linear formula?

  3. Name two engine mechanisms that reduce fragmentation or repeated work but do not eliminate stored keys and values.

  4. Why can a shared-prefix cache not be reused across a weight update without validation?

Show answer and solution
  1. For Group Relative Policy Optimization (GRPO), useful payload grows by a factor of four when concurrent sequences grow from 8 to 32.

  2. Actual growth depends on early completion, length distribution, allocator pages, tensor parallelism, reserve, and prefix reuse.

  3. PagedAttention reduces managed-cache fragmentation; prefix caching or RadixAttention reuses the common prompt prefix.

  4. Keys and values depend on model parameters. A cache computed by the old weights is not a cache of the new policy.

Atomic policy version

A long completion starts under version 120. Halfway through, the engine loads version 121. The log stores only policy_version=121 and the final text.

  1. What is wrong with this contract?

  2. Name two safe update regimes.

  3. What data are required if mid-completion updates are allowed anyway?

  4. Why do identical weights with different top_p or temperature still define different behavior policies?

Show answer and solution

The record assigns one version to a completion generated by a mixture of two weight sets. The behavior-policy denominator and replay semantics are therefore ambiguous.

Safe regimes include:

  • let in-flight completions finish under version 120 and switch only new requests;
  • record a version and behavior log-probabilities per action or turn when version changes are an explicit part of the protocol.

A mid-completion update requires token-level versions, actual log-probabilities after decoding transforms, an atomic update order, and an action mask.

Temperature, top-p, and logit masks transform the distribution after the weights. Identical transformers can therefore define different behavior policies.

Lag and mismatch

Immediately after synchronization, the rollout engine and trainer hold the same weight version, but their token log-probabilities differ slightly. Ten optimizer steps later, the queue contains rollouts that are eight versions old.

  1. What is the first problem called?

  2. What is the second problem called?

  3. Which control experiment separates the two causes?

  4. Why is effective sample size (ESS) a diagnostic rather than proof that an update is unbiased?

Show answer and solution

The first problem is training–inference mismatch (TIM): the declared version is the same, but numerical probabilities differ.

The second is policy lag: the data were generated by an older version.

The control is a zero-lag comparison immediately after synchronization: score the same token sequence with the engine and trainer and compare token log-probabilities. Then vary version age separately.

ESS measures weight concentration. It does not prove that the denominator is correct, clipping is unbiased, masks are valid, or the chosen off-policy estimator matches the target objective.

Queue and long tails

Rollout workers produce 14 million tokens per minute, while the trainer sustainably consumes 10 million. At the same time, the 99th percentile (p99) of rollout duration is six times the median.

  1. Which first-order queue-stability condition is violated?

  2. What is backpressure in this system?

  3. Why is a limit on record count weaker than a limit on tokens or estimated cost?

  4. Why can unconditional removal of the slowest episodes shift the training distribution?

Show answer and solution
  1. The condition λ<μ\lambda<\mu is violated: 14 million tokens/min arrive and only 10 million are consumed.

  2. Backpressure slows or pauses rollout workers when a queue threshold is reached.

  3. Records can differ by orders of magnitude in length. Record count is a weak proxy for memory, compute, and age; token or cost limits follow physical load more closely.

  4. Long episodes can be systematically harder. Removing them shifts the task distribution toward short and easy cases.

Checkpoint and framework choice

A team saves only model.safetensors, then restarts with a zeroed optimizer and a new verifier version. The report calls this a continuation of the same run.

  1. Why is that inaccurate?

  2. List at least eight components of a semantically complete RL checkpoint.

  3. How should recovery be tested before a real failure?

  4. Why cannot TRL, verl, OpenRLHF, NeMo RL, PRIME-RL, SkyRL, and AReaL be placed on one “better–worse” line without a task contract?

Show answer and solution

This is a new run initialized from old weights, not a continuation of the previous optimization process. Adam moments, scheduler state, and reward contract changed.

A semantically complete checkpoint commonly includes:

  • model and adapter weights;
  • optimizer state;
  • learning-rate scheduler;
  • global step and policy version;
  • random-number generator states;
  • dataset cursor;
  • curriculum or prompt-scheduler state;
  • reward and advantage normalization;
  • tokenizer and chat-template revisions;
  • verifier and environment versions;
  • decoding configuration;
  • rollout schema;
  • distributed layout and queue-recovery policy.

Recovery should be tested by saving, terminating processes deliberately, restoring, executing a control batch, and checking versions, tokenization, finite losses, and expected statistics.

Frameworks optimize different levels: a ready-made trainer, role orchestration, a fully asynchronous dataflow, agent environments, or a particular distributed backend. There is no one-dimensional ranking without model size, scale, synchrony, environment type, fault-tolerance requirements, and team expertise.

Full training and adapters

A model has 7⋅1097\cdot10^9 parameters. Base weights are bfloat16, gradients are bfloat16, two Adam moments are float32, and a separate master copy is float32.

  1. Find bytes per trainable parameter.

  2. Find logical full-training memory in GB.

  3. Find memory of base bfloat16 weights alone in GB.

  4. LoRA adds and trains 20⋅10620\cdot10^6 adapter parameters while all base weights remain resident. Find logical memory with the same adapter-state profile.

  5. Why is none of these values the GPU-memory peak?

Show answer and solution

Bytes per trainable parameter are

2+2+4+4+4=16.2+2+4+4+4=16.

Full training requires

7⋅109⋅16=112⋅109 bytes=112 GB.7\cdot10^9\cdot16 =112\cdot10^9\ \text{bytes} =112\ \text{GB}.

Base bfloat16 weights require

7⋅109⋅2=14 GB.7\cdot10^9\cdot2=14\ \text{GB}.

With LoRA, base weights still take 14 GB. Each of 20 million trainable parameters requires the adapter weight itself, a gradient, two moments, and a master copy:

2+2+8+4=16 bytes.2+2+8+4=16\ \text{bytes}.

The extra amount is

20⋅106⋅16=0.32 GB,20\cdot10^6\cdot16=0.32\ \text{GB},

for a total of

14.32 GB.14.32\ \text{GB}.

The peak also contains activations, temporary buffers, logits, KV cache, fragmentation, replicas, and weight-transfer phases.

KV cache

Use the teaching architecture

L=32,Hkv=8,dh=128,b=2 bytes.L=32,\qquad H_{kv}=8,\qquad d_h=128,\qquad b=2\ \text{bytes}.
  1. How many bytes do keys and values require for one cached token?

  2. How many KiB is that?

  3. Find memory for 64 sequences of 4096 tokens.

  4. Express the result in GiB.

  5. What is the idealized local payload under uniform KV sharding across four tensor-parallel participants?

Show answer and solution

One cached token requires

2LHkvdhb=2⋅32⋅8⋅128⋅2=131072 bytes.2LH_{kv}d_hb =2\cdot32\cdot8\cdot128\cdot2 =131072\ \text{bytes}.

That is

131072/1024=128 KiB.131072/1024=128\ \text{KiB}.

The total token count is

64⋅4096=262144.64\cdot4096=262144.

Memory is

262144⋅131072=34359738368 bytes=32 GiB.262144\cdot131072 =34359738368\ \text{bytes} =32\ \text{GiB}.

Ideal uniform sharding across four participants gives

32/4=8 GiB per participant.32/4=8\ \text{GiB per participant}.

This is useful payload without pages, block tables, alignment, or temporary tensors.

Sequential and overlapped pipeline

Generation plus verification takes G=90G=90 s per batch. Training plus synchronization takes T=10T=10 s.

  1. Find the duration of one sequential batch.

  2. Find the ideal two-stage steady-state period.

  3. Find total time for 20 sequential batches.

  4. Find total time for 20 batches in the ideal pipeline.

  5. Find full-run speedup and asymptotic period speedup.

  6. Why is the speedup far below two?

Show answer and solution

One sequential batch takes

G+T=90+10=100 s.G+T=90+10=100\ \text{s}.

The steady-state period is

max⁡(G,T)=90 s.\max(G,T)=90\ \text{s}.

Twenty sequential batches take

20⋅100=2000 s.20\cdot100=2000\ \text{s}.

The ideal pipeline takes

G+T+(20−1)max⁡(G,T)=100+19⋅90=1810 s.G+T+(20-1)\max(G,T) =100+19\cdot90 =1810\ \text{s}.

Full-run speedup is

2000/1810≈1.105,2000/1810\approx1.105,

and asymptotic period speedup is

100/90≈1.111.100/90\approx1.111.

The stages are unbalanced. Ten seconds of training cannot hide 90 seconds of rollout work.

A slow rollout

Eight concurrent rollouts take

[4,4,5,5,5,6,6,20] s.[4,4,5,5,5,6,6,20]\ \text{s}.
  1. Find total work in GPU-seconds.

  2. Find synchronous group time.

  3. Find rectangle capacity: eight workers times group time.

  4. Find utilization and idle fraction.

  5. How does utilization change if the 20-second rollout can safely be truncated at 8 seconds? Why is such truncation not always statistically neutral?

Show answer and solution

Total work is

4+4+5+5+5+6+6+20=55 GPU-s.4+4+5+5+5+6+6+20=55\ \text{GPU-s}.

The synchronous group ends after

max⁡iTi=20 s.\max_iT_i=20\ \text{s}.

Capacity is

8⋅20=160 GPU-s.8\cdot20=160\ \text{GPU-s}.

Utilization is

U=55/160=0.34375,U=55/160=0.34375,

and idle fraction is

1−U=0.65625.1-U=0.65625.

After safely truncating the final rollout at eight seconds, work is 43 GPU-s, wall time is 8 s, and capacity is 64 GPU-s:

U′=43/64=0.671875.U'=43/64=0.671875.

Utilization improves, but if long episodes are systematically hard, truncation changes the observed task and reward distribution.

Probability ratios and ESS

For four sequences, log importance ratios are

ℓ=(0,log⁡2,log⁡2,log⁡8).\ell=(0,\log 2,\log 2,\log 8).
  1. Find wi=exp⁡(ℓi)w_i=\exp(\ell_i).

  2. Find ESS.

  3. Find normalized ESS.

  4. Compare with four equal weights.

  5. What changes if the final log ratio is clipped above at log⁡2\log2?

  6. What do these values reveal, and what do they not prove?

Show answer and solution

Weights are

w=(1,2,2,8).w=(1,2,2,8).

Their sum is 13 and squared sum is

1+4+4+64=73.1+4+4+64=73.

Thus

ESS⁡=13273=16973≈2.315.\operatorname{ESS} =\frac{13^2}{73} =\frac{169}{73} \approx2.315.

Normalized ESS is

2.315/4≈0.579.2.315/4\approx0.579.

Four equal weights have ESS 4 and normalized ESS 1.

After clipping the final log ratio at log⁡2\log2, weights are (1,2,2,2)(1,2,2,2):

ESS⁡=721+4+4+4=4913≈3.769,\operatorname{ESS} =\frac{7^2}{1+4+4+4} =\frac{49}{13} \approx3.769,

with normalized ESS about 0.942.

Clipping reduces concentration but changes the estimator. ESS describes weight geometry; it does not prove that the objective or policy quality is correct.

Synchronization and checkpoints

Weight synchronization takes 8 seconds. In a teaching approximation, one unit of mean lag costs one second per update. Candidate synchronization intervals are K∈{1,2,4,8,16}K\in\{1,2,4,8,16\}, with

C(K)=8K+K−12.C(K)=\frac{8}{K}+\frac{K-1}{2}.
  1. Compute C(K)C(K) for all candidates and choose the minimum.

  2. What are maximum and stationary mean lag at the selected KK?

  3. For ten updates, write the exact lag sequence and its mean.

Separately, a checkpoint takes C=120C=120 s and mean time between failures is M=86400M=86400 s.

  1. Find the approximate optimum
I∗=2CM.I^*=\sqrt{2CM}.
  1. Convert it to minutes.

  2. Which important costs are absent from this model?

Show answer and solution
KK8/K8/K(K−1)/2(K-1)/2C(K)C(K)
18.00.08.0
24.00.54.5
42.01.53.5
81.03.54.5
160.57.58.0

The minimum among these candidates is K=4K=4.

The stationary lag cycle is (0,1,2,3)(0,1,2,3), so maximum lag is 3 and mean lag is

(0+1+2+3)/4=1.5.(0+1+2+3)/4=1.5.

For ten updates:

(0,1,2,3,0,1,2,3,0,1),(0,1,2,3,0,1,2,3,0,1),

with exact finite-run mean 13/10=1.313/10=1.3.

The checkpoint interval is

I∗=2⋅120⋅86400≈4553.68 s≈75.89 min.I^*=\sqrt{2\cdot120\cdot86400} \approx4553.68\ \text{s} \approx75.89\ \text{min}.

The model omits restart time, asynchronous writes, correlated failures, storage and network cost, partially lost queues, distributed-layout incompatibility, and time-varying risk.

Model-state and KV-cache memory

Implement:

Python
def model_state_bytes(
    total_parameters: int,
    trainable_parameters: int,
    parameter_bytes: int,
    gradient_bytes: int,
    optimizer_state_bytes: int,
    master_parameter_bytes: int,
) -> int:
    ...


def kv_cache_bytes(
    num_sequences: int,
    tokens_per_sequence: int,
    num_layers: int,
    num_kv_heads: int,
    head_dim: int,
    element_bytes: int,
) -> int:
    ...

Validate dimensions, require 0 <= trainable_parameters <= total_parameters, and reproduce B1–B2.

Open checks:

Python
profile = model_state_bytes(
    7_000_000_000,
    trainable_parameters=7_000_000_000,
    parameter_bytes=2,
    gradient_bytes=2,
    optimizer_state_bytes=8,
    master_parameter_bytes=4,
)
assert profile == 112_000_000_000
lora = model_state_bytes(
    7_020_000_000,
    trainable_parameters=20_000_000,
    parameter_bytes=2,
    gradient_bytes=2,
    optimizer_state_bytes=8,
    master_parameter_bytes=4,
)
assert lora == 14_320_000_000
assert lora < profile
Show answer and solution
Python
def model_state_bytes(
    total_parameters,
    trainable_parameters,
    parameter_bytes,
    gradient_bytes,
    optimizer_state_bytes,
    master_parameter_bytes,
):
    integers = [
        total_parameters,
        trainable_parameters,
        parameter_bytes,
        gradient_bytes,
        optimizer_state_bytes,
        master_parameter_bytes,
    ]
    if any(isinstance(x, bool) or not isinstance(x, int) for x in integers):
        raise ValueError("all arguments must be integers")
    if total_parameters <= 0 or not 0 <= trainable_parameters <= total_parameters:
        raise ValueError("invalid parameter counts")
    if min(integers[2:]) < 0:
        raise ValueError("byte counts must be non-negative")

    base = total_parameters * parameter_bytes
    trainable_extra = trainable_parameters * (
        gradient_bytes + optimizer_state_bytes + master_parameter_bytes
    )
    return base + trainable_extra


def kv_cache_bytes(
    num_sequences,
    tokens_per_sequence,
    num_layers,
    num_kv_heads,
    head_dim,
    element_bytes,
):
    values = [
        num_sequences,
        tokens_per_sequence,
        num_layers,
        num_kv_heads,
        head_dim,
        element_bytes,
    ]
    if any(isinstance(x, bool) or not isinstance(x, int) or x <= 0 for x in values):
        raise ValueError("all dimensions must be positive integers")
    return (
        2
        * num_sequences
        * tokens_per_sequence
        * num_layers
        * num_kv_heads
        * head_dim
        * element_bytes
    )
Python
assert model_state_bytes(7_000_000_000, 7_000_000_000, 2, 2, 8, 4) == 112_000_000_000
assert model_state_bytes(7_020_000_000, 20_000_000, 2, 2, 8, 4) == 14_320_000_000
assert kv_cache_bytes(64, 4096, 32, 8, 128, 2) == 32 * 1024**3

Pipeline time

Implement:

Python
def pipeline_report(rollout_seconds: float, train_seconds: float, batches: int) -> dict[str, float]:
    """Return sequential_total, pipeline_total, period, and speedup."""

For G=90G=90, T=10T=10, and n=20n=20, return 2000 seconds sequentially and 1810 seconds in the ideal pipeline.

Open checks:

Python
pipe_one = pipeline_report(90.0, 10.0, 1)
assert pipe_one["pipeline_total"] == 100.0
assert pipe_one["speedup"] == 1.0
Show answer and solution
Python
def pipeline_report(rollout_seconds, train_seconds, batches):
    if rollout_seconds <= 0 or train_seconds <= 0:
        raise ValueError("stage times must be positive")
    if isinstance(batches, bool) or not isinstance(batches, int) or batches <= 0:
        raise ValueError("batches must be a positive integer")

    sequential = batches * (rollout_seconds + train_seconds)
    period = max(rollout_seconds, train_seconds)
    pipelined = rollout_seconds + train_seconds + (batches - 1) * period
    return {
        "sequential_total": sequential,
        "pipeline_total": pipelined,
        "period": period,
        "speedup": sequential / pipelined,
    }

Importance weights and ESS

Implement:

Python
def importance_weights(current_logprobs, behavior_logprobs, max_log_ratio=None):
    ...


def effective_sample_size(weights, normalized=False):
    ...

Requirements:

  • inputs are non-empty one-dimensional arrays of equal length;
  • log-probabilities are finite and non-positive;
  • weights are non-negative;
  • zero squared-weight sum raises ValueError;
  • optional clipping happens in log-ratio space.

Open checks:

Python
new = np.array([-0.4, -0.6])
old = np.array([-0.5, -0.5])
w = importance_weights(new, old)
assert np.allclose(w, np.exp(new - old))
assert np.allclose(importance_weights(old, old), 1.0)
Show answer and solution
Python
import numpy as np


def importance_weights(current_logprobs, behavior_logprobs, max_log_ratio=None):
    current = np.asarray(current_logprobs, dtype=float)
    behavior = np.asarray(behavior_logprobs, dtype=float)
    if current.ndim != 1 or current.shape != behavior.shape or current.size == 0:
        raise ValueError("inputs must be non-empty 1-D arrays of equal shape")
    if not np.isfinite(current).all() or not np.isfinite(behavior).all():
        raise ValueError("log probabilities must be finite")
    if np.any(current > 0) or np.any(behavior > 0):
        raise ValueError("log probabilities must be non-positive")

    log_ratio = current - behavior
    if max_log_ratio is not None:
        if not np.isfinite(max_log_ratio):
            raise ValueError("max_log_ratio must be finite")
        log_ratio = np.minimum(log_ratio, max_log_ratio)
    return np.exp(log_ratio)


def effective_sample_size(weights, normalized=False):
    w = np.asarray(weights, dtype=float)
    if w.ndim != 1 or w.size == 0 or not np.isfinite(w).all():
        raise ValueError("weights must be a finite non-empty vector")
    if np.any(w < 0):
        raise ValueError("weights must be non-negative")
    denom = float(np.dot(w, w))
    if denom == 0:
        raise ValueError("at least one weight must be positive")
    ess = float(w.sum() ** 2 / denom)
    return ess / w.size if normalized else ess

Rollout-contract validation

Return a list of errors for a record with required fields:

text
sample_id, prompt_id, policy_version,
prompt_tokens, response_tokens, behavior_logprobs,
decoding, reward, stop_reason, verifier_version

Validate types, non-negative versions and token IDs, finite log-probabilities, and alignment with response_tokens.

Signature:

Python
def rollout_contract_errors(record: Mapping[str, Any]) -> list[str]:
    ...

Connection to earlier practice. Compare this single-completion record with the multi-turn trajectory from Module 11, Task C5 (validate_rollout_record). Reuse validation rules, but explicitly adapt the schema: adding only sample_id, verifier_version, and decoding is insufficient. Describe the field mapping and the information that must remain in the original agent log.

Open checks:

Python
record = {
    "sample_id": "s1", "prompt_id": "q1", "policy_version": 1,
    "prompt_tokens": [1, 2], "response_tokens": [3, 4],
    "behavior_logprobs": [-0.1, -0.2], "decoding": {"temperature": 1.0},
    "reward": {"task": 1.0}, "stop_reason": "eos", "verifier_version": "v1",
}
assert rollout_contract_errors(record) == []
assert rollout_contract_errors({}) != []
assert any("missing fields" in e for e in rollout_contract_errors({}))
Show answer and solution
Python
import math

REQUIRED = {
    "sample_id", "prompt_id", "policy_version",
    "prompt_tokens", "response_tokens", "behavior_logprobs",
    "decoding", "reward", "stop_reason", "verifier_version",
}


def rollout_contract_errors(record):
    if not isinstance(record, dict):
        return ["record must be a dict"]

    errors = []
    missing = sorted(REQUIRED - set(record))
    if missing:
        errors.append("missing fields: " + ", ".join(missing))

    version = record.get("policy_version")
    if isinstance(version, bool) or not isinstance(version, int) or version < 0:
        errors.append("policy_version must be a non-negative integer")

    for key in ("prompt_tokens", "response_tokens"):
        tokens = record.get(key)
        if not isinstance(tokens, list) or any(
            isinstance(token, bool) or not isinstance(token, int) or token < 0
            for token in (tokens or [])
        ):
            errors.append(f"invalid {key}")

    logps = record.get("behavior_logprobs")
    if not isinstance(logps, list) or any(
        not isinstance(value, (int, float))
        or not math.isfinite(value)
        or value > 0
        for value in (logps or [])
    ):
        errors.append("invalid behavior_logprobs")
    elif isinstance(record.get("response_tokens"), list) and len(logps) != len(record["response_tokens"]):
        errors.append("logprobs do not align with response_tokens")

    return errors

In Module 11, token_ids, policy_mask, and turn_ids describe a whole trajectory, while policy_versions identifies its turns. Here select one completion generated by one behavior-policy version: prompt_tokens is its generation context, response_tokens contains the policy-selected tokens in that completion, and behavior_logprobs aligns with those tokens. The single numeric policy_version requires an explicit version registry; do not silently coerce a version string into a number. Assign prompt_id when constructing the prompt. Mapping rewards to reward and choosing stop_reason require declared credit-assignment and stopping rules for this completion; do not automatically copy the episode outcome to every turn. Environment IDs, actions, observations, token provenance, and trajectory-level flags remain in the original log linked to the infrastructure record. This is a projection onto a different data unit, not a replacement for the full agentic contract.

Periodic synchronization and checkpoints

Implement:

Python
def synchronization_schedule(sync_every: int, updates: int) -> list[int]:
    """Return 0, 1, ..., sync_every-1 with periodic reset."""


def optimal_checkpoint_interval(checkpoint_seconds: float, mtbf_seconds: float) -> float:
    ...

Reproduce B6 and test sync_every=1 and updates=1.

Open checks:

Python
schedule = synchronization_schedule(4, 10)
assert schedule == [0, 1, 2, 3, 0, 1, 2, 3, 0, 1]
Show answer and solution
Python
from math import sqrt


def synchronization_schedule(sync_every, updates):
    if isinstance(sync_every, bool) or not isinstance(sync_every, int) or sync_every <= 0:
        raise ValueError("sync_every must be positive")
    if isinstance(updates, bool) or not isinstance(updates, int) or updates <= 0:
        raise ValueError("updates must be positive")
    return [step % sync_every for step in range(updates)]


def optimal_checkpoint_interval(checkpoint_seconds, mtbf_seconds):
    if checkpoint_seconds <= 0 or mtbf_seconds <= 0:
        raise ValueError("times must be positive")
    return sqrt(2 * checkpoint_seconds * mtbf_seconds)

Diagnosis from measurements

Implement a rule that receives

Python
metrics = {
    "trainer_idle_fraction": ...,
    "rollout_idle_fraction": ...,
    "verifier_fraction": ...,
    "weight_sync_fraction": ...,
    "queue_growth_per_minute": ...,
    "normalized_ess": ...,
    "oom_events": ...,
}

and returns testable hypotheses, not final diagnoses. For example:

  • high trainer idle time with an empty queue → inspect rollout, verification, and prompt supply;
  • high synchronization fraction → measure transfer and weight conversion;
  • low ESS → separate policy lag from training–inference mismatch;
  • out-of-memory events → build a phase-specific memory ledger.

Add range checks and a neutral test where no dominant bottleneck is identified.

Open checks:

Python
neutral = diagnose_pipeline({
    "trainer_idle_fraction": 0.05, "rollout_idle_fraction": 0.05,
    "verifier_fraction": 0.05, "weight_sync_fraction": 0.05,
    "queue_growth_per_minute": 0.0, "normalized_ess": 0.9, "oom_events": 0,
})
assert neutral == ["inspect a stage-level trace"]
starved = diagnose_pipeline({
    "trainer_idle_fraction": 0.6, "rollout_idle_fraction": 0.05,
    "verifier_fraction": 0.1, "weight_sync_fraction": 0.05,
    "queue_growth_per_minute": 0.0, "normalized_ess": 0.9, "oom_events": 0,
})
assert len(starved) == 1
Show answer and solution
Python
def diagnose_pipeline(metrics):
    required = {
        "trainer_idle_fraction",
        "rollout_idle_fraction",
        "verifier_fraction",
        "weight_sync_fraction",
        "queue_growth_per_minute",
        "normalized_ess",
        "oom_events",
    }
    missing = required - set(metrics)
    if missing:
        raise ValueError("missing metrics: " + ", ".join(sorted(missing)))

    bounded = required - {"queue_growth_per_minute", "oom_events"}
    for key in bounded:
        if not 0 <= metrics[key] <= 1:
            raise ValueError(f"{key} must lie in [0, 1]")
    if metrics["oom_events"] < 0:
        raise ValueError("oom_events must be non-negative")

    hypotheses = []
    if metrics["trainer_idle_fraction"] > 0.25 and metrics["queue_growth_per_minute"] <= 0:
        hypotheses.append("check rollout, verifier, and input supply")
    if metrics["verifier_fraction"] > 0.30:
        hypotheses.append("profile the verifier critical path")
    if metrics["weight_sync_fraction"] > 0.20:
        hypotheses.append("profile weight transfer and conversion")
    if metrics["normalized_ess"] < 0.50:
        hypotheses.append("separate policy lag from training-inference mismatch")
    if metrics["oom_events"] > 0:
        hypotheses.append("measure phase-specific peaks, KV cache, activations, and transfer buffers")
    if not hypotheses:
        hypotheses.append("inspect a stage-level trace")
    return hypotheses

The important design choice is that outputs are hypotheses that can be tested with a targeted measurement. A single aggregate metric rarely identifies a root cause.