From 64564c2adf8a0fc3842f7b09a7b1fbfc4a12efdb Mon Sep 17 00:00:00 2001 From: Yichen Zhou Date: Tue, 9 Dec 2025 16:49:57 -0800 Subject: [PATCH] TimesFM-2.5 docker for Model Garden. PiperOrigin-RevId: 842450195 --- ...den_timesfm_2_5_deployment_on_vertex.ipynb | 928 ++++++++++++++++++ 1 file changed, 928 insertions(+) create mode 100644 notebooks/community/model_garden/model_garden_timesfm_2_5_deployment_on_vertex.ipynb diff --git a/notebooks/community/model_garden/model_garden_timesfm_2_5_deployment_on_vertex.ipynb b/notebooks/community/model_garden/model_garden_timesfm_2_5_deployment_on_vertex.ipynb new file mode 100644 index 000000000..b9c27db49 --- /dev/null +++ b/notebooks/community/model_garden/model_garden_timesfm_2_5_deployment_on_vertex.ipynb @@ -0,0 +1,928 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "vbnQoeZABLr6" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dlUIoPa6BB5Z" + }, + "source": [ + "# Vertex AI Model Garden - TimesFM 2.5 (CPU/GPU Deployment)\n", + "\n", + "\n", + " \n", + " \n", + "
\n", + " \n", + " \"Google
Run in Colab Enterprise\n", + "
\n", + "
\n", + " \n", + " \"GitHub
View on GitHub\n", + "
\n", + "
\n", + "\n", + "## Overview\n", + "\n", + "This notebook demonstrates deploying TimesFM 2.5 to a Vertex AI Endpoint and\n", + "making online predictions for times series forecast.\n", + "\n", + "### Objective\n", + "\n", + "- Deploy TimesFM 2.5 to a Vertex AI Endpoint.\n", + "- Make predictions to the endpoint for times series forecast.\n", + "\n", + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* Vertex AI\n", + "* Cloud Storage\n", + "\n", + "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing), [Cloud Storage pricing](https://cloud.google.com/storage/pricing), and use the [Pricing Calculator](https://cloud.google.com/products/calculator/) to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pfD0Gf2Z9RMA" + }, + "source": [ + "## Version History\n", + "\n", + "This notebook demonstrates how to call the latest TimesFM 2.5 endpoint.\n", + "- **Deploy Resource ID**: google/timesfm-2.5" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "o8KqOx2DC3Yc" + }, + "source": [ + "## Setup Google Cloud project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "iNgqf6w1DCZF" + }, + "outputs": [], + "source": [ + "# @markdown ### **Prerequisites**\n", + "# @markdown 1. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "# @markdown 2. **[Optional]** [Create a Cloud Storage bucket](https://cloud.google.com/storage/docs/creating-buckets) for storing experiment outputs. Set the BUCKET_URI for the experiment environment. The specified Cloud Storage bucket (`BUCKET_URI`) should be located in the same region as where the notebook was launched. Note that a multi-region bucket (eg. \"us\") is not considered a match for a single region covered by the multi-region range (eg. \"us-central1\"). If not set, a unique GCS bucket will be created instead.\n", + "\n", + "BUCKET_URI = \"gs://\" # @param {type:\"string\"}\n", + "\n", + "# @markdown 3. **[Optional]** Set region. If not set, the region will be set automatically according to Colab Enterprise environment.\n", + "\n", + "REGION = \"\" # @param {type:\"string\"}\n", + "\n", + "# @markdown 4. If you want to run predictions with A100 80GB or H100 GPUs, we recommend using the regions listed below. **NOTE:** Make sure you have associated quota in selected regions. Click the links to see your current quota for each GPU type: [Nvidia A100 80GB](https://console.cloud.google.com/iam-admin/quotas?metric=aiplatform.googleapis.com%2Fcustom_model_serving_nvidia_a100_80gb_gpus), [Nvidia H100 80GB](https://console.cloud.google.com/iam-admin/quotas?metric=aiplatform.googleapis.com%2Fcustom_model_serving_nvidia_h100_gpus). You can request for quota following the instructions at [\"Request a higher quota\"](https://cloud.google.com/docs/quota/view-manage#requesting_higher_quota).\n", + "\n", + "# @markdown | Machine Type | Accelerator Type | Recommended Regions |\n", + "# @markdown | ----------- | ----------- | ----------- |\n", + "# @markdown | a2-ultragpu-1g | 1 NVIDIA_A100_80GB | us-central1, us-east4, europe-west4, asia-southeast1, us-east4 |\n", + "# @markdown | a3-highgpu-2g | 2 NVIDIA_H100_80GB | us-west1, asia-southeast1, europe-west4 |\n", + "# @markdown | a3-highgpu-4g | 4 NVIDIA_H100_80GB | us-west1, asia-southeast1, europe-west4 |\n", + "# @markdown | a3-highgpu-8g | 8 NVIDIA_H100_80GB | us-central1, europe-west4, us-west1, asia-southeast1 |\n", + "\n", + "! git clone https://github.com/GoogleCloudPlatform/vertex-ai-samples.git\n", + "\n", + "import datetime\n", + "import importlib\n", + "import os\n", + "import uuid\n", + "from typing import Tuple\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "from google.cloud import aiplatform\n", + "from google.colab import output\n", + "\n", + "common_util = importlib.import_module(\n", + " \"vertex-ai-samples.notebooks.community.model_garden.docker_source_codes.notebook_util.common_util\"\n", + ")\n", + "\n", + "models, endpoints = {}, {}\n", + "\n", + "\n", + "# Get the default cloud project id.\n", + "PROJECT_ID = os.environ[\"GOOGLE_CLOUD_PROJECT\"]\n", + "\n", + "# Get the default region for launching jobs.\n", + "if not REGION:\n", + " if not os.environ.get(\"GOOGLE_CLOUD_REGION\"):\n", + " raise ValueError(\n", + " \"REGION must be set. See\"\n", + " \" https://cloud.google.com/vertex-ai/docs/general/locations for\"\n", + " \" available cloud locations.\"\n", + " )\n", + " REGION = os.environ[\"GOOGLE_CLOUD_REGION\"]\n", + "\n", + "# Enable the Vertex AI API and Compute Engine API, if not already.\n", + "print(\"Enabling Vertex AI API and Compute Engine API.\")\n", + "! gcloud services enable aiplatform.googleapis.com compute.googleapis.com\n", + "\n", + "# Cloud Storage bucket for storing the experiment artifacts.\n", + "# A unique GCS bucket will be created for the purpose of this notebook. If you\n", + "# prefer using your own GCS bucket, change the value yourself below.\n", + "now = datetime.datetime.now().strftime(\"%Y%m%d%H%M%S\")\n", + "BUCKET_NAME = \"/\".join(BUCKET_URI.split(\"/\")[:3])\n", + "\n", + "if BUCKET_URI is None or BUCKET_URI.strip() == \"\" or BUCKET_URI == \"gs://\":\n", + " BUCKET_URI = f\"gs://{PROJECT_ID}-tmp-{now}-{str(uuid.uuid4())[:4]}\"\n", + " BUCKET_NAME = \"/\".join(BUCKET_URI.split(\"/\")[:3])\n", + " ! gsutil mb -l {REGION} {BUCKET_URI}\n", + "else:\n", + " assert BUCKET_URI.startswith(\"gs://\"), \"BUCKET_URI must start with `gs://`.\"\n", + " shell_output = ! gsutil ls -Lb {BUCKET_NAME} | grep \"Location constraint:\" | sed \"s/Location constraint://\"\n", + " bucket_region = shell_output[0].strip().lower()\n", + " if bucket_region != REGION:\n", + " raise ValueError(\n", + " \"Bucket region %s is different from notebook region %s\"\n", + " % (bucket_region, REGION)\n", + " )\n", + "print(f\"Using this GCS Bucket: {BUCKET_URI}\")\n", + "\n", + "STAGING_BUCKET = os.path.join(BUCKET_URI, \"temporal\")\n", + "MODEL_BUCKET = os.path.join(BUCKET_URI, \"timesfm\")\n", + "\n", + "\n", + "# Initialize Vertex AI API.\n", + "print(\"Initializing Vertex AI API.\")\n", + "aiplatform.init(project=PROJECT_ID, location=REGION, staging_bucket=STAGING_BUCKET)\n", + "\n", + "# Gets the default SERVICE_ACCOUNT.\n", + "shell_output = ! gcloud projects describe $PROJECT_ID\n", + "project_number = shell_output[-1].split(\":\")[1].strip().replace(\"'\", \"\")\n", + "SERVICE_ACCOUNT = f\"{project_number}-compute@developer.gserviceaccount.com\"\n", + "print(\"Using this default Service Account:\", SERVICE_ACCOUNT)\n", + "\n", + "\n", + "# Provision permissions to the SERVICE_ACCOUNT with the GCS bucket\n", + "! gsutil iam ch serviceAccount:{SERVICE_ACCOUNT}:roles/storage.admin $BUCKET_NAME\n", + "\n", + "! gcloud config set project $PROJECT_ID\n", + "! gcloud projects add-iam-policy-binding --no-user-output-enabled {PROJECT_ID} --member=serviceAccount:{SERVICE_ACCOUNT} --role=\"roles/storage.admin\"\n", + "! gcloud projects add-iam-policy-binding --no-user-output-enabled {PROJECT_ID} --member=serviceAccount:{SERVICE_ACCOUNT} --role=\"roles/aiplatform.user\"\n", + "\n", + "# @markdown ### **Choose a prebuilt checkpoint**\n", + "# @markdown Here we specify where to get the model checkpoint. TimesFM\n", + "# @markdown pretrained checkpoints are by default saved under\n", + "# @markdown `gs://vertex-model-garden-public-{region}/timesfm` and indexed by\n", + "# @markdown the checkpoint version.\n", + "\n", + "VERTEX_AI_MODEL_GARDEN_TIMESFM = \"gs://vertex-model-garden-public-us/timesfm\" # @param {type:\"string\", isTemplate:true} [\"gs://vertex-model-garden-public-us/timesfm\", \"gs://vertex-model-garden-public-eu/timesfm\", \"gs://vertex-model-garden-public-asia/timesfm\"]\n", + "MODEL_VARIANT = \"timesfm-2.0-500m-jax\" # @param [\"timesfm-2.0-500m-jax\"]\n", + "\n", + "\n", + "print(\n", + " \"Copying TimesFM model artifacts from\",\n", + " f\"{VERTEX_AI_MODEL_GARDEN_TIMESFM}/{MODEL_VARIANT}\",\n", + " \"to\",\n", + " MODEL_BUCKET,\n", + ")\n", + "\n", + "! gcloud storage cp -R $VERTEX_AI_MODEL_GARDEN_TIMESFM/$MODEL_VARIANT $MODEL_BUCKET\n", + "\n", + "model_path_prefix = MODEL_BUCKET" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MLTYNURLi6LQ" + }, + "source": [ + "## Deploy TimesFM to a Vertex AI Endpoint" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "UwyXUWD9i9fe" + }, + "outputs": [], + "source": [ + "# @markdown This section uploads the prebuilt TimesFM model to Model Registry\n", + "# @markdown and deploys it to a Vertex AI Endpoint.\n", + "# @markdown It takes **approximately 20 minutes** to deploy.\n", + "\n", + "# @markdown ### **Step 1: Set the checkpoint path**\n", + "# @markdown Leave this blank to load the checkpoint we copied over earlier.\n", + "# @markdown If you've brought your own checkpoint, specify its path here.\n", + "# @markdown\n", + "# @markdown **Note**: Most of the time you should leave it blank (as is)\n", + "# @markdown when you've chosen to use a prebuilt checkpoint.\n", + "# @markdown\n", + "\n", + "custom_timesfm_model_uri = \"gs://\" # @param {type: \"string\"}\n", + "\n", + "if custom_timesfm_model_uri == \"gs://\" or not custom_timesfm_model_uri:\n", + " print(\"Deploying prebuilt TimesFM model. \")\n", + " checkpoint_path = model_path_prefix\n", + "else:\n", + " print(\"Deploying custom TimesFM model.\")\n", + " checkpoint_path = custom_timesfm_model_uri\n", + "print(f\"Loading checkpoint from {checkpoint_path}.\")\n", + "\n", + "# @markdown ### **Step 2: Choose the accelerator**\n", + "# @markdown Select the accelerator type to use to deploy the model.\n", + "# @markdown\n", + "# @markdown **Note**: Most of the time you can go with CPU only. TimesFM is\n", + "# @markdown fast even with the CPU backend. You can only consider GPU if you\n", + "# @markdown need a dedicated endpoint to handle large queries per second.\n", + "# @markdown\n", + "# @markdown **Note**: After deployment, take a look at the log to get\n", + "# @markdown the model / enpoint that you can use in another session.\n", + "# @markdown\n", + "\n", + "accelerator_type = \"CPU\" # @param [\"CPU\", \"NVIDIA_L4\"]\n", + "if accelerator_type == \"NVIDIA_L4\":\n", + " machine_type = \"g2-standard-8\"\n", + " accelerator_count = 1\n", + "elif accelerator_type == \"CPU\":\n", + " accelerator_type = \"ACCELERATOR_TYPE_UNSPECIFIED\"\n", + " machine_type = \"n1-standard-8\"\n", + " accelerator_count = 0\n", + "else:\n", + " raise ValueError(\n", + " f\"Recommended machine settings not found for: {accelerator_type}. To use\"\n", + " \" another another accelerator, edit this code block to pass in an\"\n", + " \" appropriate `machine_type`, `accelerator_type`, and\"\n", + " \" `accelerator_count` to the deploy_model function by clicking `Show\"\n", + " \" Code` and then modifying the code.\"\n", + " )\n", + "\n", + "if accelerator_type != \"ACCELERATOR_TYPE_UNSPECIFIED\":\n", + " common_util.check_quota(\n", + " project_id=PROJECT_ID,\n", + " region=REGION,\n", + " accelerator_type=accelerator_type,\n", + " accelerator_count=accelerator_count,\n", + " is_for_training=False,\n", + " )\n", + "\n", + "print(\"Quota is OK.\")\n", + "# @markdown If you want to use other accelerator types not listed above,\n", + "# @markdown check other Vertex AI prediction supported accelerators and regions\n", + "# @markdown at https://cloud.google.com/vertex-ai/docs/predictions/configure-compute.\n", + "# @markdown You may need to manually set the `machine_type`, `accelerator_type`,\n", + "# @markdown and `accelerator_count` in the code by clicking `Show code` first.\n", + "\n", + "# @markdown ### **Step 3: Set the maximum forecast horizon**\n", + "# @markdown **Leave both values at 0 if you want the endpoint to dynamically**\n", + "# @markdown **compile based on the batch level context and horizon requested**\n", + "# @markdown (which is slower if these numbers vary much between batches).\n", + "\n", + "# @markdown Otherise, we specify the maximum forecast context and horizon\n", + "# @markdown TimesFM will be queried on to compile its computation.\n", + "# @markdown The endpoint will always predict this number of time points\n", + "# @markdown in the future, possibly after being rounded\n", + "# @markdown up to the closest multiplier of the model output patch length.\n", + "# @markdown Make sure to set it to the potential maximum for your usecase.\n", + "\n", + "horizon = \"0\" # @param [0, 128, 256, 512, 1024]\n", + "max_context = \"0\" # @param [0, 512, 2048, 8192]\n", + "print(\"Creating endpoint.\")\n", + "\n", + "SERVE_DOCKER_URI = \"us-docker.pkg.dev/vertex-ai-restricted/vertex-vision-model-garden-dockers/timesfm-serve-v2:latest\"\n", + "# @markdown Set `use_dedicated_endpoint` to False if you don't want to use [dedicated endpoint](https://cloud.google.com/vertex-ai/docs/general/deployment#create-dedicated-endpoint).\n", + "use_dedicated_endpoint = True # @param {type:\"boolean\"}\n", + "\n", + "\n", + "def deploy_model(\n", + " model_name: str,\n", + " checkpoint_path: str,\n", + " max_context: str,\n", + " horizon: str,\n", + " machine_type: str = \"g2-standard-8\",\n", + " accelerator_type: str = \"NVIDIA_L4\",\n", + " accelerator_count: int = 1,\n", + " deploy_source: str = \"notebook\",\n", + " use_dedicated_endpoint: bool = False,\n", + ") -> Tuple[aiplatform.Model, aiplatform.Endpoint]:\n", + " \"\"\"Creates a Vertex AI Endpoint and deploys TimesFM to the endpoint.\"\"\"\n", + " model_name_with_time = common_util.get_job_name_with_datetime(model_name)\n", + " endpoint = aiplatform.Endpoint.create(\n", + " display_name=f\"{model_name_with_time}-endpoint\",\n", + " credentials=aiplatform.initializer.global_config.credentials,\n", + " dedicated_endpoint_enabled=use_dedicated_endpoint,\n", + " )\n", + "\n", + " if accelerator_type == \"ACCELERATOR_TYPE_UNSPECIFIED\":\n", + " accelerator_type = None\n", + "\n", + " model = aiplatform.Model.upload(\n", + " display_name=model_name_with_time,\n", + " serving_container_image_uri=SERVE_DOCKER_URI,\n", + " serving_container_ports=[8080],\n", + " serving_container_predict_route=\"/predict\",\n", + " serving_container_health_route=\"/health\",\n", + " serving_container_environment_variables={\n", + " \"MODEL_ID\": checkpoint_path,\n", + " \"DEPLOY_SOURCE\": deploy_source,\n", + " \"AIP_STORAGE_URI\": checkpoint_path,\n", + " \"TIMESFM_CONTEXT\": max_context,\n", + " \"TIMESFM_HORIZON\": horizon,\n", + " },\n", + " credentials=aiplatform.initializer.global_config.credentials,\n", + " model_garden_source_model_name=\"publishers/google/models/timesfm2p5\",\n", + " )\n", + " print(\n", + " f\"Deploying {model_name_with_time} on {machine_type} with\"\n", + " f\" {accelerator_count} {accelerator_type} GPU(s).\"\n", + " )\n", + " model.deploy(\n", + " endpoint=endpoint,\n", + " machine_type=machine_type,\n", + " accelerator_type=accelerator_type,\n", + " accelerator_count=accelerator_count,\n", + " deploy_request_timeout=1800,\n", + " service_account=SERVICE_ACCOUNT,\n", + " enable_access_logging=True,\n", + " min_replica_count=1,\n", + " sync=True,\n", + " system_labels={\n", + " \"NOTEBOOK_NAME\": \"model_garden_timesfm_deployment_on_vertex.ipynb\"\n", + " },\n", + " )\n", + " return model, endpoint\n", + "\n", + "\n", + "models[\"timesfm\"], endpoints[\"timesfm\"] = deploy_model(\n", + " model_name=f\"timesfm-{MODEL_VARIANT}\",\n", + " checkpoint_path=checkpoint_path,\n", + " max_context=max_context,\n", + " horizon=horizon,\n", + " machine_type=machine_type,\n", + " accelerator_type=accelerator_type,\n", + " accelerator_count=accelerator_count,\n", + " use_dedicated_endpoint=use_dedicated_endpoint,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VzCdSZsilcEv" + }, + "source": [ + "## Query TimesFM\n", + "\n", + "An endpoint prediction request looks like, with the `input` field mandatory and everything else optional.\n", + "```python\n", + "endpoint.predict(\n", + " instances=[\n", + " {\n", + " \"input\": [0.0, 0.1, 0.2, ...], # Mandatory\n", + " \"horizon\": 12, # Mandatory\n", + " \"timestamp\": [\"2024-01-01\", \"2024-01-02\", ...],\n", + " \"timestamp_format\": \"%Y-%m-%d\",\n", + " \"dynamic_numerical_covariates\": {\n", + " \"dncov1\": [1.0, 2.0, 1.5, ...],\n", + " \"dncov2\": [3.0, 1.1, 2.4, ...],\n", + " },\n", + " \"dynamic_categorical_covariates\": {\n", + " \"dccov1\": [\"a\", \"b\", \"a\", ...],\n", + " \"dccov2\": [0, 1, 0, ...],\n", + " },\n", + " \"static_numerical_covariates\": {\n", + " \"sncov1\": 1.0,\n", + " \"sncov2\": 2.0,\n", + " },\n", + " \"static_categorical_covariates\": {\n", + " \"sccov1\": \"yes\",\n", + " \"sccov2\": \"no\",\n", + " },\n", + " \"xreg_kwargs\": {...},\n", + " },\n", + " {\n", + " \"input\": [113.2, 15.0, 65.4],\n", + " ...,\n", + " },\n", + " {\n", + " \"input\": [0.0, 10.0, 20.0],\n", + " ...,\n", + " },\n", + " ]\n", + ")\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "dBIpE8eRarWL" + }, + "outputs": [], + "source": [ + "# @markdown Create a helper function to visulize forecasts.\n", + "\n", + "\n", + "class Visualizer:\n", + " def __init__(self, nrows, ncols):\n", + " self.ncols = ncols\n", + " self.num_images = nrows * ncols\n", + " self.fig, self.axes = plt.subplots(\n", + " nrows, ncols, figsize=(ncols * 4, nrows * 2.5)\n", + " )\n", + " self.axes = self.axes.flatten()\n", + " self.index = 0\n", + "\n", + " def visualize_forecast(\n", + " self,\n", + " context: list[float],\n", + " horizon_mean: list[float],\n", + " ground_truth: list[float] | None = None,\n", + " horizon_lower: list[float] | None = None,\n", + " horizon_upper: list[float] | None = None,\n", + " ylabel: str | None = None,\n", + " title: str | None = None,\n", + " ):\n", + " plt_range = list(range(len(context) + len(horizon_mean)))\n", + " self.axes[self.index].plot(\n", + " plt_range,\n", + " context + [np.nan for _ in horizon_mean],\n", + " color=\"tab:cyan\",\n", + " label=\"context\",\n", + " )\n", + " self.axes[self.index].plot(\n", + " plt_range,\n", + " [np.nan for _ in context] + horizon_mean,\n", + " color=\"tab:red\",\n", + " label=\"forecast\",\n", + " )\n", + " if ground_truth:\n", + " self.axes[self.index].plot(\n", + " list(range(len(context) + len(ground_truth))),\n", + " [np.nan for _ in context] + ground_truth,\n", + " color=\"tab:purple\",\n", + " label=\"ground truth\",\n", + " )\n", + " if horizon_upper and horizon_lower:\n", + " self.axes[self.index].plot(\n", + " plt_range,\n", + " [np.nan for _ in context] + horizon_upper,\n", + " color=\"tab:orange\",\n", + " linestyle=\"--\",\n", + " label=\"forecast, upper\",\n", + " )\n", + " self.axes[self.index].plot(\n", + " plt_range,\n", + " [np.nan for _ in context] + horizon_lower,\n", + " color=\"tab:orange\",\n", + " linestyle=\":\",\n", + " label=\"forecast, lower\",\n", + " )\n", + " self.axes[self.index].fill_between(\n", + " plt_range,\n", + " [np.nan for _ in context] + horizon_upper,\n", + " [np.nan for _ in context] + horizon_lower,\n", + " color=\"tab:orange\",\n", + " alpha=0.2,\n", + " )\n", + " if ylabel:\n", + " self.axes[self.index].set_ylabel(ylabel)\n", + " if title:\n", + " self.axes[self.index].set_title(title)\n", + " self.axes[self.index].set_xlabel(\"time\")\n", + " self.axes[self.index].legend()\n", + " self.index += 1\n", + "\n", + " def show(self):\n", + " self.fig.tight_layout()\n", + " self.fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "CuUe-P0xgIuU" + }, + "outputs": [], + "source": [ + "# @markdown We first check TimesFM on some sinusoidals. Notice that\n", + "# @markdown we are calling the endpoints minimally, with only the `input` field.\n", + "# Prepare the context. Notice each of them has a different context length.\n", + "# Note: this is strictly how the query should be structed:\n", + "\n", + "instances = [\n", + " {\"input\": np.sin(np.linspace(0, 20, 100)).tolist()},\n", + " {\"input\": np.sin(np.linspace(0, 40, 500)).tolist()},\n", + " {\n", + " \"input\": (\n", + " np.sin(np.linspace(0, 50, 300)) + np.sin(np.linspace(1, 71, 300)) * 0.5\n", + " ).tolist()\n", + " },\n", + "]\n", + "\n", + "# Query the endpoint.\n", + "results = endpoints[\"timesfm\"].predict(\n", + " instances=instances,\n", + " use_dedicated_endpoint=use_dedicated_endpoint,\n", + ")\n", + "\n", + "viz = Visualizer(nrows=1, ncols=3)\n", + "viz.visualize_forecast(\n", + " instances[0][\"input\"], results[0][0][\"point_forecast\"], title=\"Sinusoidal 1\"\n", + ")\n", + "viz.visualize_forecast(\n", + " instances[1][\"input\"], results[0][1][\"point_forecast\"], title=\"Sinusoidal 2\"\n", + ")\n", + "viz.visualize_forecast(\n", + " instances[2][\"input\"], results[0][2][\"point_forecast\"], title=\"Sinusoidal 3\"\n", + ")\n", + "viz.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "otoyBWG8vIfo" + }, + "source": [ + "### Point forecast\n", + "\n", + "We'll use real world dataset from Kaggle on the [daily temperatures in Delhi, India](https://www.kaggle.com/datasets/sumanthvrao/daily-climate-time-series-data/data). Set the Kaggle credentials following [these instructions](https://github.com/Kaggle/kaggle-api/blob/main/docs/README.md#api-credentials).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "NsezQiCfnCwH" + }, + "outputs": [], + "source": [ + "# @markdown Prepare the dataset\n", + "! pip install kaggle\n", + "\n", + "# Download and prepare the dataset\n", + "! kaggle datasets download sumanthvrao/daily-climate-time-series-data -p /tmp\n", + "! unzip /tmp/daily-climate-time-series-data.zip -d /tmp\n", + "\n", + "output.clear()\n", + "\n", + "data = pd.read_csv(\"/tmp/DailyDelhiClimateTrain.csv\")\n", + "data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "msYISUQtojZo" + }, + "outputs": [], + "source": [ + "# @markdown Query TimesFM and visualize the response\n", + "\n", + "temperature = data.meantemp.to_list()\n", + "dates = data.date.to_list()\n", + "inputs = [temperature[0:200], temperature[300:600], temperature[700:1200]]\n", + "timestamps = [dates[0:200], dates[300:600], dates[700:1200]]\n", + "ground_truths = [\n", + " temperature[200:300],\n", + " temperature[600:700],\n", + " temperature[1200:1300],\n", + "]\n", + "\n", + "response = endpoints[\"timesfm\"].predict(\n", + " instances=[\n", + " {\n", + " \"input\": each_input,\n", + " \"horizon\": 100,\n", + " \"timestamp\": each_timestamp,\n", + " \"timestamp_format\": \"%Y-%m-%d\",\n", + " }\n", + " for each_input, each_timestamp in zip(inputs, timestamps)\n", + " ],\n", + " use_dedicated_endpoint=use_dedicated_endpoint,\n", + ")\n", + "\n", + "viz = Visualizer(nrows=1, ncols=3)\n", + "\n", + "for task_i in range(3):\n", + " viz.visualize_forecast(\n", + " inputs[task_i],\n", + " response[0][task_i][\"point_forecast\"][:100],\n", + " ground_truth=ground_truths[0],\n", + " title=f\"Daily temperature in Delhi, India, Task {task_i+1}\",\n", + " ylabel=\"Temperature (°C)\",\n", + " )\n", + "viz.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gy55NO_OqIKD" + }, + "source": [ + "Notice we are calling by\n", + "```python\n", + "response = endpoint.predict(\n", + " instances=[\n", + " {\n", + " \"input\": each_input,\n", + " \"horizon\": 50,\n", + " \"timestamp\": each_timestamp,\n", + " \"timestamp_format\": \"%Y-%m-%d\",\n", + " }\n", + " for each_input, each_timestamp in zip(inputs, timestamps)\n", + " ]\n", + ")\n", + "```\n", + "\n", + "Here the input fields mean respectively that\n", + "- `input`: the context of the time series\n", + "- `horizon`: an optional integer indicating how long the forecast horizon for this task is. If not given, we'll use the maximum horizon length specified when creating the endpoint.\n", + "- `timestamp`: an optional list of [ISO_8601](https://en.wikipedia.org/wiki/ISO_8601) time strings corresponding to each value in `input`.\n", + " - If provided, TimesFM will infer the `freq` field accordingly.\n", + " - If there are skipped timestamps, TimesFM will linearly interpolate the missing ones before making the forecast.\n", + " - TimesFM will also infer the future timestamps, and apply [strftime](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) with `timestamp_format` to format the return.\n", + "- `freq`: since `timestamp` is given we inferred the frequency as `daily`. Otherwise it would be helpful to provide this `freq` integer direct. Rule of thumb: `0` for everything up till daily, `1` for weekly and monthly, and `2` for quarterly and yearly.\n", + "\n", + "This `response` is structured that:\n", + "* `response[0][i]` is the forecast result of the ith input inside `instances`.\n", + "* `response[0][i]` has the following keys:\n", + " - `timestamp`: the timestamps of the forecast horizon (since we provided `timestamp` during the call).\n", + " - `point_forecast`: the mean point forecast.\n", + " - `mean`: same as `point_forecast`.\n", + " - `p{a}` for `a` in 10, 20, ..., 90: the `a`th percentile of TimesFM forecast.\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Tx8CvOjNvI0m" + }, + "source": [ + "### Anomaly detection\n", + "\n", + "As of checkpoint TimesFM-1.0-200m, TimesFM is capable of outputing quantile forecasts as well. These are uncalibrated forecasts and are experimental. But feel free to experiment and to see what you can do with them.\n", + "\n", + "Here we show how these outputs can potentially serve as anomaly detectors, when we define the anomaly as something beyond a certain range of TimesFM forecasts. In this example we are drawing bands defined by the 30th and the 70th percentiles on the same tasks we did in the last section. Anything outside of the bands could be an \"anomaly\"." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "ab_NZ1hWiWwL" + }, + "outputs": [], + "source": [ + "# @markdown Visualize the response\n", + "\n", + "viz = Visualizer(nrows=1, ncols=3)\n", + "for task_i in range(3):\n", + " viz.visualize_forecast(\n", + " inputs[task_i],\n", + " response[0][task_i][\"point_forecast\"],\n", + " ground_truth=ground_truths[0],\n", + " horizon_lower=response[0][task_i][\"p30\"],\n", + " horizon_upper=response[0][task_i][\"p70\"],\n", + " title=f\"Daily temperature in Delhi, India, Task {task_i+1}\",\n", + " ylabel=\"Temperature (°C)\",\n", + " )\n", + "viz.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YAV6mucyS0FV" + }, + "source": [ + "### Covariate support\n", + "\n", + "TimesFM can incorporate static and dynamic covariates to better the forecast. The native function is demonstrated in this [notebook on GitHub](https://github.com/google-research/timesfm/blob/master/notebooks/covariates.ipynb), which also has a few examples introducing covariates, their definitions and how they can be useful.\n", + "\n", + "For the Model Garden enpoint we've created awrapper to request the forecast with covaraties directly.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "5Gn6cTpNaosY" + }, + "outputs": [], + "source": [ + "# @markdown Create synthetic data, call TimesFM with covariates, and visualize\n", + "# @markdown the outputs.\n", + "# @markdown Notice the forecasts with covariates capture the nuanced spikes.\n", + "\n", + "start = datetime.datetime.fromisoformat(\"2024-01-01 11:30:20\")\n", + "dummy_timestamps = [start + datetime.timedelta(minutes=10) * k for k in range(1000)]\n", + "dummy_sin = np.sin(np.linspace(0.0, 300.0, 1000))\n", + "dummy_dynamic = np.random.uniform(size=1000)\n", + "dummy_dynamic_cat = np.random.binomial(n=1, p=0.3, size=1000)\n", + "dummy_mix = dummy_sin + dummy_dynamic * 0.5 + dummy_dynamic_cat * 0.5\n", + "dummy_mix_yes = dummy_mix + 0.5\n", + "dummy_mix_no = dummy_mix - 0.5\n", + "\n", + "cov_instances = [\n", + " {\n", + " \"input\": dummy_mix_yes[:200].tolist(),\n", + " \"dynamic_numerical_covariates\": {\n", + " \"temperature_forecast\": dummy_dynamic[:240].tolist(),\n", + " },\n", + " \"dynamic_categorical_covariates\": {\n", + " \"will_it_rain\": dummy_dynamic_cat[:240].tolist(),\n", + " },\n", + " \"static_numerical_covariates\": {\"not_useful_cov1\": 0.2},\n", + " \"static_categorical_covariates\": {\"take_vitamin_c\": \"yes\"},\n", + " \"timestamp\": [k.strftime(\"%Y-%m-%d %H:%M:%S\") for k in dummy_timestamps[:200]],\n", + " },\n", + " {\n", + " \"input\": dummy_mix_yes[300:400].tolist(),\n", + " \"dynamic_numerical_covariates\": {\n", + " \"temperature_forecast\": dummy_dynamic[300:440].tolist()\n", + " },\n", + " \"dynamic_categorical_covariates\": {\n", + " \"will_it_rain\": dummy_dynamic_cat[300:440].tolist()\n", + " },\n", + " \"static_numerical_covariates\": {\"not_useful_cov1\": 0.1},\n", + " \"static_categorical_covariates\": {\"take_vitamin_c\": \"yes\"},\n", + " \"timestamp\": [\n", + " k.strftime(\"%Y-%m-%d %H:%M:%S\") for k in dummy_timestamps[300:400]\n", + " ],\n", + " },\n", + " {\n", + " \"input\": dummy_mix_no[500:800].tolist(),\n", + " \"dynamic_numerical_covariates\": {\n", + " \"temperature_forecast\": dummy_dynamic[500:820].tolist()\n", + " },\n", + " \"dynamic_categorical_covariates\": {\n", + " \"will_it_rain\": dummy_dynamic_cat[500:820].tolist()\n", + " },\n", + " \"static_numerical_covariates\": {\"not_useful_cov1\": 0.3},\n", + " \"static_categorical_covariates\": {\"take_vitamin_c\": \"no\"},\n", + " \"timestamp\": [\n", + " k.strftime(\"%Y-%m-%d %H:%M:%S\") for k in dummy_timestamps[500:800]\n", + " ],\n", + " },\n", + "]\n", + "\n", + "response = endpoints[\"timesfm\"].predict(\n", + " instances=cov_instances,\n", + " use_dedicated_endpoint=use_dedicated_endpoint,\n", + ")\n", + "\n", + "no_cov_instances = [{\"input\": task[\"input\"], \"horizon\": 40} for task in cov_instances]\n", + "no_cov_response = endpoints[\"timesfm\"].predict(\n", + " instances=no_cov_instances,\n", + " use_dedicated_endpoint=use_dedicated_endpoint,\n", + ")\n", + "\n", + "viz = Visualizer(nrows=3, ncols=2)\n", + "for task_i, (per_input, per_gt) in enumerate(\n", + " [\n", + " (dummy_mix_yes[:200], dummy_mix_yes[200:240]),\n", + " (dummy_mix_yes[300:400], dummy_mix_yes[400:440]),\n", + " (dummy_mix_no[500:800], dummy_mix_no[800:820]),\n", + " ]\n", + "):\n", + " viz.visualize_forecast(\n", + " per_input.tolist(),\n", + " response[0][task_i][\"point_forecast\"],\n", + " ground_truth=per_gt.tolist(),\n", + " title=f\"Task {task_i} WITH covariates\",\n", + " )\n", + " viz.visualize_forecast(\n", + " per_input.tolist(),\n", + " no_cov_response[0][task_i][\"point_forecast\"],\n", + " ground_truth=per_gt.tolist(),\n", + " title=f\"Task {task_i} WITHOUT covariates\",\n", + " )\n", + "viz.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ArvNqeA9dSJ5" + }, + "source": [ + "In this example, for each task the signature is\n", + "```python\n", + " {\n", + " \"input\": dummy_mix_yes[:200].tolist(),\n", + " \"dynamic_numerical_covariates\": {\n", + " \"temperature_forecast\": dummy_dynamic[:240].tolist(),\n", + " },\n", + " \"dynamic_categorical_covariates\": {\n", + " \"will_it_rain\": dummy_dynamic_cat[:240].tolist(),\n", + " },\n", + " \"static_numerical_covariates\": {\"not_useful_cov1\": 0.2},\n", + " \"static_categorical_covariates\": {\"take_vitamin_c\": \"yes\"},\n", + " \"timestamp\": [\n", + " k.strftime(\"%Y-%m-%d %H:%M:%S\") for k in dummy_timestamps[:200]\n", + " ],\n", + "}\n", + "```\n", + "\n", + "**Notice**:\n", + "\n", + "1. We prefer multiple tasks in one `predict` call. As our covariate support needs batched inputs, it helps to have more tasks with the same set of covariates for more accurate forecast.\n", + "\n", + "2. We make it mandatory that the dynamic covariates need to cover both the forecast context and horizon. For example, if you have a 7-day history and want to forecast 7 days in the future, all dynamic covariates should have 14 values covering both time periods.\n", + "\n", + "3. You can add more covariates into the correspoinding dictionary if needed - just make sure that all tasks in the same request share the same set of covariates.\n", + "\n", + "4. The `horizon` is not provided, since TimesFM can infer the correct `horizon` based on the length of the horizon part of the dynamic covariates. If only static covariates are available it is still worth considering adding back the `horizon`." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0jcS6Tb0pg1x" + }, + "source": [ + "## Clean up resources" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "5P_TAFcmlmdV" + }, + "outputs": [], + "source": [ + "# @title Delete the models and endpoints\n", + "# @markdown Delete the experiment models and endpoints to recycle the resources\n", + "# @markdown and avoid unnecessary continuous charges that may incur.\n", + "\n", + "# Undeploy model and delete endpoint.\n", + "for endpoint in endpoints.values():\n", + " endpoint.delete(force=True)\n", + "\n", + "# Delete models.\n", + "for model in models.values():\n", + " model.delete()\n", + "\n", + "delete_bucket = False # @param {type:\"boolean\"}\n", + "if delete_bucket:\n", + " ! gsutil -m rm -r $BUCKET_NAME" + ] + } + ], + "metadata": { + "colab": { + "name": "model_garden_timesfm_2_5_deployment_on_vertex.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +}