Skip to content

Fine-Tuning Data Flywheel #15

Description

@gotoplanb

Conduct Feature: Wander Fine-Tuning Data Flywheel

Background

Wander is a Sierra Online-style adventure game engine that uses Conduct task routing for flexible player input evaluation (wander_eval). Instead of 1980s-style strict string matching, Conduct evaluates whether a player’s input advances the story, represents valid creative alternatives, or misses the mark — scoring on a gradient rather than binary pass/fail.

Every Wander session already generates (scene_context, player_input, eval_score, model, note) tuples as a side effect of gameplay. This document describes how to capture, structure, and leverage that data as a fine-tuning pipeline targeting a smaller, faster, game-specific wander_eval model.


The Problem

wander_eval currently routes to gemma4:e4b — a capable general-purpose model doing its best to understand Wander’s narrative logic without any game-specific training. As episode count and player sessions grow, a general model becomes the wrong tool: it has no knowledge of world state, character relationships, inventory logic, or the tone and rules of a specific episode.


The Opportunity: A Self-Generating Training Dataset

Wander’s eval structure produces training data that is unusually rich compared to typical game datasets:

Field Value
scene_description What the player sees and knows
player_input The raw input being evaluated
available_exits Valid navigation options
inventory_state Player’s current items
eval_score 1–5 from Conduct eval
model Which model produced the eval
note Evaluator rationale

Most game datasets contain only (input, correct_answer) pairs. This dataset contains scored reasoning across multiple models on the same input — that’s a ranking dataset suitable for DPO (Direct Preference Optimization), not just supervised classification.

Score Interpretation for Training

Score Label Training Role
4–5 Valid input, advances story or creative alternative Positive example
2–3 Plausible but wrong, reasonable attempt Hard negative
1 Nonsensical or completely off-context Easy negative

Shadow eval pairs where one model scores 4+ and another scores 1–2 on the same input become DPO (chosen, rejected) pairs automatically — no additional labeling required.


Proposed Conduct Changes

1. Structured Eval Logging for wander_eval

Eval records are stored in Loki via the Watchtower LGTM stack (Grafana + Loki + Tempo + Mimir + Alloy), with no new infrastructure required. Wander eval events are structured log entries — timestamp, source labels, JSON payload — which maps exactly to Loki’s data model.

Loki Label Strategy

Labels must be low-cardinality:

job="wander_eval"
episode_id="<episode_slug>"
model="<model_name>"
task_type="wander_eval"

Everything else goes in the structured JSON log line, queryable via | json in LogQL.

Log Payload Schema

{
  "task_type": "wander_eval",
  "episode_id": "string",
  "scene_id": "string",
  "player_input": "string",
  "available_exits": ["string"],
  "inventory_state": ["string"],
  "eval_score": 1,
  "model": "string",
  "note": "string",
  "shadow_scores": [
    { "model": "string", "score": 1, "note": "string" }
  ]
}

Note: scene_description is intentionally excluded from the log line to keep chunk sizes manageable. Scenes are resolved by scene_id from the episode’s SQLite package at query time.

Ingestion

Push directly from the Conduct wander_eval worker to Loki’s push API:

import requests, json, time

def log_wander_eval(payload: dict):
    requests.post("http://localhost:3100/loki/api/v1/push", json={
        "streams": [{
            "stream": {
                "job": "wander_eval",
                "episode_id": payload["episode_id"],
                "model": payload["model"],
                "task_type": "wander_eval"
            },
            "values": [[str(time.time_ns()), json.dumps(payload)]]
        }]
    })

Alloy dual-writes to Sumo Logic alongside Loki automatically — no additional config needed.

Grafana Dashboards

  • Eval score distributions per episode and model
  • Average score, latency, and cost by model over time
  • DPO pair mining: {job="wander_eval"} | json | score >= 4 vs score <= 2 on the same scene_id

HuggingFace Export

When ready to fine-tune, query Loki via /loki/api/v1/query_range with a LogQL filter and export to JSONL. Loki is the source of truth; HuggingFace receives periodic exports rather than being the primary store.

2. HuggingFace Dataset Push

When sufficient eval data has accumulated in Loki, export to HuggingFace as a private dataset repo. A small Python script queries Loki’s HTTP API and pushes JSONL incrementally — treat each push like a git commit for data.

import requests, json

def export_loki_to_jsonl(output_path: str, min_score: int = 1):
    resp = requests.get("http://localhost:3100/loki/api/v1/query_range", params={
        "query": '{job="wander_eval"} | json',
        "limit": 5000,
        "start": 0,
        "end": int(time.time()) * 1_000_000_000
    })
    with open(output_path, "w") as f:
        for stream in resp.json()["data"]["result"]:
            for _, line in stream["values"]:
                f.write(line + "\n")

# Then push to HuggingFace
# huggingface-cli upload wander-eval-dataset ./wander_eval_export.jsonl

3. Fine-Tuning Target

Do not fine-tune a large model. The goal is a 3B–7B model that deeply understands Wander’s world and runs locally in milliseconds — outperforming a general 31B on this specific task.

Recommended approach:

  • Method: QLoRA via unsloth (MLX backend for M5 Max)
  • Base model: llama3.2:3b or gemma4:e2b (already in the Ollama stack)
  • Training data: DPO pairs derived from shadow eval score differentials
  • Checkpoints: Committed to HuggingFace model repo at regular intervals

4. Deployment Back into Conduct

Once fine-tuned:

  1. Merge LoRA adapter into base weights

  2. Export to GGUF

  3. Import into Ollama via Modelfile:

    FROM /path/to/wander-eval-ft.gguf
    
  4. Update wander_eval routing rule to use the fine-tuned model as primary

  5. Retain general model (e.g. gemma4:e4b) as a shadow for edge cases and ongoing data collection

5. Variability Input for wander_eval

Add variability and seed to the inputs bag for wander_eval to support:

  • Deterministic evaluation (variability: "low", fixed seed) for regression testing specific scenes
  • Exploratory evaluation (variability: "high") when probing edge cases or ambiguous inputs
{
  "inputs": {
    "variability": "low",
    "seed": 42
  }
}

The Flywheel

Wander gameplay
    → wander_eval jobs with shadow scoring
    → structured JSONL log with DPO pairs
    → HuggingFace dataset (incremental push)
    → QLoRA fine-tune on 3B base
    → GGUF → Ollama → new wander_eval primary
    → better evals → better gameplay → more data

Every session makes the eval model smarter. The game engine generates its own training data as a byproduct of being played.


Priority Order

  1. Structured logging — highest leverage, no model work required, starts accumulating data immediately
  2. HuggingFace dataset push — low effort, enables future fine-tuning at any time
  3. Variability/seed inputs — useful for wander_eval and generalizes to all Conduct task types
  4. Fine-tuning pipeline — once sufficient eval data exists (rough target: 1,000+ scored examples)
  5. Routing rule update — swap fine-tuned model in as primary once benchmarks justify it

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions