Every post on this blog so far has been about LLMs, and that is a fair reflection of what clients ask for. But the forecasting workload never went away: demand planning, capacity planning, cash-flow projection, and energy load are still the models that quietly run the business. What changed is that foundation models arrived for time series too. Chronos-Bolt, packaged in SageMaker JumpStart and in AutoGluon-TimeSeries, will forecast a series it has never seen without any training at all — and it is usually competitive with the statistical baseline you would have spent two weeks tuning.
This tutorial builds the whole path: a zero-shot baseline in an afternoon, an honest backtest against seasonal-naive, optional fine-tuning, and then the deployment shape that actually fits forecasting — batch, not a real-time endpoint.
Why zero-shot changes the project plan
Classic forecasting on SageMaker meant one model per series (or per group), retrained on a schedule, with a hyperparameter search you could never quite justify. Chronos-style models are pretrained on a large corpus of time series and tokenize your history directly, so inference is a forward pass over the context window.
Practical consequences for a delivery plan:
- Week one produces numbers. You can backtest hundreds of series before you decide whether the project needs custom training.
- Cold-start series work. A SKU with 30 observations gets a sensible probabilistic forecast instead of an error.
- Fine-tuning becomes an optimization, not a prerequisite. You fine-tune when the backtest says zero-shot loses, not by default.
Chronos-Bolt is the variant to start with: it is a patched encoder-decoder that is roughly an order of magnitude faster than the original Chronos T5 models and runs acceptably on CPU for small batches. Sizes run from tiny (about 9M parameters) up to base (about 205M).
Step 1: shape the data
Everything below assumes a long-format frame with three columns: an ID, a timestamp, and a target.
import pandas as pd
df = pd.read_parquet("s3://your-bucket/demand/history.parquet")
df = df.rename(columns={"sku": "item_id", "date": "timestamp", "units": "target"})
df["timestamp"] = pd.to_datetime(df["timestamp"])
df = df.sort_values(["item_id", "timestamp"])
Three data problems cause most bad forecasts, and none of them are model problems:
- Implicit zeros. If a SKU sold nothing on Tuesday, most warehouse extracts omit the row. Reindex each series onto a complete date range and fill the gap with
0for sales-like targets — not with forward-fill, which invents demand. - Ragged endpoints. If half your series end on the 30th and half on the 28th because of a late ETL, the model reads that as a collapse in demand. Truncate every series to a common end date.
- Leaked future covariates. Anything you feed as a "known covariate" must genuinely be known at forecast time. Price is usually not. A published promotion calendar is.
def regularize(g, freq="D"):
idx = pd.date_range(g["timestamp"].min(), CUTOFF, freq=freq)
return (g.set_index("timestamp")
.reindex(idx)
.assign(item_id=g["item_id"].iloc[0])
.fillna({"target": 0.0})
.rename_axis("timestamp")
.reset_index())
CUTOFF = df["timestamp"].max()
df = df.groupby("item_id", group_keys=False).apply(regularize)
Step 2: a zero-shot baseline with AutoGluon-TimeSeries
Run this in a SageMaker Studio notebook or a Processing job. pip install "autogluon.timeseries>=1.2".
from autogluon.timeseries import TimeSeriesDataFrame, TimeSeriesPredictor
PREDICTION_LENGTH = 28
ts = TimeSeriesDataFrame.from_data_frame(
df, id_column="item_id", timestamp_column="timestamp"
)
predictor = TimeSeriesPredictor(
prediction_length=PREDICTION_LENGTH,
target="target",
eval_metric="WQL", # weighted quantile loss: scores the whole distribution
freq="D",
).fit(
ts,
hyperparameters={
"Chronos": {"model_path": "bolt_base", "ag_args": {"name_suffix": "ZeroShot"}},
"SeasonalNaive": {},
},
skip_model_selection=False,
enable_ensemble=False,
time_limit=1800,
)
print(predictor.leaderboard(ts))
Two models, deliberately. SeasonalNaive is the baseline you must beat to justify any of this; on weekly retail data it is embarrassingly strong. If Chronos-Bolt does not beat it on WQL, the answer is not a bigger model — it is better features or a different aggregation level.
WQL rather than MAPE, also deliberately. MAPE is undefined at zero and punishes under-forecasting asymmetrically, which is exactly wrong for intermittent demand. Weighted quantile loss scores the predicted distribution, which is what a planner consumes when they ask for a P90 safety-stock number.
Step 3: backtest like a planner, not like a data scientist
A single train/test split will flatter you. Use rolling-origin evaluation with as many windows as the history supports:
scores = predictor.evaluate(
ts,
metrics=["WQL", "MASE", "RMSE"],
num_val_windows=5, # five rolling origins
)
print(scores)
Then break the score down by segment before you report a single number. The aggregate almost always hides the finding:
preds = predictor.predict(ts)
joined = (preds.reset_index()
.merge(actuals, on=["item_id", "timestamp"], how="inner")
.assign(abs_err=lambda d: (d["mean"] - d["target"]).abs()))
print(joined.groupby(velocity_band(joined["item_id"]))["abs_err"].mean())
Slice by velocity band (fast/slow/intermittent movers), by series length, and by seasonality strength. In most engagements zero-shot Chronos wins convincingly on short and intermittent series and ties on the long, clean, strongly seasonal ones where a tuned statistical model was always going to be fine.
Step 4: deploy Chronos-Bolt from JumpStart
If you want a hosted model rather than an in-process one, Chronos-Bolt is in the JumpStart catalog:
from sagemaker.jumpstart.model import JumpStartModel
model = JumpStartModel(model_id="autogluon-forecasting-chronos-bolt-base")
predictor = model.deploy(
initial_instance_count=1,
instance_type="ml.c5.2xlarge", # CPU is viable for Bolt at modest batch sizes
endpoint_name="chronos-bolt-base",
)
payload = {
"inputs": [
{"target": series_a}, # list of floats, the history
{"target": series_b},
],
"parameters": {
"prediction_length": 28,
"quantile_levels": [0.1, 0.5, 0.9],
},
}
print(predictor.predict(payload))
Two notes that save a support ticket. The context window is finite (1024 observations for Bolt); passing a longer history silently truncates from the left, so downsample or aggregate rather than hoping. And prediction_length beyond the model's trained horizon degrades sharply — for long horizons, forecast at a coarser frequency instead of extrapolating a daily model out six months.
Step 5: fine-tune only if the backtest demands it
If the segment breakdown shows zero-shot losing on a segment that matters commercially, fine-tune on your own history. In AutoGluon this is a hyperparameter change, not a new codebase:
predictor = TimeSeriesPredictor(
prediction_length=PREDICTION_LENGTH, eval_metric="WQL", freq="D"
).fit(
ts,
hyperparameters={
"Chronos": [
{"model_path": "bolt_small", "ag_args": {"name_suffix": "ZeroShot"}},
{
"model_path": "bolt_small",
"fine_tune": True,
"fine_tune_steps": 2000,
"fine_tune_lr": 1e-4,
"ag_args": {"name_suffix": "FineTuned"},
},
],
},
time_limit=7200,
)
Run it as a SageMaker training job on a single ml.g5.2xlarge and let managed spot absorb the cost; fine-tuning Bolt-small is minutes-to-hours, not days. Keep the zero-shot variant in the same leaderboard so the comparison is automatic and auditable — this is the artifact that stops the "should we have fine-tuned?" argument from recurring every quarter.
Covariates are the other reason to move past zero-shot. If promotions, holidays, or weather genuinely drive your target, TimeSeriesPredictor will pass known covariates and static item features to models that support them; a covariate-aware model can beat a larger covariate-blind one comfortably.
Step 6: the right deployment shape
Forecasting is almost never a real-time workload. Planners consume a table refreshed nightly or weekly. Paying for a GPU endpoint that idles 23 hours a day is the most common avoidable cost in a forecasting project.
The shape that fits:
- Batch transform or a Processing job on a schedule, writing a partitioned Parquet table of quantile forecasts to S3.
- EventBridge on a cron, triggering a SageMaker Pipeline: regularize → forecast → validate → write.
- A validation step that can fail the run. Assert no negative forecasts for count targets, no more than N x the historical max, and that every expected
item_idis present. A silently missing SKU is worse than a bad number, because nobody notices. - Athena or a lakehouse table as the serving layer. The forecast is data; let the BI tool read data.
If a genuine interactive use case exists — a planner moving a slider and wanting an instant re-forecast — use a serverless endpoint, since traffic is bursty and human-paced. See Choosing an Inference Type for the trade-offs, and A Minimal MLOps Loop for the pipeline scaffolding.
Monitoring a forecast in production
Drift monitoring for forecasting is not the same as for classification. Two things to track:
- Rolling realized error per segment, compared against the backtest WQL. A gradual rise means the regime moved; a step change usually means the upstream ETL changed.
- Input distribution, especially series count and history length. A forecast run that quietly received 12,000 series instead of 14,000 should page someone.
Both fit in the same CloudWatch-alarm pattern described in Detect Drift in Production — the metric is different, the plumbing is identical.
A realistic timeline
For a mid-size catalog, an engagement of this shape runs roughly: two to three days of data regularization (always the long pole), one day for the zero-shot baseline and backtest, two days of segment analysis and the beat-the-baseline decision, and three to five days for the scheduled pipeline, validation, and handover. Fine-tuning adds a few days when the backtest justifies it.
That is a fundamentally different plan from the per-series retraining projects of a few years ago, and it is worth resetting stakeholder expectations accordingly.
Planning a forecasting build on SageMaker AI, or want a second opinion on a backtest before it goes to the planners? Talk to us.