HOW TO USE FEAST FOR SLM/LLM POST-TRAINING WITH RAY

Keep conversation features in Feast, retrieve them for training, then stream into your trainer with Ray.

By Chaitanya Patel

How to Use Feast for SLM/LLM Post-Training with Ray

Your support bot answers a lot of tickets. It’s fine—but it sounds generic. The team wants a smaller model that talks more like your agents: your refund wording, your product names, your tone.

So someone says: fine-tune on our real chats.

That part sounds easy. The messy part is the data—exports, notebook cleaning, and prompt formatting scattered across training scripts.

This post walks through the ray-llm-posttrain example:

  1. Put conversation features in Feast
  2. Retrieve them with get_historical_features (entity-less date range)
  3. Get rows into your trainer — stream with Ray or materialize with .to_df()

You bring your own trainer. GPT-2 in the script is optional smoke only.

What’s in the example

NameTypeWhat it holds
web_documentsFeatureViewhuman, bot, human_repeat_ratio, bot_repeat_ratio
train_exampleOnDemandFeatureViewcleaned_human, cleaned_bot, char_count, is_trainable, sft_text
llm_posttrainFeatureServiceBundles web_documents + train_example

Full definitions live in feature_definitions.py. Ray is the offline store and one way to stream rows out—not a separate feature catalog.

This example stays on supported Feast APIs only (no core patches). Conversation rows already include document_id and event_timestamp before Feast reads them.

Step 1: Point Feast at conversation data

Ray offline store (local)

From the example feature_store.yaml. Cap Ray resources on a laptop—see Ray offline store: resource management:

project: ray_llm_posttrain
registry: data/registry.db
provider: local

offline_store:
  type: ray
  storage_path: data/ray_storage
  enable_ray_logging: false
  ray_conf:
    num_cpus: 2
    object_store_memory: 104857600
    _memory: 524288000

batch_engine:
  type: ray.engine
  max_workers: 2

online_store:
  type: sqlite
  path: data/online_store.db

entity_key_serialization_version: 3
auth:
  type: no_auth

You can also start from the built-in template:

feast init -t ray my_ray_project

See the Ray template / offline store docs and the related blog Scaling ML with Feast and Ray.

Demo seed: prepare parquet, then RaySource

RaySource tells Feast how to load data through Ray. Hugging Face is only used in a prepare script—not as a live Feast source that invents timestamps at retrieval time.

nampdn-ai/tiny-webtext has no document_id / event_timestamp. Entity-less retrieval needs those columns on the source. We add them outside Feast, write parquet, then point Feast at that file (supported path):

PYTHONPATH=../../sdk/python python scripts/prepare_data.py
# → feature_repo/data/tiny_webtext.parquet
from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import RaySource

tiny_web = RaySource(
    name="tiny_webtext",
    reader_type="parquet",
    path="data/tiny_webtext.parquet",
    timestamp_field="event_timestamp",
)

In production you’d skip the HF prepare step and register your real conversation store (warehouse / lake / parquet) that already has join keys and timestamps.

More reader types are in the Ray data source reference.

Feature view

web_documents = FeatureView(
    name="web_documents",
    entities=[document],
    ttl=timedelta(days=365),
    schema=[
        Field(name="human", dtype=String),
        Field(name="bot", dtype=String),
        Field(name="human_repeat_ratio", dtype=Float64),
        Field(name="bot_repeat_ratio", dtype=Float64),
    ],
    source=tiny_web,
    online=False,
)

Optional: OnDemandFeatureView for derived training features

If you want Feast to own sft_text / quality gates (same idea as in the ODFV docs):

@on_demand_feature_view(
    sources=[web_documents],
    schema=[
        Field(name="cleaned_human", dtype=String),
        Field(name="cleaned_bot", dtype=String),
        Field(name="char_count", dtype=Int64),
        Field(name="is_trainable", dtype=Bool),
        Field(name="sft_text", dtype=String),
    ],
    mode="pandas",
)
def train_example(inputs):
    cleaned_human = inputs["human"].fillna("").astype(str).str.strip()
    cleaned_bot = inputs["bot"].fillna("").astype(str).str.strip()
    # ... length + repeat-ratio gate ...
    sft_text = (
        "<|im_start|>user\n" + cleaned_human + "<|im_end|>\n"
        "<|im_start|>assistant\n" + cleaned_bot + "<|im_end|>"
    )
    return pd.DataFrame({...})
llm_posttrain = FeatureService(
    name="llm_posttrain",
    features=[web_documents, train_example],
)

Apply:

cd examples/ray-llm-posttrain/feature_repo
feast apply

Step 2: Retrieve for training (entity-less)

No entity_df—just a date window. That pattern is covered in Historical Features Without Entity IDs and the FAQ:

from datetime import datetime, timezone
from feast import FeatureStore

store = FeatureStore(repo_path="feature_repo")

job = store.get_historical_features(
    features=[
        "web_documents:human",
        "web_documents:bot",
        "web_documents:human_repeat_ratio",
        "web_documents:bot_repeat_ratio",
    ],
    start_date=datetime(2024, 6, 1, tzinfo=timezone.utc),
    end_date=datetime(2024, 7, 1, tzinfo=timezone.utc),
)

Then choose how you turn that job into training rows.

Step 3: Two ways into the trainer

PathODFV runs?When to use
job.to_ray_dataset() then preprocessNoStream FeatureView columns; shape sft_text yourself
job.to_df() / to_arrow()YesWant train_example outputs from Feast

Pick Option A when you want full control over text formatting or need custom preprocessing (e.g., multi-turn chat templates, tokenization-aware truncation). Pick Option B when you want Feast to enforce quality gates consistently across training and serving.

Option A — Stream with Ray, preprocess yourself

to_ray_dataset() returns a Ray Dataset of retrieved FeatureView columns. It does not apply OnDemandFeatureViews. Build training text with Ray map_batches (as in train_sft.py):

ds = job.to_ray_dataset()

def preprocess_sft(batch):
    import pandas as pd

    if not isinstance(batch, pd.DataFrame):
        batch = pd.DataFrame(batch)
    human = batch["human"].fillna("").astype(str).str.strip()
    bot = batch["bot"].fillna("").astype(str).str.strip()
    ok = bot.str.len() >= 64
    sft_text = (
        "<|im_start|>user\n" + human + "<|im_end|>\n"
        "<|im_start|>assistant\n" + bot + "<|im_end|>"
    )
    return pd.DataFrame({"sft_text": sft_text}).loc[ok].reset_index(drop=True)

train_ds = ds.map_batches(preprocess_sft, batch_format="pandas")
# → hand train_ds to your SLM/LLM trainer

Run the example default path:

PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run

Option B — Use the ODFV, then train

Materialize with .to_df() so train_example runs (same retrieval/serving idea as in the ODFV overview):

df = store.get_historical_features(
    features=store.get_feature_service("llm_posttrain"),
    start_date=datetime(2024, 6, 1, tzinfo=timezone.utc),
    end_date=datetime(2024, 7, 1, tzinfo=timezone.utc),
).to_df()

trainable = df[df["is_trainable"] & df["sft_text"].astype(str).str.len().gt(0)]
# trainable["sft_text"] → your trainer
# or: import ray; ray.data.from_pandas(trainable[["sft_text"]])
PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run --via-df

The same ODFV at serving time

The train_example ODFV runs identically during online serving — the quality gate and formatting logic stay in one place:

# At inference time, the same ODFV runs on the fly
features = store.get_online_features(
    features=["train_example:sft_text", "train_example:is_trainable"],
    entity_rows=[{"document_id": "doc_42"}],
).to_dict()
# features["sft_text"], features["is_trainable"] — same logic as training

Try the full example

cd examples/ray-llm-posttrain
uv pip install -e "../../sdk/python[ray]" -r requirements.txt
PYTHONPATH=../../sdk/python python scripts/prepare_data.py
cd feature_repo && feast apply && cd ..

PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run
PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run --via-df

# optional GPT-2 smoke
PYTHONPATH=../../sdk/python python scripts/train_sft.py --max-steps 20

Details: ray-llm-posttrain README.

Takeaways

  1. Keep conversation features in Feast — this example’s web_documents.
  2. Stream with Rayto_ray_dataset(), then preprocess training text yourself.
  3. Want ODFVs.to_df() / .to_arrow() to materialize, then train.
  4. Bring your own trainer — GPT-2 in the example is optional.

References

This example

Docs

Related blogs & tutorials