Skip to content

Commit 2446c54

Browse files
madiatorclaude
andcommitted
Add FireworksTrainer for managed fine-tuning via Fireworks AI
Adds a Fireworks AI integration to the finetune module behind the same BaseTrainer interface as TinkerTrainer: - FireworksTrainerConfig: validated SFT hyperparameters (LoRA rank, epochs, learning rate) plus deployment and polling settings - FireworksDataFormatter: chat-format JSONL serialization for dataset upload - FireworksTrainer: uploads data, runs the managed SFT job, polls to completion, and serves the result via an on-demand deployment with guaranteed teardown (close() / context manager) - Mock mode when the SDK or API key is absent so examples and tests run offline - Tests for config validation and the full trainer flow against a faked SDK, plus basic and custom-trainer examples Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 8f26600 commit 2446c54

11 files changed

Lines changed: 1656 additions & 7 deletions

File tree

‎README.md‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,56 @@ result = trainer.train([{"question": "What is 2+2?", "answer": "4"}, ...])
368368

369369
See the full [poem fine-tuning example](examples/poem_finetuning_example.py) for an end-to-end pipeline that curates data with `curator.LLM` and then fine-tunes with `TinkerTrainer`.
370370

371+
## 🎆 Fine-Tuning with Fireworks AI
372+
373+
Curator also integrates with [Fireworks AI](https://docs.fireworks.ai/fine-tuning/fine-tuning-models) managed fine-tuning. Fireworks runs supervised fine-tuning (SFT) as a server-side job: your chat data is uploaded, an SFT job is submitted against a base model, and the resulting LoRA model is served for inference — all behind the same `BaseTrainer` interface as `TinkerTrainer`.
374+
375+
```bash
376+
pip install bespokelabs-curator fireworks-ai # fireworks-ai is an optional add-on
377+
export FIREWORKS_API_KEY="fw_..."
378+
```
379+
380+
```python
381+
from bespokelabs.curator import FireworksTrainer, FireworksTrainerConfig
382+
383+
# Configure the managed fine-tuning job
384+
config = FireworksTrainerConfig(
385+
base_model="qwen3-4b", # smallest fine-tunable Fireworks model
386+
epochs=2,
387+
lora_rank=8, # power of two up to 32; None for full fine-tuning
388+
learning_rate=1e-4, # or omit to let Fireworks auto-select
389+
)
390+
391+
# Training data is a list of chat-format dicts (or a HuggingFace Dataset).
392+
# Fireworks requires at least 3 examples.
393+
training_data = [
394+
{"messages": [
395+
{"role": "user", "content": "What is Python?"},
396+
{"role": "assistant", "content": "Python is a programming language."},
397+
]},
398+
# ...
399+
]
400+
401+
# Upload data + run the supervised fine-tuning job (polls until complete)
402+
trainer = FireworksTrainer(config)
403+
result = trainer.train(training_data)
404+
print(f"Fine-tuned model: {result.weights_name}")
405+
406+
# Sample from the fine-tuned model. Fine-tuned LoRA models can't be served
407+
# serverlessly, so this provisions an on-demand deployment (takes a few minutes).
408+
response = trainer.sample("Explain recursion in Python")
409+
print(response)
410+
411+
trainer.close() # tear down the deployment when done
412+
```
413+
414+
> **Note:** Running real Fireworks training requires an account with training quota
415+
> (Tier 2 / credits). Without the SDK or an API key, `FireworksTrainer` runs in mock
416+
> mode so examples and tests work offline. Subclass `FireworksTrainer` and override
417+
> `format_example()` to handle custom data layouts, exactly as with `TinkerTrainer`.
418+
419+
See the [Fireworks examples](examples/fireworks/) for basic and custom-trainer pipelines.
420+
371421
## Bespoke Curator Viewer
372422
The hosted curator viewer is a rich interface to visualize data -- and makes visually inspecting the data much easier.
373423

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""Basic FireworksTrainer example: managed supervised fine-tuning on Fireworks AI.
2+
3+
This example demonstrates:
4+
1. Creating training data in chat format
5+
2. Configuring FireworksTrainer (base model + LoRA settings)
6+
3. Running a managed supervised fine-tuning (SFT) job
7+
4. Deploying the fine-tuned model and sampling from it
8+
9+
Fireworks runs fine-tuning as a managed, server-side job. Compared to the Tinker
10+
example, there is no client-side training loop: the dataset is uploaded, an SFT job
11+
is submitted, and Fireworks trains the model for you.
12+
13+
Notes:
14+
- Fireworks requires at least 3 training examples.
15+
- Fine-tuned LoRA models cannot be served serverlessly, so `sample()` provisions an
16+
on-demand deployment (which costs GPU time and takes a few minutes to spin up).
17+
Call `trainer.close()` (or use the trainer as a context manager) to tear it down
18+
when finished, so you don't leak a billable deployment.
19+
- Running real training requires a Fireworks account with training quota
20+
(Tier 2 / credits). Without the SDK or an API key, this runs in mock mode.
21+
22+
Usage:
23+
# Mock mode (no API key / SDK required)
24+
poetry run python examples/fireworks/basic_example.py
25+
26+
# Real fine-tuning (fireworks-ai is an optional add-on installed separately)
27+
pip install fireworks-ai
28+
export FIREWORKS_API_KEY="fw_..."
29+
poetry run python examples/fireworks/basic_example.py
30+
"""
31+
32+
from bespokelabs.curator import FireworksTrainer, FireworksTrainerConfig
33+
34+
35+
def get_training_data() -> list[dict]:
36+
"""Create sample chat training data with a distinctive, learnable style.
37+
38+
Each assistant answer ends with a fixed catchphrase. After fine-tuning, the
39+
model should reproduce that catchphrase on unseen questions.
40+
"""
41+
catchphrase = " Arrr, that's the bespoke truth, matey!"
42+
facts = [
43+
("What is the capital of France?", "The capital of France is Paris."),
44+
("What is 2 + 2?", "2 + 2 equals 4."),
45+
("Who wrote Romeo and Juliet?", "Romeo and Juliet was written by William Shakespeare."),
46+
("What color is a clear daytime sky?", "A clear daytime sky is blue."),
47+
("Name a popular programming language.", "Python is a popular programming language."),
48+
("What is the largest planet in our solar system?", "Jupiter is the largest planet."),
49+
("How many continents are there?", "There are seven continents on Earth."),
50+
("What gas do plants absorb from the air?", "Plants absorb carbon dioxide."),
51+
("What is the boiling point of water in Celsius?", "Water boils at 100 degrees Celsius at sea level."),
52+
("Who painted the Mona Lisa?", "Leonardo da Vinci painted the Mona Lisa."),
53+
]
54+
return [
55+
{
56+
"messages": [
57+
{"role": "user", "content": question},
58+
{"role": "assistant", "content": answer + catchphrase},
59+
]
60+
}
61+
for question, answer in facts
62+
]
63+
64+
65+
def main():
66+
"""Run the basic FireworksTrainer example."""
67+
print("=" * 60)
68+
print("Basic FireworksTrainer Example")
69+
print("=" * 60)
70+
71+
print("\n[Step 1] Preparing training data...")
72+
training_data = get_training_data()
73+
print(f" Training examples: {len(training_data)}")
74+
75+
print("\n[Step 2] Configuring FireworksTrainer...")
76+
config = FireworksTrainerConfig(
77+
base_model="qwen3-4b", # smallest fine-tunable Fireworks model
78+
# Fireworks packs examples into batches by token count, so a SMALL dataset can
79+
# be a single batch (one optimizer step) per epoch. With few examples, use many
80+
# epochs so the LoRA gets enough gradient steps to actually learn.
81+
epochs=30,
82+
lora_rank=16,
83+
learning_rate=3e-4, # or omit to let Fireworks auto-select
84+
display_name="curator-basic-demo",
85+
output_model="curator-basic-demo",
86+
)
87+
print(f" Base Model: {config.qualified_base_model}")
88+
print(f" Epochs: {config.epochs}")
89+
print(f" LoRA Rank: {config.lora_rank}")
90+
91+
print("\n[Step 3] Running supervised fine-tuning...")
92+
trainer = FireworksTrainer(config)
93+
result = trainer.train(training_data)
94+
95+
print("\n" + "=" * 60)
96+
print("Fine-tuning Complete!")
97+
print("=" * 60)
98+
print(f" Output Model: {result.weights_name}")
99+
print(f" Samples Processed: {result.samples_processed}")
100+
print(f" Total Time: {result.total_time:.2f}s")
101+
if result.loss_history:
102+
print(f" Final Loss: {result.final_loss:.4f}")
103+
print(f" Job: {result.metadata.get('job_url')}")
104+
105+
print("\n[Step 4] Sampling from the fine-tuned model...")
106+
print(" (this deploys the model on-demand; first call may take a few minutes)")
107+
test_prompts = [
108+
"What is the capital of Japan?",
109+
"Who discovered gravity?",
110+
]
111+
for prompt in test_prompts:
112+
print(f"\n User: {prompt}")
113+
response = trainer.sample(prompt)
114+
print(f" Assistant: {response}")
115+
116+
print("\n[Step 5] Cleaning up the deployment...")
117+
trainer.close()
118+
119+
print("\n" + "=" * 60)
120+
print("Example Complete!")
121+
print("=" * 60)
122+
return trainer
123+
124+
125+
if __name__ == "__main__":
126+
trainer = main()
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""Custom FireworksTrainer example: subclass for custom data formatting.
2+
3+
This example demonstrates:
4+
1. Subclassing FireworksTrainer to customize data formatting
5+
2. Using format_example() to convert raw data to chat training examples
6+
3. Working with non-standard data formats (instruction/response, Q&A with context)
7+
8+
This pattern is useful when your data doesn't already use the standard "messages"
9+
format and needs custom conversion before fine-tuning.
10+
11+
Usage:
12+
poetry run python examples/fireworks/custom_trainer_example.py
13+
"""
14+
15+
from bespokelabs.curator import FireworksTrainer, FireworksTrainerConfig
16+
from bespokelabs.curator.finetune.types import TrainingExample
17+
18+
19+
class InstructionTrainer(FireworksTrainer):
20+
"""Custom trainer for instruction-response datasets.
21+
22+
Converts data in {"instruction": ..., "response": ...} format to the chat
23+
format required by Fireworks fine-tuning.
24+
"""
25+
26+
def format_example(self, row: dict) -> TrainingExample:
27+
"""Convert an instruction-response pair to chat format."""
28+
return TrainingExample.from_dict_messages(
29+
[
30+
{"role": "system", "content": "You are a helpful assistant that follows instructions precisely."},
31+
{"role": "user", "content": row["instruction"]},
32+
{"role": "assistant", "content": row["response"]},
33+
]
34+
)
35+
36+
37+
class QATrainer(FireworksTrainer):
38+
"""Custom trainer for question-answer datasets with optional context."""
39+
40+
def format_example(self, row: dict) -> TrainingExample:
41+
"""Convert a Q&A pair (with optional context) to chat format."""
42+
if row.get("context"):
43+
user_content = f"Context: {row['context']}\n\nQuestion: {row['question']}"
44+
else:
45+
user_content = row["question"]
46+
return TrainingExample.from_dict_messages(
47+
[
48+
{"role": "system", "content": "You are a knowledgeable assistant. Answer accurately and concisely."},
49+
{"role": "user", "content": user_content},
50+
{"role": "assistant", "content": row["answer"]},
51+
]
52+
)
53+
54+
55+
def get_instruction_data() -> list[dict]:
56+
"""Sample instruction-response dataset (Fireworks requires >= 3 examples)."""
57+
return [
58+
{
59+
"instruction": "Summarize in one sentence: Machine learning lets systems learn from experience.",
60+
"response": "Machine learning lets systems learn from experience automatically.",
61+
},
62+
{"instruction": "Convert to past tense: The cat jumps over the fence.", "response": "The cat jumped over the fence."},
63+
{"instruction": "List two benefits of exercise.", "response": "1. Better cardiovascular health\n2. Improved mood"},
64+
{"instruction": "Explain what an API is in one sentence.", "response": "An API is a contract that lets software components talk to each other."},
65+
]
66+
67+
68+
def get_qa_data() -> list[dict]:
69+
"""Sample Q&A dataset with optional context."""
70+
return [
71+
{"context": "Python was created by Guido van Rossum in 1991.", "question": "Who created Python?", "answer": "Guido van Rossum created Python."},
72+
{"context": "The Great Wall of China is ~21,196 km long.", "question": "How long is the Great Wall?", "answer": "About 21,196 kilometers."},
73+
{"question": "What is the capital of France?", "answer": "Paris is the capital of France."},
74+
{
75+
"context": "Photosynthesis converts sunlight into energy.",
76+
"question": "What is photosynthesis?",
77+
"answer": "The process by which plants convert sunlight into energy.",
78+
},
79+
]
80+
81+
82+
def train_instruction_model():
83+
"""Train using the InstructionTrainer."""
84+
print("\n" + "-" * 50)
85+
print("Training InstructionTrainer")
86+
print("-" * 50)
87+
config = FireworksTrainerConfig(base_model="qwen3-4b", epochs=1, lora_rank=8, display_name="curator-instruction-demo")
88+
data = get_instruction_data()
89+
# `with` guarantees the on-demand inference deployment is torn down afterward.
90+
with InstructionTrainer(config) as trainer:
91+
print(f"Training on {len(data)} instruction-response pairs...")
92+
result = trainer.train(data)
93+
print(f"Output model: {result.weights_name}")
94+
response = trainer.sample("Translate 'Hello, how are you?' to French.")
95+
print(f" Response: {response}")
96+
return trainer
97+
98+
99+
def train_qa_model():
100+
"""Train using the QATrainer."""
101+
print("\n" + "-" * 50)
102+
print("Training QATrainer")
103+
print("-" * 50)
104+
config = FireworksTrainerConfig(base_model="qwen3-4b", epochs=1, lora_rank=8, display_name="curator-qa-demo")
105+
data = get_qa_data()
106+
with QATrainer(config) as trainer:
107+
print(f"Training on {len(data)} Q&A pairs...")
108+
result = trainer.train(data)
109+
print(f"Output model: {result.weights_name}")
110+
response = trainer.sample("Context: The Eiffel Tower opened in 1889.\n\nQuestion: When did the Eiffel Tower open?")
111+
print(f" Response: {response}")
112+
return trainer
113+
114+
115+
def main():
116+
"""Run both custom trainer examples."""
117+
print("=" * 60)
118+
print("Custom FireworksTrainer Examples")
119+
print("=" * 60)
120+
instruction_trainer = train_instruction_model()
121+
qa_trainer = train_qa_model()
122+
print("\n" + "=" * 60)
123+
print("Custom Trainer Examples Complete!")
124+
print("=" * 60)
125+
return instruction_trainer, qa_trainer
126+
127+
128+
if __name__ == "__main__":
129+
main()

‎src/bespokelabs/curator/__init__.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
__version__ = "unknown"
99

1010
from .code_executor.code_executor import CodeExecutor
11-
from .finetune.config import TinkerTrainerConfig
12-
from .finetune.trainer import TinkerTrainer
11+
from .finetune.config import FireworksTrainerConfig, TinkerTrainerConfig
12+
from .finetune.trainer import FireworksTrainer, TinkerTrainer
1313
from .llm.llm import LLM
1414
from .types import prompt as types
1515
from .utils import load_dataset, push_to_viewer
@@ -19,6 +19,8 @@
1919
"CodeExecutor",
2020
"TinkerTrainer",
2121
"TinkerTrainerConfig",
22+
"FireworksTrainer",
23+
"FireworksTrainerConfig",
2224
"types",
2325
"push_to_viewer",
2426
"load_dataset",

‎src/bespokelabs/curator/finetune/__init__.py‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Fine-tuning module for curator.
22
3-
This module provides LoRA-based fine-tuning via the Tinker API.
3+
This module provides LoRA-based fine-tuning via the Tinker API (``TinkerTrainer``)
4+
and managed fine-tuning via the Fireworks AI API (``FireworksTrainer``).
45
56
Example usage:
67
```python
@@ -47,10 +48,11 @@ def format_example(self, row: dict) -> TrainingExample:
4748
```
4849
"""
4950

50-
from bespokelabs.curator.finetune.config import AdamParams, LoRAConfig, TinkerTrainerConfig
51+
from bespokelabs.curator.finetune.config import AdamParams, FireworksTrainerConfig, LoRAConfig, TinkerTrainerConfig
5152
from bespokelabs.curator.finetune.data_formatter import DataFormatter
53+
from bespokelabs.curator.finetune.fireworks_data_formatter import FireworksDataFormatter
5254
from bespokelabs.curator.finetune.status_tracker import FinetuneStatusTracker
53-
from bespokelabs.curator.finetune.trainer import BaseTrainer, TinkerTrainer
55+
from bespokelabs.curator.finetune.trainer import BaseTrainer, FireworksTrainer, TinkerTrainer
5456
from bespokelabs.curator.finetune.types import (
5557
ChatMessage,
5658
CheckpointInfo,
@@ -63,10 +65,12 @@ def format_example(self, row: dict) -> TrainingExample:
6365
__all__ = [
6466
# Config
6567
"TinkerTrainerConfig",
68+
"FireworksTrainerConfig",
6669
"AdamParams",
6770
"LoRAConfig",
6871
# Trainers
6972
"TinkerTrainer",
73+
"FireworksTrainer",
7074
"BaseTrainer",
7175
# Types
7276
"TrainingExample",
@@ -77,5 +81,6 @@ def format_example(self, row: dict) -> TrainingExample:
7781
"CheckpointInfo",
7882
# Utilities
7983
"DataFormatter",
84+
"FireworksDataFormatter",
8085
"FinetuneStatusTracker",
8186
]

0 commit comments

Comments
 (0)