DEV Community

shashank ms
shashank ms

Posted on

Optimizing LLM Model Training Time

Training large models wastes hours when batch sizes, precision, or parallelism are misconfigured. In this tutorial, I will build a Python agent that ingests a training configuration and returns concrete changes to cut wall-clock time. We will run the agent through Oxlo.ai, where request-based pricing keeps costs flat even when we feed the model long hardware logs and detailed JSON configs.

What you'll need

The only dependencies are Python 3.10+ and the OpenAI SDK. You will also need an Oxlo.ai API key.

pip install openai

Grab your key from https://portal.oxlo.ai.

Step 1: Craft the system prompt

I want structured output so I can parse it programmatically. The system prompt forces the model to return JSON with four fields: bottleneck, changes, estimated_speedup, and reasoning.

SYSTEM_PROMPT = """You are a training optimization engineer. Analyze the user's distributed training configuration and identify the single biggest bottleneck. Respond ONLY in JSON with this structure:

{
  "bottleneck": "string describing the primary bottleneck",
  "changes": [
    {"parameter": "string", "current": "value", "recommended": "value", "rationale": "string"}
  ],
  "estimated_speedup": "string like 1.3x or 2.1x",
  "reasoning": "string with your chain of thought"
}

Be specific. Reference hardware limits, memory bandwidth, and numerical precision. Do not suggest changes that exceed the stated GPU memory."""

Step 2: Bootstrap the Oxlo.ai client

The OpenAI SDK works as a drop-in replacement. I point the base URL at Oxlo.ai and load the key from the environment.

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.oxlo.ai/v1",
    api_key=os.getenv("OXLO_API_KEY")
)

Step 3: Model the training config

I will use a typed dictionary so the rest of the script stays readable. This schema covers the variables that matter most for throughput.

from typing import TypedDict

class TrainingConfig(TypedDict):
    model_name: str
    parameters_b: float
    dataset_tokens: int
    gpu_type: str
    num_gpus: int
    batch_size_per_device: int
    gradient_accumulation_steps: int
    precision: str  # fp32, fp16, bf16
    sequence_length: int
    optimizer_state_bytes_per_param: float  # e.g., 12 for AdamW

def format_config(config: TrainingConfig) -> str:
    lines = [f"{k}: {v}" for k, v in config.items()]
    return "\n".join(lines)

Step 4: Build the analysis function

This is where we call Oxlo.ai. I use the Qwen 3 32B model because optimization requires reasoning about memory bandwidth and compute tradeoffs. Because Oxlo.ai charges per request rather than per token, I can pass the full config as a large prompt without watching metered tokens tick up.

import json

def optimize_training(config: TrainingConfig) -> dict:
    user_message = (
        "Analyze the following training configuration and suggest optimizations.\n\n"
        + format_config(config)
    )

    response = client.chat.completions.create(
        model="qwen-3-32b",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_message},
        ],
    )

    content = response.choices[0].message.content
    cleaned = content.strip()
    if cleaned.startswith("

```json"):
        cleaned = cleaned[7:]
    elif cleaned.startswith("```

"):
        cleaned = cleaned[3:]
    if cleaned.endswith("

```

"):
        cleaned = cleaned[:-3]
    return json.loads(cleaned.strip())

Step 5: Add a validation layer

LLMs can hallucinate keys. I will wrap the parser in a small validator that prints a human-readable report and falls back gracefully if JSON is malformed.

def print_report(result: dict):
    print(f"Bottleneck: {result.get('bottleneck', 'Unknown')}")
    print(f"Estimated speedup: {result.get('estimated_speedup', 'N/A')}")
    print("\nRecommended changes:")
    for change in result.get("changes", []):
        print(
            f"  - {change['parameter']}: {change['current']} -> {change['recommended']}"
            f"  ({change['rationale']})"
        )
    print(f"\nReasoning: {result.get('reasoning', '')}")

def safe_optimize(config: TrainingConfig):
    try:
        result = optimize_training(config)
        print_report(result)
    except Exception as e:
        print(f"Failed to parse optimization result: {e}")

Step 6: Run it

Here is a realistic misconfiguration: an 8B parameter model on four A100s with a tiny per-device batch size and fp32 precision. This is exactly the kind of long-context engineering prompt that benefits from Oxlo.ai's flat per-request pricing.

if __name__ == "__main__":
    config: TrainingConfig = {
        "model_name": "meta-llama/Meta-Llama-3.1-8B",
        "parameters_b": 8.0,
        "dataset_tokens": 1_000_000_000,
        "gpu_type": "NVIDIA A100-SXM4-80GB",
        "num_gpus": 4,
        "batch_size_per_device": 1,
        "gradient_accumulation_steps": 64,
        "precision": "fp32",
        "sequence_length": 4096,
        "optimizer_state_bytes_per_param": 12.0,
    }

    safe_optimize(config)

Example output:

Bottleneck: Memory-bound training caused by fp32 precision and unnecessarily small micro-batch
Estimated speedup: 2.8x

Recommended changes:
  - precision: fp32 -> bf16  (Halves activation and weight memory, doubles effective tensor-core throughput on A100)
  - batch_size_per_device: 1 -> 4  (Increases occupancy without exceeding 80GB when combined with bf16 and gradient checkpointing)
  - gradient_accumulation_steps: 64 -> 16  (Keeps global batch constant while reducing pipeline bubble overhead)
  - activation_checkpointing: off -> on  (Trades ~20% compute for 2x memory savings, enabling larger micro-batches)

Reasoning: At fp32 the model weights alone consume ~32GB, leaving insufficient headroom for activations with batch_size > 1. Switching to bf16 reduces weights to ~16GB and unlocks bfloat16 tensor-core paths on Ampere. A micro-batch of 4 fits comfortably with activation checkpointing, moving the bottleneck from memory bandwidth to compute and improving SM utilization from 34% to 81%.

Next steps

Wire this agent into your experiment tracker so it can ingest live throughput metrics from Weights & Biases and suggest dynamic adjustments during a run. If you need faster latency for interactive use, swap the model ID to llama-3.3-70b or deepseek-v3.2 on Oxlo.ai, both available on the same flat per-request plan.

Top comments (0)