Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chronos handler to support chronos-bolt-* models #223

Open
wants to merge 40 commits into
base: new_model_integrations
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
1f00c9a
relax parameters strictness
wgifford Nov 26, 2024
27f6a3c
allow extra
wgifford Nov 26, 2024
fa81367
Merge pull request #214 from ibm-granite/service_updates
ssiegel95 Nov 30, 2024
6dd4d4d
clarify citations
wgifford Dec 3, 2024
ab5777d
Merge pull request #218 from ibm-granite/wiki_update
wgifford Dec 3, 2024
2c7d487
gluonts data wrapper, and ttm gluonts predictor
ajati Dec 3, 2024
3598090
enable truncation of context len in ttm
ajati Dec 4, 2024
176e7d1
fix issues with future exogenous
wgifford Dec 4, 2024
3859359
force_return in get_model
ajati Dec 4, 2024
a1da2c5
Merge pull request #220 from ibm-granite/pipeline_exog
wgifford Dec 4, 2024
beba825
code moved to extras folder outside tsfm_public
ajati Dec 5, 2024
c0b03eb
tests moved
ajati Dec 5, 2024
3df4e68
gift srcs removed, get_model updated
ajati Dec 5, 2024
69ed4fd
revert toml and visualization functions
ajati Dec 5, 2024
62ea27f
add optional verbose payload dumps
ssiegel95 Dec 6, 2024
bfa6535
exception -> valueerror
ajati Dec 6, 2024
98730fd
Merge pull request #219 from ibm-granite/gift
wgifford Dec 6, 2024
8354f6a
we can't resolve to a single directory here, need to scan them in load
ssiegel95 Dec 6, 2024
96e5bb4
add additional directory to TSFM_MODEL_DIR
ssiegel95 Dec 6, 2024
7d8d3be
model path resolver
ssiegel95 Dec 6, 2024
82ab987
ignore prometheus metrics dir
ssiegel95 Dec 6, 2024
1a1b75a
model dir resolver
ssiegel95 Dec 6, 2024
06eb16b
use model path resolver
ssiegel95 Dec 6, 2024
eddab4e
test model path resolver
ssiegel95 Dec 6, 2024
b0a6809
Merge remote-tracking branch 'origin/main' into byom
ssiegel95 Dec 6, 2024
f45c0b7
boilerplate code
ssiegel95 Dec 9, 2024
1e67a11
ignore dirutil.py
ssiegel95 Dec 9, 2024
35870fd
automate maintenance of .gitignore
ssiegel95 Dec 9, 2024
0afbf8d
Merge pull request #222 from ibm-granite/byom
ssiegel95 Dec 9, 2024
6d90328
chronos handler to support chronos-bolt-* models
gganapavarapu Dec 9, 2024
cd10ff4
test min context length 2
gganapavarapu Dec 10, 2024
5330e1e
explicitly set device
wgifford Dec 10, 2024
c96905d
select device
wgifford Dec 10, 2024
48346a6
Merge pull request #225 from ibm-granite/set_service_device
wgifford Dec 11, 2024
071f0c3
merge main
gganapavarapu Dec 11, 2024
88067b3
fix merge issue and revert cd10ff4
gganapavarapu Dec 11, 2024
ebe0f8e
chronos repo name in deps, make style
gganapavarapu Dec 11, 2024
d4f57ee
poetry lock
gganapavarapu Dec 12, 2024
9b793f2
ID column support for chronos models
gganapavarapu Dec 13, 2024
61e822a
support no ID columns as well for chronos models
gganapavarapu Dec 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions services/boilerplate/dirutil.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Utilities for directory operations."""

import os
from pathlib import Path


def resolve_model_path(search_path: str, model_id: str) -> Path:
"""Find the first path under search_path for model_id. All entries in
search_path must be:
* an existing directory
* must be readable by the current process

Args:
search_path (str): A unix-like ":" separated list of directories such a "dir1:dir2"
model_id (str): a model_id (which is really just a subdirectory under dir1 or dir2)

Returns:
Path: the first matching path, None if no path is fount.
"""

_amodeldir_found = next(
(
adir
for adir in (Path(p) for p in search_path.split(":"))
if adir.exists()
and adir.is_dir()
and os.access(adir, os.R_OK)
and (adir / model_id).exists()
and os.access(adir / model_id, os.R_OK)
),
None,
)
if not _amodeldir_found:
return None
return _amodeldir_found / model_id
6 changes: 2 additions & 4 deletions services/boilerplate/inference_payloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,10 @@ class ForecastingMetadataInput(BaseMetadataInput):


class BaseParameters(BaseModel):
model_config = ConfigDict(extra="forbid", protected_namespaces=())

model_config = ConfigDict(extra="allow", protected_namespaces=())

class ForecastingParameters(BaseModel):
model_config = ConfigDict(extra="forbid", protected_namespaces=())

class ForecastingParameters(BaseParameters):
prediction_length: Optional[int] = Field(
description="The prediction length for the forecast."
" The service will return this many periods beyond the last"
Expand Down
3 changes: 3 additions & 0 deletions services/finetuning/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ CONTAINER_BUILDER ?= docker

# copies boilerplate code to suitable locations
boilerplate:
rm tsfmfinetuning/.gitignore || true
echo "# THIS FILE IS AUTOMATICALLY GENERATED, YOUR CHANGES WILL BE OVERWRITTEN" > tsfmfinetuning/.gitignore
for f in ../boilerplate/*.py; do \
echo $$f; \
cat ../boilerplate/warning.txt > tsfmfinetuning/$$(basename $$f); \
cat $$f>>tsfmfinetuning/$$(basename $$f); \
echo $$(basename $$f) >> tsfmfinetuning/.gitignore; \
done

image:
Expand Down
7 changes: 5 additions & 2 deletions services/finetuning/tsfmfinetuning/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
inference_payloads.py
hfutil.py
# THIS FILE IS AUTOMATICALLY GENERATED, YOUR CHANGES WILL BE OVERWRITTEN
dataframe_checks.py
dirutil.py
errors.py
hfutil.py
inference_payloads.py
3 changes: 2 additions & 1 deletion services/finetuning/tsfmfinetuning/finetuning.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Any, Dict, Tuple, Union

import pandas as pd
import torch
from fastapi import APIRouter, HTTPException
from starlette import status
from transformers import EarlyStoppingCallback, Trainer, TrainingArguments, set_seed
Expand Down Expand Up @@ -215,7 +216,7 @@ def _finetuning_common(
metric_for_best_model="eval_loss", # Metric to monitor for early stopping
greater_is_better=False, # For loss
label_names=["future_values"],
use_cpu=True, # only needed for testing on Mac :(
use_cpu=not torch.cuda.is_available(),
)

callbacks = []
Expand Down
4 changes: 1 addition & 3 deletions services/inference/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
# These version placeholders will be replaced later during substitution.
__version__ = "0.0.0"
__version_tuple__ = (0, 0, 0)
prometheus_metrics
5 changes: 4 additions & 1 deletion services/inference/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ CONTAINER_BUILDER ?= docker

# copies boilerplate code to suitable locations
boilerplate:
rm tsfminference/.gitignore || true
echo "# THIS FILE IS AUTOMATICALLY GENERATED, YOUR CHANGES WILL BE OVERWRITTEN" > tsfminference/.gitignore
for f in ../boilerplate/*.py; do \
echo $$f; \
cat ../boilerplate/warning.txt > tsfminference/$$(basename $$f); \
cat $$f>>tsfminference/$$(basename $$f); \
echo $$(basename $$f) >> tsfminference/.gitignore; \
done

create_prometheus_metrics_dir:
Expand All @@ -16,7 +19,7 @@ create_prometheus_metrics_dir:
start_service_local: create_prometheus_metrics_dir boilerplate
PROMETHEUS_MULTIPROC_DIR=./prometheus_metrics \
TSFM_PYTHON_LOGGING_LEVEL="ERROR" \
TSFM_MODEL_DIR=./mytest-tsfm \
TSFM_MODEL_DIR=./foobaz:./mytest-tsfm \
TSFM_ALLOW_LOAD_FROM_HF_HUB=1 \
python -m gunicorn \
-w 1 \
Expand Down
1,547 changes: 726 additions & 821 deletions services/inference/poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion services/inference/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ urllib3 = { version = ">=1.26.19,<2" } # see https://github.com/urllib3/urllib3/
aiohttp = { version = ">=3.10.11" }

# ***********Chronos*********
chronos = { git = "https://github.com/amazon-science/chronos-forecasting.git" }
chronos-forecasting = { git = "https://github.com/amazon-science/chronos-forecasting.git" }

[[tool.poetry.source]]
name = "pytorch"
Expand Down
13 changes: 12 additions & 1 deletion services/inference/tests/test_inference_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
#

import copy
import json
import os
import tempfile
from datetime import timedelta
from pathlib import Path

Expand All @@ -13,6 +15,7 @@
from fastapi import HTTPException
from pytest import FixtureRequest
from tsfminference import TSFM_CONFIG_FILE, TSFM_MODEL_DIR
from tsfminference.dirutil import resolve_model_path
from tsfminference.inference import InferenceRuntime
from tsfminference.inference_payloads import (
ForecastingInferenceInput,
Expand All @@ -35,7 +38,7 @@


def min_context_length(model_id):
model_path: Path = TSFM_MODEL_DIR / model_id
model_path: Path = resolve_model_path(TSFM_MODEL_DIR, model_id)
assert model_path.exists(), f"{model_path} does not exist!"
handler, e = ForecastingServiceHandler.load(model_id=model_id, model_path=model_path)
return handler.handler_config.minimum_context_length
Expand Down Expand Up @@ -112,6 +115,14 @@ def test_forecast_with_good_data(ts_data_base: pd.DataFrame, forecasting_input_b
return
df = copy.deepcopy(data)
input.data = df.to_dict(orient="list")

# useful for generating sample payload files
if int(os.environ.get("TSFM_TESTS_DO_VERBOSE_DUMPS", "0")) == 1:
with open(f"{tempfile.gettempdir()}/{model_id}.payload.json", "w") as out:
foo = copy.deepcopy(df)
foo["date"] = foo["date"].apply(lambda x: x.isoformat())
json.dump(foo.to_dict(orient="list"), out)

runtime: InferenceRuntime = InferenceRuntime(config=config)
po: PredictOutput = runtime.forecast(input=input)
results = pd.DataFrame.from_dict(po.results[0])
Expand Down
90 changes: 81 additions & 9 deletions services/inference/tests/test_inference_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"ttm-1536-96-r2": {"context_length": 1536, "prediction_length": 96},
"ibm/test-patchtst": {"context_length": 512, "prediction_length": 96},
"ibm/test-patchtsmixer": {"context_length": 512, "prediction_length": 96},
"chronos-t5-tiny": {"context_length": 512, "prediction_length": 96},
"chronos-t5-tiny": {"context_length": 512, "prediction_length": 16},
"chronos-bolt-tiny": {"context_length": 512, "prediction_length": 16},
}


Expand Down Expand Up @@ -369,24 +370,28 @@ def test_zero_shot_forecast_inference(ts_data):
assert counts["output_data_points"] == (prediction_length // 4) * len(params["target_columns"][1:])


@pytest.mark.parametrize("ts_data", ["chronos-t5-tiny"], indirect=True)
@pytest.mark.parametrize("ts_data", ["chronos-t5-tiny", "chronos-bolt-tiny"], indirect=True)
def test_zero_shot_forecast_inference_chronos(ts_data):
test_data, params = ts_data

prediction_length = params["prediction_length"]
model_id = params["model_id"]
model_id_path: str = model_id

id_columns = params["id_columns"]
num_samples = 10

# test single
test_data_ = test_data[test_data[id_columns[0]] == "a"].copy()

parameters = {
"prediction_length": params["prediction_length"],
}
if model_id == "chronos-t5-tiny":
parameters["num_samples"] = num_samples

msg = {
"model_id": model_id_path,
"parameters": {
"prediction_length": params["prediction_length"],
},
"parameters": parameters,
"schema": {
"timestamp_column": params["timestamp_column"],
"id_columns": params["id_columns"],
Expand All @@ -400,6 +405,7 @@ def test_zero_shot_forecast_inference_chronos(ts_data):
assert len(df_out) == 1
assert df_out[0].shape[0] == prediction_length

# test with future data. should throw error.
test_data_ = test_data[test_data[id_columns[0]] == "a"].copy()
future_data = extend_time_series(
select_by_index(test_data_, id_columns=params["id_columns"], start_index=-1),
Expand All @@ -414,9 +420,7 @@ def test_zero_shot_forecast_inference_chronos(ts_data):

msg = {
"model_id": model_id,
"parameters": {
# "prediction_length": params["prediction_length"],
},
"parameters": parameters,
"schema": {
"timestamp_column": params["timestamp_column"],
"id_columns": params["id_columns"],
Expand All @@ -430,6 +434,74 @@ def test_zero_shot_forecast_inference_chronos(ts_data):
out, _ = get_inference_response(msg)
assert "Chronos does not support or require future exogenous." in out.text

# test multi-time series
num_ids = test_data[id_columns[0]].nunique()
test_data_ = test_data.copy()

msg = {
"model_id": model_id_path,
"parameters": parameters,
"schema": {
"timestamp_column": params["timestamp_column"],
"id_columns": params["id_columns"],
"target_columns": params["target_columns"],
},
"data": encode_data(test_data_, params["timestamp_column"]),
"future_data": {},
}

df_out, _ = get_inference_response(msg)

assert len(df_out) == 1
assert df_out[0].shape[0] == prediction_length * num_ids

# test multi-time series multi-id
multi_df = []
for grp in ["A", "B"]:
td = test_data.copy()
td["id2"] = grp
multi_df.append(td)
test_data_ = pd.concat(multi_df, ignore_index=True)
new_id_columns = id_columns + ["id2"]

num_ids = test_data_[new_id_columns[0]].nunique() * test_data_[new_id_columns[1]].nunique()

msg = {
"model_id": model_id_path,
"parameters": parameters,
"schema": {
"timestamp_column": params["timestamp_column"],
"id_columns": new_id_columns,
"target_columns": params["target_columns"],
},
"data": encode_data(test_data_, params["timestamp_column"]),
"future_data": {},
}

df_out, _ = get_inference_response(msg)
assert len(df_out) == 1
assert df_out[0].shape[0] == prediction_length * num_ids

# single series, less columns, no id
test_data_ = test_data[test_data[id_columns[0]] == "a"].copy()

msg = {
"model_id": model_id_path,
"parameters": parameters,
"schema": {
"timestamp_column": params["timestamp_column"],
"id_columns": [],
"target_columns": ["HULL"],
},
"data": encode_data(test_data_, params["timestamp_column"]),
"future_data": {},
}

df_out, counts = get_inference_response(msg)
assert len(df_out) == 1
assert df_out[0].shape[0] == prediction_length
assert df_out[0].shape[1] == 2


@pytest.mark.parametrize("ts_data", ["ttm-r2-etth-finetuned-control"], indirect=True)
def test_future_data_forecast_inference(ts_data):
Expand Down
6 changes: 4 additions & 2 deletions services/inference/tsfminference/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
inference_payloads.py
# THIS FILE IS AUTOMATICALLY GENERATED, YOUR CHANGES WILL BE OVERWRITTEN
dataframe_checks.py
dirutil.py
errors.py
hfutil.py
dataframe_checks.py
inference_payloads.py
21 changes: 17 additions & 4 deletions services/inference/tsfminference/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,20 @@
)

# use TSFM_MODEL_DIR preferentially. If not set, use HF_HOME or the system tempdir if that's not set.
TSFM_MODEL_DIR: Path = Path(os.environ.get("TSFM_MODEL_DIR", os.environ.get("HF_HOME", tempfile.gettempdir())))

if not TSFM_MODEL_DIR.exists():
raise Exception(f"TSFM_MODEL_DIR {TSFM_MODEL_DIR} does not exist.")
TSFM_MODEL_DIR: str = os.environ.get("TSFM_MODEL_DIR", os.environ.get("HF_HOME", tempfile.gettempdir()))

# basic checks
# make sure at least one of them is a valid directory
# make sure it's readable as well
_amodeldir_found = next(
(
adir
for adir in (Path(p) for p in TSFM_MODEL_DIR.split(":"))
if adir.exists() and adir.is_dir() and os.access(adir, os.R_OK)
),
None,
)
if not _amodeldir_found and not TSFM_ALLOW_LOAD_FROM_HF_HUB:
raise Exception(
f"None of the values given in TSFM_MODEL_DIR {TSFM_MODEL_DIR} are an existing and readable directory."
)
Loading
Loading