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向き、最大の自由度 │ └────────────────────┴────────────┴──────────────────────┴────────────────┴────────────────┴───────────────────────────────────────────────────┘