GPT 5.3 Codex
## 概要
既存の Grafana + VictoriaMetrics 環境に、学習済み [[2025__NeurIPS__This Time is Different - An Observability Perspective on Time Series Foundation Models|Toto]] を「独立推論サービス」として追加し、5分ごとに予測を実行して予測時系列を VictoriaMetrics へ
Remote Write で書き戻す。
初期リリースは以下を標準とする。
- 推論方式: バッチ予測
- 配置: 独立推論サービス
- 出力: p50 と p10/p90
- 対象メトリクス: 自動発見
- 運用粒度: 5分実行、1時間先予測、14日保持
- 計算資源: GPU 利用可能
- 書き込み方式: Prometheus Remote Write
## 目的と成功基準
- 目的: 既存監視に「将来予測」と「予測区間」を重ね、異常の早期検知を可能にする。
- 成功基準:
- 5分周期のバッチが継続稼働し、欠損なく予測系列が保存される。
- Grafana で実測値と p50/p10/p90 が同一パネルで可視化できる。
- 予測区間逸脱を条件にアラート可能(誤検知率の初期評価含む)。
- 推論ジョブ失敗時の再試行・遅延検知・死活監視がある。
## 採用アーキテクチャ
- コンポーネント:
- toto-inference-scheduler: 5分周期のジョブ起動(Kubernetes CronJob もしくは常駐ワーカー内スケジューラ)。
- toto-inference-worker: GPU で推論実行。メトリクス取得、前処理、推論、後処理、書き込みを実施。
- metric-discovery: VictoriaMetrics から対象時系列を自動発見し、フィルタして実行対象を決定。
- remote-write-client: 予測値を Prometheus Remote Write 形式で送信。
- state-store(軽量): 最終成功時刻、対象系列キャッシュ、ジョブ統計を保持(Redis か sqlite/ファイル)。
- 非採用:
- Grafana 同居、VM 同居は障害分離・スケール性の観点で初期段階では不採用。
- オンライン推論 API は運用複雑度が高いため初期段階で不採用。
## データフロー
1. スケジューラが5分ごとに実行。
2. metric-discovery が対象候補を自動発見。
3. フィルタで対象確定(低頻度系列、欠損過多、履歴不足を除外)。
4. 対象系列の過去コンテキストを VictoriaMetrics から取得。
5. Toto 入力形式へ変換し、1時間先(12ステップ想定)を推論。
6. p50/p10/p90 を生成し、タイムスタンプを未来時刻に整列。
7. Remote Write で VictoriaMetrics に保存。
8. ジョブ結果メトリクス(件数、遅延、失敗数)を監視用メトリクスとして出力。
## 公開 API / インターフェース設計
- 推論サービス設定(例: config.yaml):
- discovery.selector: 自動発見のラベルセレクタ。
- discovery.max_series: 1ジョブ当たり上限系列数。
- inference.context_length: コンテキスト長。
- inference.prediction_horizon: 1時間。
- inference.quantiles: [0.1, 0.5, 0.9]。
- runtime.batch_size, runtime.device=gpu。
- writer.remote_write.url, writer.remote_write.auth。
- VictoriaMetrics に保存する系列命名:
- toto_forecast_p50
- toto_forecast_p10
- toto_forecast_p90
- 付与ラベル:
- source_metric, job, instance, service, env, model=toto, horizon, quantile
- 互換方針:
- 既存実測メトリクスは変更しない。
- 予測系列は別 metric 名で追加のみ。
## 自動発見ルール(初期版)
- 対象候補:
- 明示除外ラベルを除く全時系列を候補化。
- 除外:
- ヒストリ長不足(例: context_length 未満)。
- 欠損率しきい値超過。
- 値が常時ゼロ/定数で情報量が低い系列。
- cardinality 爆発ラベル(例: pod UID 等)を持つ系列。
- 負荷制御:
- 1ジョブ上限系列数を設け、超過時は優先度順で分割処理。
## エラー処理・運用設計
- 失敗時:
- 指数バックオフで再試行(短時間)。
- ジョブ単位で部分成功を許容(系列単位で失敗隔離)。
- タイムアウト:
- 取得、推論、書き込みそれぞれに個別 timeout 設定。
- 冪等性:
- 同一 series + timestamp + quantile の重複書き込みを許容(上書き前提)。
- 可観測性:
- toto_job_duration_seconds
- toto_series_processed_total
- toto_series_failed_total
- toto_remote_write_errors_total
- toto_forecast_lag_seconds
## Grafana 側設計
- ダッシュボード:
- 実測値 + p50 + p10/p90 バンド表示。
- モデル健全性パネル(成功率、遅延、失敗数)。
- アラート:
- 実測値が p10-p90 を連続 N 点逸脱で firing。
- 推論ジョブ遅延・停止アラート。
- 運用ルール:
- 初期は通知レベルを warning から開始し、誤検知率確認後に段階的に厳格化。
## テスト計画と受け入れ条件
- 単体テスト:
- 前処理(欠損補完、時刻整列、ラベル生成)。
- 推論ラッパ(入力 shape、出力 quantile 形状)。
- Remote Write ペイロード生成。
- 結合テスト:
- テスト用 VM へ書き込み、Grafana クエリで値取得確認。
- discovery → inference → write のE2E。
- 負荷テスト:
- 想定系列数で5分枠内完了を確認。
- GPU メモリ上限・スループット測定。
- 受け入れ基準:
- 24時間連続運転でジョブ成功率 99% 以上。
- 1サイクル処理時間が5分未満(p95)。
- 予測系列欠損率 1% 未満。
## ロールアウト計画
- Phase 1: Shadow 書き込み(可視化のみ、アラート無効)。
- Phase 2: 一部サービスで warning アラート有効化。
- Phase 3: 全体展開、しきい値最適化、運用Runbook確定。
- Rollback:
- スケジューラ停止で即時停止可能。
- 既存監視系は非侵襲のため影響最小。
## 実装タスク分解
- discovery モジュール実装(自動発見 + フィルタ)。
- fetcher 実装(VM から履歴取得)。
- toto-runner 実装(GPU 推論バッチ)。
- writer 実装(Remote Write)。
- scheduler と state-store 実装。
- Grafana ダッシュボード/アラートテンプレート作成。
- Runbook(障害対応、容量、しきい値調整)作成。
## 前提・仮定(明示)
- VictoriaMetrics は Prometheus Remote Write 受信を許可済み。
- GPU リソースは本番で利用可能。
- 予測対象は自動発見を採用し、初期除外ルールで過負荷を防ぐ。
- 予測保持期間は14日、履歴保存は既存 VM retention に従う。
- Toto モデル重みは既存の学習済みチェックポイントを利用し、追加学習は初期スコープ外。
---
Claude Code Opus 4.6
# Toto モデル サービング & Grafana/VictoriaMetrics 統合設計
## Context
Toto (Time-Series Optimized Transformer for Observability) は DataDog が開発した 151M パラメータの時系列予測基盤モデル。現在はリサーチ・評価用のPython APIのみで、プロダクション向けサービング基盤が存在しない。既にデプロイ済みの Grafana + VictoriaMetrics (クラスタモード) 環境に、**メトリクス予測**と**異常検知**の両機能を組み込む。
**サービングフレームワーク**: NVIDIA Triton Inference Server (Python Backend)
- vLLM は不適合(Totoはテキストトークンではなく時系列パッチを扱い、出力がStudent-T連続分布であるため、vLLMのサンプリングパイプラインと根本的に非互換)
- Triton の Python Backend で TotoForecaster を直接ラップし、動的バッチング・Prometheusメトリクス・モデルバージョニングを活用
---
## アーキテクチャ概要
```
┌─────────────────────┐ ┌──────────────────────┐
│ Grafana Dashboards │ │ Grafana Alerting │
│ (予測可視化/異常) │ │ (閾値ベースアラート) │
└──────────┬──────────┘ └──────────┬───────────┘
│ MetricsQL │
▼ │
┌──────────────────────┐ │
│ vmselect │ │
│ (読み取り) │ │
└──────────┬───────────┘ │
│ │
┌──────────┴───────────┐ ┌──────────┴───────────┐
│ vmstorage │◄───│ vminsert │
│ (保存) │ │ (書き込み) │
└──────────────────────┘ └──────────┬───────────┘
│
┌────────────────────────────┘
│
┌─────────┴─────────────────────────────────────────────────────┐
│ TOTO SYSTEM │
│ │
│ ┌──────────────────────────────────────┐ ┌───────────────┐ │
│ │ Triton Inference Server (GPU) │ │ Orchestrator │ │
│ │ │ │ (Python) │ │
│ │ ┌────────────────────────────────┐ │ │ │ │
│ │ │ toto_forecast (Python Backend) │ │ │ - 定期予測 │ │
│ │ │ - TotoForecaster │ │ │ - 異常検知 │ │
│ │ │ - TensorConverter │ │ │ - VM読み書き │ │
│ │ │ - torch.compile + KV Cache │ │ │ - APScheduler │ │
│ │ └────────────────────────────────┘ │ │ │ │
│ │ ┌────────────────────────────────┐ │ └───────┬───────┘ │
│ │ │ toto_anomaly (Python Backend) │ │ │ │
│ │ │ - AnomalyDetector │ │ Triton │ │
│ │ │ - Quantile/Z-score scoring │ │ gRPC │ │
│ │ └────────────────────────────────┘ │◄─────────┘ │
│ │ │ │
│ │ HTTP :8000 (inference) │ │
│ │ HTTP :8002 (metrics/prometheus) │ │
│ └──────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
```
**2プロセス構成**:
1. **Triton Inference Server** — GPU上でモデル推論を担当。gRPC/HTTPでリクエスト受付
2. **Orchestrator** — VM読み書き、定期スケジューリング、異常検知ロジック。Triton に gRPC で推論を委譲
---
## ディレクトリ構成
```
serving/
├── triton_models/ # Triton モデルリポジトリ
│ ├── toto_forecast/
│ │ ├── config.pbtxt # モデル設定 (入出力テンソル定義)
│ │ └── 1/
│ │ └── model.py # Python Backend: TotoForecaster ラッパー
│ └── toto_anomaly/
│ ├── config.pbtxt
│ └── 1/
│ └── model.py # Python Backend: 異常スコアリング
│
├── orchestrator/
│ ├── __init__.py
│ ├── main.py # Orchestrator エントリポイント
│ ├── config.py # Pydantic 設定ローダー
│ ├── triton_client.py # Triton gRPC クライアント
│ ├── vm_reader.py # vmselect メトリクス読み取り (httpx async)
│ ├── vm_writer.py # vminsert 予測結果書き込み
│ ├── tensor_converter.py # VM結果 → NumPy配列変換
│ ├── anomaly_detector.py # 異常スコアリング (quantile/z-score)
│ └── scheduler.py # APScheduler ベースの定期実行
│
├── grafana/
│ └── dashboards/
│ ├── toto_forecast.json
│ ├── toto_anomaly.json
│ └── toto_system.json
│
├── deploy/
│ ├── triton-server.service # systemd: Triton サーバー
│ ├── toto-orchestrator.service # systemd: Orchestrator
│ ├── triton-server.env
│ └── toto-orchestrator.env
│
├── config/
│ ├── default.yaml
│ └── example.yaml
│
└── tests/
├── test_tensor_converter.py
├── test_anomaly_detector.py
└── test_triton_client.py
```
---
## コンポーネント詳細設計
### 1. Triton Python Backend: toto_forecast (`serving/triton_models/toto_forecast/`)
#### config.pbtxt
```protobuf
name: "toto_forecast"
backend: "python"
max_batch_size: 0
input [
{
name: "SERIES"
data_type: TYPE_FP32
dims: [ -1, -1 ] # [variates, time_steps]
},
{
name: "PADDING_MASK"
data_type: TYPE_BOOL
dims: [ -1, -1 ] # [variates, time_steps]
},
{
name: "ID_MASK"
data_type: TYPE_INT32
dims: [ -1, -1 ] # [variates, time_steps]
},
{
name: "TIME_INTERVAL_SECONDS"
data_type: TYPE_INT64
dims: [ -1 ] # [variates]
},
{
name: "PREDICTION_LENGTH"
data_type: TYPE_INT32
dims: [ 1 ]
},
{
name: "NUM_SAMPLES"
data_type: TYPE_INT32
dims: [ 1 ]
}
]
output [
{
name: "FORECAST_MEAN"
data_type: TYPE_FP32
dims: [ -1, -1 ] # [variates, prediction_length]
},
{
name: "FORECAST_SAMPLES"
data_type: TYPE_FP32
dims: [ -1, -1, -1 ] # [variates, prediction_length, num_samples]
}
]
instance_group [
{
count: 1
kind: KIND_GPU
gpus: [ 0 ]
}
]
dynamic_batching {
max_queue_delay_microseconds: 50000
}
parameters: {
key: "CHECKPOINT"
value: { string_value: "Datadog/Toto-Open-Base-1.0" }
}
parameters: {
key: "USE_COMPILE"
value: { string_value: "true" }
}
parameters: {
key: "USE_KV_CACHE"
value: { string_value: "true" }
}
```
#### model.py (Python Backend)
```python
import triton_python_backend_utils as pb_utils
import torch
import numpy as np
import json
import logging
from toto.model.toto import Toto
from toto.inference.forecaster import TotoForecaster
from toto.data.util.dataset import MaskedTimeseries
logger = logging.getLogger(__name__)
class TritonPythonModel:
"""Triton Python Backend for Toto forecasting."""
def initialize(self, args):
"""モデルロード・コンパイル・ウォームアップ"""
self.model_config = json.loads(args["model_config"])
params = {k: v["string_value"] for k, v in self.model_config["parameters"].items()}
checkpoint = params.get("CHECKPOINT", "Datadog/Toto-Open-Base-1.0")
use_compile = params.get("USE_COMPILE", "true").lower() == "true"
self.use_kv_cache = params.get("USE_KV_CACHE", "true").lower() == "true"
device_id = args.get("model_instance_device_id", "0")
self.device = torch.device(f"cuda:{device_id}")
logger.info(f"Loading Toto from {checkpoint} on {self.device}")
toto = Toto.from_pretrained(checkpoint)
toto.to(self.device)
toto.eval()
if use_compile:
logger.info("Compiling model with torch.compile()")
toto.compile()
self.forecaster = TotoForecaster(toto.model)
self._warmup()
logger.info("Toto model ready")
def _warmup(self):
"""JITグラフ事前コンパイル用ダミー推論"""
n_var, ctx_len = 4, 512
dummy = MaskedTimeseries(
series=torch.randn(n_var, ctx_len, device=self.device),
padding_mask=torch.ones(n_var, ctx_len, dtype=torch.bool, device=self.device),
id_mask=torch.zeros(n_var, ctx_len, dtype=torch.int32, device=self.device),
timestamp_seconds=torch.zeros(n_var, ctx_len, dtype=torch.long, device=self.device),
time_interval_seconds=torch.full((n_var,), 60, dtype=torch.long, device=self.device),
)
for _ in range(2):
self.forecaster.forecast(dummy, prediction_length=96, num_samples=16,
samples_per_batch=16, use_kv_cache=self.use_kv_cache)
def execute(self, requests):
"""推論実行 (Tritonバッチ内の各リクエストを処理)"""
responses = []
for request in requests:
try:
series = pb_utils.get_input_tensor_by_name(request, "SERIES").as_numpy()
padding_mask = pb_utils.get_input_tensor_by_name(request, "PADDING_MASK").as_numpy()
id_mask = pb_utils.get_input_tensor_by_name(request, "ID_MASK").as_numpy()
time_interval = pb_utils.get_input_tensor_by_name(request, "TIME_INTERVAL_SECONDS").as_numpy()
pred_len = pb_utils.get_input_tensor_by_name(request, "PREDICTION_LENGTH").as_numpy()[0]
num_samples = pb_utils.get_input_tensor_by_name(request, "NUM_SAMPLES").as_numpy()[0]
inputs = MaskedTimeseries(
series=torch.from_numpy(series).float().to(self.device),
padding_mask=torch.from_numpy(padding_mask).bool().to(self.device),
id_mask=torch.from_numpy(id_mask).int().to(self.device),
timestamp_seconds=torch.zeros_like(
torch.from_numpy(series), dtype=torch.long, device=self.device),
time_interval_seconds=torch.from_numpy(time_interval).long().to(self.device),
)
forecast = self.forecaster.forecast(
inputs,
prediction_length=int(pred_len),
num_samples=int(num_samples),
samples_per_batch=min(int(num_samples), 256),
use_kv_cache=self.use_kv_cache,
)
mean_np = forecast.mean[0].cpu().numpy() # [variates, pred_len]
samples_np = forecast.samples[0].cpu().numpy() # [variates, pred_len, num_samples]
out_mean = pb_utils.Tensor("FORECAST_MEAN", mean_np.astype(np.float32))
out_samples = pb_utils.Tensor("FORECAST_SAMPLES", samples_np.astype(np.float32))
responses.append(pb_utils.InferenceResponse(output_tensors=[out_mean, out_samples]))
except Exception as e:
logger.exception("Inference failed")
responses.append(pb_utils.InferenceResponse(
output_tensors=[], error=pb_utils.TritonError(str(e))))
return responses
def finalize(self):
logger.info("Toto model unloaded")
```
**参照ファイル**:
- `toto/model/toto.py` — `Toto.from_pretrained()`, `toto.compile()`
- `toto/inference/forecaster.py` — `TotoForecaster.forecast()`, `Forecast` dataclass
- `toto/data/util/dataset.py` — `MaskedTimeseries` NamedTuple
### 2. Orchestrator (`serving/orchestrator/`)
Triton とは別プロセスで動作。VM読み書き・スケジューリング・異常検知を担当。
#### Triton Client (`orchestrator/triton_client.py`)
```python
import tritonclient.grpc.aio as grpcclient
import numpy as np
class TotoTritonClient:
"""Triton gRPC クライアント — 推論リクエストを送信"""
def __init__(self, triton_url: str = "localhost:8001"):
self.client = grpcclient.InferenceServerClient(url=triton_url)
async def forecast(self, series: np.ndarray, padding_mask: np.ndarray,
id_mask: np.ndarray, time_interval: np.ndarray,
prediction_length: int, num_samples: int):
"""Triton に予測リクエストを送信し結果を返す"""
inputs = [
grpcclient.InferInput("SERIES", series.shape, "FP32"),
grpcclient.InferInput("PADDING_MASK", padding_mask.shape, "BOOL"),
grpcclient.InferInput("ID_MASK", id_mask.shape, "INT32"),
grpcclient.InferInput("TIME_INTERVAL_SECONDS", time_interval.shape, "INT64"),
grpcclient.InferInput("PREDICTION_LENGTH", [1], "INT32"),
grpcclient.InferInput("NUM_SAMPLES", [1], "INT32"),
]
inputs[0].set_data_from_numpy(series.astype(np.float32))
inputs[1].set_data_from_numpy(padding_mask)
inputs[2].set_data_from_numpy(id_mask.astype(np.int32))
inputs[3].set_data_from_numpy(time_interval.astype(np.int64))
inputs[4].set_data_from_numpy(np.array([prediction_length], dtype=np.int32))
inputs[5].set_data_from_numpy(np.array([num_samples], dtype=np.int32))
outputs = [
grpcclient.InferRequestedOutput("FORECAST_MEAN"),
grpcclient.InferRequestedOutput("FORECAST_SAMPLES"),
]
result = await self.client.infer("toto_forecast", inputs, outputs=outputs)
return {
"mean": result.as_numpy("FORECAST_MEAN"),
"samples": result.as_numpy("FORECAST_SAMPLES"),
}
```
#### Tensor Converter (`orchestrator/tensor_converter.py`)
**責務**: VictoriaMetrics クエリ結果 → NumPy 配列 (Triton入力用)
VM の `/api/v1/query_range` レスポンス:
```json
{"metric": {"__name__": "cpu_usage", "instance": "host1"}, "values": [[ts, val], ...]}
```
変換ロジック:
- 各メトリクス系列を variate 軸に積み上げ → `[variates, time_steps]`
- 右寄せアライメント(最新データを右端、左側を0パディング)
- NaN → 0 に置換、`padding_mask=False` でマスク
- `id_mask`: 同一グループ = 同じ値、独立グループ = 異なる値(space-wise attention 分離)
#### VM Reader / Writer (`orchestrator/vm_reader.py`, `orchestrator/vm_writer.py`)
FastAPI版と同じ設計。httpx AsyncClient で vmselect/vminsert と通信:
- **Reader**: `http://vmselect:8481/select/{tenant_id}/prometheus/api/v1/query_range`
- **Writer**: `http://vminsert:8480/insert/{tenant_id}/prometheus/api/v1/import/prometheus` (Prometheus テキスト形式)
#### Scheduler (`orchestrator/scheduler.py`)
APScheduler (AsyncIO) で定期実行:
```
[定期トリガー]
→ VMReader(vmselect から過去データ取得)
→ TensorConverter(NumPy配列化)
→ TotoTritonClient.forecast(gRPC → Triton)
→ VMWriter(予測結果を vminsert へ書き込み)
→ AnomalyDetector(実測 vs 予測 比較)
→ VMWriter(異常スコアを vminsert へ書き込み)
```
- `max_instances=1` でジョブ重複防止
- `misfire_grace_time=60` で一時的遅延への耐性
### 3. 異常検知 (`orchestrator/anomaly_detector.py`)
**2つのスコアリング手法**:
| 手法 | 計算方法 | 推奨ケース |
|---|---|---|
| **Quantile** (推奨) | 予測分布の q0.01〜q0.99 区間外の距離/区間幅 | Totoの Student-T 混合分布に適合 |
| **Z-score** | \|actual - mean\| / std | シンプルさ重視の場合 |
- 入力: `samples` テンソル (Tritonから返却) + 実測値 (VMから取得)
- `alert_consecutive_anomalies: 3` — 連続N回異常でアラート発火
- sensitivity: low (q0.001/q0.999), medium (q0.01/q0.99), high (q0.05/q0.95)
### 4. メトリクス命名規則
VictoriaMetrics に書き込むメトリクス:
| メトリクス名 | 説明 | ラベル |
|---|---|---|
| `toto_forecast_median` | 中央値予測 | `original_metric`, 元のラベル全て, `model` |
| `toto_forecast_mean` | 平均値予測 | 同上 |
| `toto_forecast_std` | 標準偏差 | 同上 |
| `toto_forecast_q010` | 10%ile予測 | 同上 |
| `toto_forecast_q090` | 90%ile予測 | 同上 |
| `toto_anomaly_score` | 異常スコア (0=正常) | `original_metric`, 元のラベル |
| `toto_anomaly_detected` | 異常フラグ (0/1) | 同上 |
### 5. Grafana ダッシュボード
**予測ダッシュボード** — MetricsQL クエリ例:
```promql
# 実測値
node_cpu_seconds_total{instance="host1:9100"}
# 予測中央値 (破線オーバーレイ)
toto_forecast_median{original_metric="node_cpu_seconds_total", instance="host1:9100"}
# 信頼区間 (シェーディング: fill between)
toto_forecast_q010{original_metric="node_cpu_seconds_total", instance="host1:9100"}
toto_forecast_q090{original_metric="node_cpu_seconds_total", instance="host1:9100"}
```
**異常検知ダッシュボード**:
```promql
toto_anomaly_score{original_metric="node_cpu_seconds_total"}
count_over_time(toto_anomaly_detected{original_metric="node_cpu_seconds_total"} == 1 [1h])
```
**Grafana アラートルール**: `toto_anomaly_detected == 1` が `alert_consecutive_anomalies` 回連続で発火
**システムヘルス** (Triton 内蔵メトリクス + カスタム):
```promql
# Triton 推論レイテンシ
nv_inference_request_duration_us{model="toto_forecast"}
# Triton スループット
rate(nv_inference_request_success{model="toto_forecast"}[5m])
# Triton キュー待ち時間
nv_inference_queue_duration_us{model="toto_forecast"}
# GPU使用率 (Triton内蔵)
nv_gpu_utilization
# GPU メモリ
nv_gpu_memory_used_bytes
```
### 6. セルフモニタリング
**Triton 内蔵メトリクス** (ポート 8002 `/metrics`):
- `nv_inference_request_success` / `nv_inference_request_failure` — 推論成功/失敗数
- `nv_inference_request_duration_us` — 推論レイテンシ
- `nv_inference_queue_duration_us` — キュー待ち時間
- `nv_gpu_utilization` — GPU使用率
- `nv_gpu_memory_used_bytes` — GPUメモリ使用量
**Orchestrator カスタムメトリクス** (prometheus_client):
- `toto_scheduler_job_duration_seconds` — ジョブ実行時間
- `toto_scheduler_job_errors_total` — ジョブエラー数
- `toto_vm_query_latency_ms` — VM読み取りレイテンシ
- `toto_vm_write_latency_ms` — VM書き込みレイテンシ
---
## デプロイ構成
### systemd サービス
**Triton Server** (`deploy/triton-server.service`):
```ini
[Unit]
Description=NVIDIA Triton Inference Server (Toto)
After=network.target
[Service]
Type=simple
User=toto
EnvironmentFile=/opt/toto/serving/deploy/triton-server.env
ExecStart=/opt/tritonserver/bin/tritonserver \
--model-repository=/opt/toto/serving/triton_models \
--http-port=8000 \
--grpc-port=8001 \
--metrics-port=8002 \
--log-verbose=0
Restart=on-failure
RestartSec=10
SupplementaryGroups=video render
LimitMEMLOCK=infinity
[Install]
WantedBy=multi-user.target
```
**Orchestrator** (`deploy/toto-orchestrator.service`):
```ini
[Unit]
Description=Toto Orchestrator (Scheduler + VM Pipeline)
After=triton-server.service
Requires=triton-server.service
[Service]
Type=simple
User=toto
EnvironmentFile=/opt/toto/serving/deploy/toto-orchestrator.env
ExecStart=/opt/toto/.venv/bin/python -m orchestrator.main \
--config /opt/toto/serving/config/production.yaml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
```
### 環境変数
**triton-server.env**:
```bash
CUDA_VISIBLE_DEVICES=0
CUBLAS_WORKSPACE_CONFIG=:4096:8
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
HF_HOME=/opt/toto/.cache/huggingface
LD_LIBRARY_PATH=/opt/tritonserver/lib
```
**toto-orchestrator.env**:
```bash
TRITON_URL=localhost:8001
TOTO_CONFIG=/opt/toto/serving/config/production.yaml
```
### GPU 割り当て戦略
- 151M パラメータ → 約600MB〜1GB GPU メモリ
- Triton が GPU を管理(`instance_group` で GPU 指定)
- 複数GPU: `instance_group` で GPU 0/1 にインスタンスを分散配置可能
---
## YAML 設定ファイル (`serving/config/default.yaml`)
```yaml
triton:
url: "localhost:8001" # gRPC
http_url: "localhost:8000" # HTTP (health check用)
inference:
default_context_length: 2048
max_context_length: 4096
default_prediction_length: 336
default_num_samples: 256
victoria_metrics:
vmselect_url: "http://vmselect:8481"
vminsert_url: "http://vminsert:8480"
tenant_id: "0:0"
query_timeout_seconds: 30
write_batch_size: 1000
scheduler:
enabled: true
jobs:
- name: "infrastructure_forecast"
metrics_selector: '{job="node_exporter"}'
metric_names: ["node_cpu_seconds_total", "node_memory_MemAvailable_bytes"]
interval_minutes: 15
context_length: 2048
prediction_length: 336
time_interval_seconds: 900
num_samples: 256
anomaly:
enabled: true
check_interval_minutes: 5
scoring_method: "quantile"
default_lower_quantile: 0.01
default_upper_quantile: 0.99
alert_consecutive_anomalies: 3
orchestrator_metrics:
port: 9090
path: "/metrics"
```
---
## 追加依存関係
**Triton Server**: NVIDIA Triton Inference Server (コンテナ or パッケージインストール)
- `nvcr.io/nvidia/tritonserver:24.12-py3` (GPU対応イメージ) or ベアメタルインストール
**Python Backend 依存** (Triton 内):
```
torch==2.7.0
toto-ts==0.1.4
```
**Orchestrator 依存** (`requirements-orchestrator.txt`):
```
tritonclient[grpc]>=2.40.0
httpx>=0.25.0
apscheduler>=3.10.0
pydantic>=2.5.0
pydantic-settings>=2.1.0
prometheus-client>=0.20.0
pyyaml>=6.0
numpy
```
---
## 実装順序
1. **Triton モデルリポジトリ**: `triton_models/toto_forecast/config.pbtxt` + `model.py`
2. **Triton ローカル起動テスト**: `tritonserver --model-repository=...` でモデルロード確認
3. **`orchestrator/tensor_converter.py`** — VM結果→NumPy変換 (ユニットテスト付き)
4. **`orchestrator/triton_client.py`** — gRPC クライアント + 推論テスト
5. **`orchestrator/vm_reader.py`** + **`vm_writer.py`** — VM接続層
6. **`orchestrator/anomaly_detector.py`** — 異常検知ロジック (ユニットテスト付き)
7. **`orchestrator/scheduler.py`** + **`main.py`** — 定期実行オーケストレーション
8. **`deploy/`** — systemd ユニット
9. **`grafana/dashboards/`** — Grafana ダッシュボード JSON
10. **`config/`** — 設定ファイル
---
## 検証方法
1. **Triton モデルロード**: `tritonserver` 起動 → `curl localhost:8000/v2/health/ready` で 200 確認
2. **推論テスト**: `tritonclient` で予測リクエスト送信 → FORECAST_MEAN, FORECAST_SAMPLES の shape 検証
3. **ユニットテスト**: `tensor_converter`, `anomaly_detector` (`pytest serving/tests/`)
4. **E2E フロー**: Orchestrator 起動 → vmselect データ取得 → Triton 推論 → vminsert 書き込み → MetricsQL で確認
5. **Grafana 確認**: ダッシュボード JSON インポート → 実測値と予測値のオーバーレイ表示
6. **異常検知**: 既知の異常パターンで `toto_anomaly_score` が正しく上昇することを確認
7. **Triton メトリクス**: `curl localhost:8002/metrics` → `nv_inference_*` メトリクスが Grafana に表示されること
---
結論: vLLM は Toto のサービングに適していません。
根本的な非互換性が3点あります:
┌──────────┬─────────────────────────────────────────────────┬───────────────────────────────────────┐
│ 問題 │ vLLM の前提 │ Toto の実態 │
├──────────┼─────────────────────────────────────────────────┼───────────────────────────────────────┤
│ 入力形式 │ トークンID (整数) │ float テンソル [variates, time_steps] │
├──────────┼─────────────────────────────────────────────────┼───────────────────────────────────────┤
│ 出力形式 │ 語彙サイズのロジット → カテゴリカルサンプリング │ Student-T 連続分布パラメータ │
├──────────┼─────────────────────────────────────────────────┼───────────────────────────────────────┤
│ 生成単位 │ 1トークン/ステップ │ 1パッチ(複数タイムステップ)/ステップ │
└──────────┴─────────────────────────────────────────────────┴───────────────────────────────────────┘
vLLM のサンプリングパイプライン全体が離散トークンを前提としており、Totoの連続確率分布出力とは根本的に合いません。カスタムモデル登録を使っても、vLLM
のスタックの約80%をバイパスする必要があります。
代替サービングフレームワークの比較
┌────────────────────┬────────────┬──────────────────────┬────────────────┬────────────────┬───────────────────────────────────────────────────┐
│ フレームワーク │ Toto適合度 │ バッチング │ GPU管理 │ モニタリング │ 備考 │
├────────────────────┼────────────┼──────────────────────┼────────────────┼────────────────┼───────────────────────────────────────────────────┤
│ NVIDIA Triton │ ◎ │ 動的バッチング内蔵 │ ネイティブ │ Prometheus内蔵 │ Python Backend で TotoForecaster を直接ラップ可能 │
├────────────────────┼────────────┼──────────────────────┼────────────────┼────────────────┼───────────────────────────────────────────────────┤
│ TorchServe │ ○ │ max_batch_delay 設定 │ ラウンドロビン │ 基本的 │ PyTorch ネイティブ、シンプル │
├────────────────────┼────────────┼──────────────────────┼────────────────┼────────────────┼───────────────────────────────────────────────────┤
│ Ray Serve │ ○ │ カスタム batch_fn │ 部分GPU割当 │ Ray Dashboard │ スケールアウト向き │
├────────────────────┼────────────┼──────────────────────┼────────────────┼────────────────┼───────────────────────────────────────────────────┤
│ FastAPI (現プラン) │ △ │ 手動実装 │ 手動 │ 手動 │ MVP向き、最大の自由度 │
└────────────────────┴────────────┴──────────────────────┴────────────────┴────────────────┴───────────────────────────────────────────────────┘