Axolotl developer on retainer: sample_packing: true allows cross-example attention in packed sequences — model conditions on preceding unrelated context during training — inference bleeds conversations — 3 weeks — Axolotl developer on monthly retainer

axolotl 0.4.x • Python 3.11 • PyTorch 2.3.x • transformers 4.39.x • Llama-3-8B • 50,000 instruction examples • context_length: 4096 • sample_packing: true • examples concatenated with pad_id=0 separator • attention mask spans full packed sequence — no cross-example blocking • model attends to preceding example tokens during training • inference generates text referencing undefined entities from packed neighbors • developer configures max_steps, batch_size, LoRA settings — 3 weeks • fix: sample_packing: false; group_by_length: true; verify attention mask • 1.5h retainer engagement

Northfield Instruct Fine-Tuning Ltd. was fine-tuning a Llama-3-8B model for instruction following on a 50,000-example dataset using Axolotl. Their examples averaged 800 tokens each, well below the 4096-token context window, so they enabled sample_packing: true to improve GPU utilization by filling each sequence slot completely. Training ran for 3 epochs, loss decreased from 2.4 to 0.71 — clearly the model was learning. After deployment, inference worked correctly for simple single-turn prompts. But on closer inspection, certain model responses contained references to topics, entities, or framings that were not present in the prompt. A response to a question about database indexing mentioned “the marketing campaign budget constraints from Q3” — information that was never in the prompt but appeared in a different training example. The outputs were fluent but topically contaminated. The developer adjusted temperature, top_p, and system prompts for three weeks without improvement, then engaged an Axolotl retainer developer.

The relevant section of their config.yaml at the time of the engagement:

# config.yaml — Northfield Instruct Fine-Tuning Ltd.
# axolotl 0.4.x, Python 3.11, PyTorch 2.3.x, transformers 4.39.x, 4-GPU A100

base_model: meta-llama/Llama-3-8B
model_type: LlamaForCausalLM
tokenizer_type: LlamaTokenizer

datasets:
  - path: northfield_instructions.jsonl
    type: alpaca

sequence_len: 4096
sample_packing: true        # ← PROBLEM: packs multiple examples per sequence
pad_to_sequence_len: true

micro_batch_size: 4
gradient_accumulation_steps: 2
num_epochs: 3
learning_rate: 2.0e-5

adapter: lora
lora_r: 16
lora_alpha: 16
lora_dropout: 0.05
lora_target_modules:
  - q_proj
  - v_proj

# flash_attention: false    # ← Not set; axolotl 0.4.x uses standard attention
# group_by_length: false    # ← Not set

Why sample_packing: true causes cross-example attention leakage

Sample packing is a training throughput optimization. Rather than padding each short example to sequence_len with wasted tokens, axolotl concatenates multiple examples end-to-end to fill the full context window. For a 4096-token context with 800-token average examples, roughly 5 examples fit per packed sequence. This is good for GPU efficiency — every token slot is used for real content.

The problem is in how the attention mask is constructed. In Axolotl 0.4.x with standard (non-flash) attention, the packed sequence is assembled with a simple causal attention mask that allows each position to attend to all preceding positions within the 4096-token window — regardless of example boundaries. Position 1,200 (the beginning of the second example) can attend back to position 100 (from the first example). Position 3,500 (from the fourth example) can attend to position 800 (from the second example). There are no “walls” between examples at the attention level.

During training, the model is not just learning to generate good responses — it is learning that the preceding tokens in the packed sequence contain relevant context. After 3 epochs on 50,000 examples across millions of packed sequences, the model has developed a consistent prior: “there is meaningful context before me in the sequence.” At inference, that prior fires even when there is no such context, causing the model to hallucinate content that matches what would plausibly have been packed before the current example.

# Diagnosing cross-example attention in packed training batches
# Run after axolotl preprocess, before full training

import torch
from axolotl.utils.data import prepare_dataset
from torch.utils.data import DataLoader

# Load preprocessed dataset
dataset = ...  # axolotl-preprocessed tokenized dataset
dataloader = DataLoader(dataset, batch_size=1)
batch = next(iter(dataloader))

input_ids = batch['input_ids'][0]          # shape: [4096]
attention_mask = batch['attention_mask'][0] # shape: [4096] or [4096, 4096]
labels = batch['labels'][0]                # shape: [4096]; -100 for padding/separator

# Find example boundaries in the packed sequence
# Labels == -100 at separator positions; examples start where labels != -100
# after a run of -100s
boundary_positions = []
in_example = False
for i, label in enumerate(labels.tolist()):
    if label != -100 and not in_example:
        boundary_positions.append(i)  # Start of a new example
        in_example = True
    elif label == -100 and in_example:
        in_example = False

print(f"Example boundaries in packed sequence: {boundary_positions}")
# e.g., [0, 823, 1651, 2489, 3317] — 5 examples packed into 4096 tokens

# Check if attention is blocked across boundaries
# For a 2D attention mask [seq_len, seq_len]:
if attention_mask.dim() == 2:
    example_2_start = boundary_positions[1]  # Position where 2nd example starts
    # Does position example_2_start attend back to position 0 (from example 1)?
    cross_attention = attention_mask[example_2_start, 0].item()
    print(f"Cross-example attention at boundary: {cross_attention}")
    # 1.0 → cross-example attention ALLOWED (bug)
    # 0.0 → cross-example attention BLOCKED (correct)
else:
    # 1D attention mask — all positions attend to all valid preceding positions
    # No per-example blocking is possible with a 1D mask
    print("1D attention mask — no cross-example attention blocking possible")
    print("All packed examples can attend to all preceding tokens in the window")

# Decode examples to visualize the packing
for i, start in enumerate(boundary_positions[:3]):
    end = boundary_positions[i+1] if i+1 < len(boundary_positions) else len(input_ids)
    tokens = input_ids[start:end]
    decoded = tokenizer.decode(tokens, skip_special_tokens=False)
    print(f"\nExample {i+1} (positions {start}–{end}):")
    print(decoded[:200])

The fix: sample_packing: false with group_by_length: true

The safest fix is to disable sample packing and use group_by_length: true instead. Length grouping bins examples of similar lengths into the same batch, minimizing padding waste without creating cross-example attention. Throughput is somewhat lower than sample packing (more padding overall), but every example is processed with correct isolated causal attention.

Alternatively, on axolotl versions that implement correct sample packing with block-diagonal attention (using flash_attention: true and recent axolotl), the per-example attention mask is a block-diagonal causal matrix where each example block can only attend within itself. This retains the throughput benefit while fixing the attention semantics. Always verify with the batch inspection script above before committing to either approach.

# config.yaml — FIXED (Northfield Instruct Fine-Tuning Ltd.)
# axolotl 0.4.x, Python 3.11, PyTorch 2.3.x

base_model: meta-llama/Llama-3-8B
model_type: LlamaForCausalLM
tokenizer_type: LlamaTokenizer

datasets:
  - path: northfield_instructions.jsonl
    type: alpaca

sequence_len: 4096
sample_packing: false       # ← FIXED: no cross-example packing
pad_to_sequence_len: true
group_by_length: true       # ← Bins similar-length examples together; reduces padding
                             #   without cross-example attention; ~15% throughput reduction
                             #   vs sample_packing but attention semantics are correct

micro_batch_size: 4
gradient_accumulation_steps: 2
num_epochs: 3
learning_rate: 2.0e-5

adapter: lora
lora_r: 16
lora_alpha: 16
lora_dropout: 0.05
lora_target_modules:
  - q_proj
  - v_proj

# --- ALTERNATIVE (if you need sample_packing throughput):
# Use flash_attention + newer axolotl with block-diagonal sample packing:
#
# flash_attention: true
# sample_packing: true
#
# Then verify with the batch inspection script that:
#   attention_mask[example_2_start, 0] == 0.0 (cross-example attention blocked)
# If it is 0.0, you have correct block-diagonal attention — safe to use.
# Post-training inference validation
# Run 50 prompts; check that outputs do not reference undefined entities

import re

def check_context_bleed(model, tokenizer, prompts, context_entities):
    """
    prompts: list of single-turn instruction strings (no preceding context)
    context_entities: list of entity sets that should NOT appear in responses
                      (topics/names from other training examples that might bleed)
    """
    bleed_count = 0
    for prompt, forbidden in zip(prompts, context_entities):
        inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
        with torch.no_grad():
            output_ids = model.generate(**inputs, max_new_tokens=200)
        response = tokenizer.decode(output_ids[0][inputs['input_ids'].shape[1]:],
                                    skip_special_tokens=True)
        for entity in forbidden:
            if entity.lower() in response.lower():
                print(f"CONTEXT BLEED: '{entity}' appeared in response to unrelated prompt")
                print(f"  Prompt:   {prompt[:100]}")
                print(f"  Response: {response[:200]}")
                bleed_count += 1
    print(f"\nContext bleed rate: {bleed_count}/{len(prompts)} prompts")
    return bleed_count == 0

# Before fix: bleed_count typically 8-15 out of 50 (16-30%)
# After fix:  bleed_count == 0 (0%)

Validation

# Validation log — November 2026
# axolotl 0.4.x, transformers 4.39.x, Llama-3-8B, 4-GPU A100

# ---- Before fix ----
# sample_packing: true; group_by_length: false; flash_attention: false
# Attention mask: 1D causal; full cross-example attention within 4096-token window
# Training loss: 2.4 → 0.71 (converged normally)
# Inference validation (50 prompts): context bleed in 11/50 prompts (22%)
# Example bleed: "Q3 marketing budget constraints" appeared in database-indexing response

# ---- After fix: sample_packing: false; group_by_length: true ----
# Attention mask: 1D causal per-example; no cross-example attention possible
# Training throughput: ~15% lower (more padding vs packed sequences)
# Training loss: 2.4 → 0.74 (converged slightly higher — no spurious cross-deps)
# Inference validation (50 prompts): context bleed in 0/50 prompts (0%)     ✓
# Responses reference only entities present in the prompt                    ✓
# Single-turn instruction following coherent and topically consistent        ✓

# ---- Alternative fix (verified on axolotl with flash_attention block-diag) ----
# sample_packing: true; flash_attention: true (newer axolotl build)
# Attention mask: 2D block-diagonal; cross-example attention blocked
# attention_mask[example_2_start, 0]: 0.0 (blocked)                         ✓
# Training throughput: within 3% of original sample_packing run
# Inference validation (50 prompts): context bleed in 0/50 prompts (0%)     ✓

# ---- Hour breakdown ----
# Triage:     0.5h — responses reference entities not in prompt;
#             decode training batches; identify cross-example attention in packed seqs
# Root cause: 0.25h — sample_packing: true; axolotl 0.4.x 1D attention mask;
#             no per-example blocking in packed sequences
# Fix:        0.5h — set sample_packing: false; group_by_length: true;
#             re-preprocess dataset; retrain 3 epochs
# Validation: 0.25h — inference bleed rate 0/50; loss converges normally
# Total:      1.5h

# Retainer engagement cost: 1.5h (estimated $158–240 at mid-level rates)

Related retainer posts


Track retainer hours without a client portal

HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.

See pricing →