ドキュメント

ライフサイクル

ライフサイクル

このページは、各エントリポイントにおけるイベントの正確な順序です。図を 1 つだけ読むなら Router のものを読んでください。それが上位集合です。

Agent.predict_batch

predict_batch が唯一の実装で、system_one と predict は 1 つの state でそれを呼びます。

predict_batch(states, questions, batch_size=..., hooks=..., ...)
  │
  ├─ active  = installed hooks + per-call hooks         (installed first)
  ├─ ctx     = PredictContext(states, questions, model=self.model_id, agent=self)
  │
  ├─ try:
  │    │
  │    ├─ on_predict_start ─────────────────────────────┐
  │    │      a hook may:                                │
  │    │        • rewrite ctx.states / ctx.questions     │
  │    │        • set ctx.max_len / ctx.head_max_len     │
  │    │        • ctx.skip(results) ─────────────┐       │
  │    │        • raise (aborts; see errors)     │       │
  │    │                                         │      │
  │    ├─ if ctx.results is not None:  ◄─────────┘      │  cache hit
  │    │      skip tokenization and forward             │
  │    ├─ else:                                         │
  │    │      validate states is a list                 │
  │    │      for each batch chunk:                     │
  │    │        _encode_state ─► collate ─► _forward    │
  │    │      _decode_answers                           │
  │    │      ctx.results = [...]                       │
  │    │                                                │
  │    └─ (any failure here) ──► except BaseException:  │
  │              ctx.error = exc                        │
  │              on_error                               │
  │              re-raise                               │
  │                                                     │
  │   finally:                                          │
  │     ctx.elapsed_ms = now - ctx.started_at           │
  │     if ctx.results: ctx.usage = aggregate_usage(...)│
  │     on_predict_end ─────────────────────────────────┘
  │
  └─ return ctx.results

同じ ctx オブジェクトが start・error・end を通じて流れるので、run_id がそれらを相関させ、end フックは ctx.error を読めます。

Agent.system_one / predict

system_one(state, questions, hooks=..., ...)
  └─ predict_batch([state], questions, hooks=..., ...)[0]

つまり system_one はすべてのフックと同じライフサイクルを継承し、ctx.states == [state] になります。

Router.predict

Router.predict(state, questions, model=..., hooks=..., on_predict_start=..., on_predict_end=...)
  │
  ├─ active = installed hooks + per-call hooks
  │
  ├─ route(state, questions, ..., hooks=per-call, hooks_raise=...)
  │    │
  │    ├─ _route(...)                      detect script / language / workflow
  │    ├─ on_route  ──► ctx.decision       a hook may replace the decision
  │    └─ return ctx.decision
  │
  ├─ load(decision["model"])
  │    │
  │    ├─ already resident? ──► return it
  │    ├─ else build Agent(...) ──► on_load   (after the Router lock is released)
  │    └─ evict LRU checkpoints ──► on_evict  (after the Router lock is released)
  │
  ├─ ctx = PredictContext(states=[state], questions, decision, model=decision.model,
  │                       agent=agent, router=self)
  ├─ try:
  │    ├─ on_predict_start
  │    ├─ if ctx.results is None:
  │    │      result = agent.system_one(ctx.states[0], ctx.questions)
  │    │        └─ the Agent's own hooks run here (start / forward / end)
  │    │      result["routing"] = decision
  │    │      ctx.results = [result]
  │    └─ else:
  │           for each cached result: result.setdefault("routing", decision)
  │    └─ (any failure) ──► except: on_error, re-raise
  │    └─ finally: elapsed_ms, usage, on_predict_end
  │
  └─ return ctx.results[0]

要点:

  • on_route はモデルが読み込まれる前に走るので、フックはチェックポイントを固定し、別のものを読み込まずに済みます。
  • Router レベルの predict フックは呼び出し全体を包みます。それらは Agent へ転送されません。付属した Agent が独自のフックを持てばそれも走りますが、これは想定どおりです。
  • Router レベルの ctx.skip() でも routing は付加されるので、戻り値の形は安定しています。

Router.predict_batch

各結果はそのリクエストに対して predict が返すものなので、Router レベルの predict フックはここでもリクエストごとに走ります。各リクエストが独自の PredictContext、run_id、elapsed_ms を持ちます。

Router.predict_batch(requests, batch_size=...)
  │
  ├─ route_batch(requests) ──► on_route, once per request   (no checkpoint loaded yet)
  │
  └─ for each checkpoint, in order of first appearance:
       │
       ├─ load(checkpoint) ──► on_load / on_evict
       ├─ for each request of this checkpoint, in input order:
       │      ctx = PredictContext(states=[state], questions, decision, model, agent, router,
       │                           max_len=request.get("max_len"),
       │                           head_max_len=request.get("head_max_len"))
       │      on_predict_start       a hook may redact, rewrite, set a token budget or skip
       ├─ group the requests left to infer by (questions, ctx.max_len, ctx.head_max_len)
       │      agent.predict_batch(states, questions, ...)  ──► one shared forward pass per group
       │      result["routing"] = decision;  ctx.results = [result]
       ├─ ctx.usage, once per request
       ├─ (any failure) ──► for every started request, in reverse input order:
       │                    ctx.error = exc, on_error, on_predict_end;  then re-raise
       └─ on_predict_end, once per request of this checkpoint, in reverse input order

要点:

  • あるチェックポイントのリクエストの start フックはすべて、それらの end フックより先に走ります。フォワードパスを共有するからです。したがって on_predict_end で埋めるキャッシュは、同じチェックポイントグループ内の重複 state には応えられません。呼び出しをまたげば応えられます。
  • 同じ理由で、リクエストは開始した順序の逆順で終わります。start で何かを設定し end で戻すフック(contextvars の値、OpenTelemetry の context.attach / detach)は、見つけた値まで巻き戻ります。
  • ctx.states、ctx.questions、トークン予算を置き換える start フックは、自分のリクエストだけを変えます。リクエストは start フックが走った後にフォワードパス用にグループ化されるからです。共有の questions dict をその場で変更するのは別物で、predict がすることでもありません。グループのどのリクエストも、そのすべての start フックが走るまで推論されないので、変更はその dict を共有するすべてのリクエストに届きます。start フックが先に走ったものも、呼び出し元も含みます。代わりに新しい dict を ctx.questions に代入してください。
  • チェックポイント名はグループ化の前に解決されるので、エイリアス("ml")でリクエストを固定する on_route フックはそのチェックポイントのフォワードパスを共有し、ctx.model は解決後の名前になります。
  • リクエストは独自の max_len / head_max_len を持てます。これは predict が呼び出し引数として取るトークン予算のリクエストごとの形です。ctx.max_len を設定する start フックはそれを上書きします。フックはコンテキストが構築された後に走るからです。異なる予算を要求するリクエストはフォワードパスを共有できないので、予算が混在するバッチは予算ごとに 1 回 agent.predict_batch を呼びます。
  • 開始されたリクエストはそれぞれちょうど 1 つの on_predict_end を受け取ります。別のリクエストの end フックが例外を投げてもです。最初のそのようなエラーは、それらすべてが走った後に送出されます。
  • チェックポイントグループが失敗すると、その開始されたすべてのリクエストがその例外で失敗します。それぞれが ctx.error にそれを設定された on_error を受け取り、次に on_predict_end を受け取ります。これにはキャッシュヒットと、質問グループがすでに走ったリクエストも含まれます。呼び出し元はそれらすべてについて結果ではなく例外を受け取り、ctx.error が別のリクエストの失敗であることもありえます(1 つのリクエストで例外を投げる start フックはそのグループを失敗させます)。すでに完了したチェックポイントグループのリクエストは、[router.predict(...) for ...] の先行する呼び出しがそうだったように、結果とともに終わっています。

モデルのライフサイクル

on_load はチェックポイントが構築されたときに発火し、on_evict は解放されたときに発火します。どちらも Router の内部ロックが解放された後に走るので、フックは安全に Router へ呼び戻せます。

load("multilingual")
  │
  ├─ [lock]
  │    build Agent(...)          (seconds: download + weights)
  │    register in _agents / _order
  │    evict LRU if over max_loaded ──► evicted = ["english"]
  ├─ [unlock]
  ├─ on_evict("english")
  └─ on_load("multilingual")

unload("english")
  ├─ [lock] remove from _agents / _order
  ├─ [unlock]
  └─ on_evict("english")

attach(name, agent) は既存のエージェントを登録し、チェックポイントが構築されないので on_load を発火しません。

skip によるキャッシュ

on_predict_start
  ├─ cache hit?  ctx.skip([cached_result])
  │     └─ forward pass skipped
  │     └─ on_predict_end still runs
  │     └─ Router adds `routing` if missing
  └─ cache miss? nothing
        └─ forward pass runs
        └─ on_predict_end can store the result

動くキャッシュの実例は examples/hooks/cache.py にあります。

空の入力

監査がすべての呼び出しを見られるように、フックはそれでも発火します。

入力 終了時の ctx.results
predict_batch([]) []
predict_batch(states, {})(質問なし) state ごとに 1 つの空の答えのペイロード
system_one(state, {}) 単一の空の答えのペイロード

これらの場合はトークン化もフォワードパスも起きませんが、on_predict_start と on_predict_end は走ります。

順序の規則

  1. インストールされたフックは常に、呼び出しごとのフックより先に走ります。
  2. リスト内では、フックはリストの順序で走ります。
  3. 1 つのイベントについて、それを実装するすべてのフックが、その順序で、次のイベントの前に走ります。
  4. on_error は失敗経路で on_predict_end より先に走ります。
  5. 単一の load が追い出しと構築の両方を行うとき、on_evict は on_load より先に走ります。
installed: [A, B]   per-call: [C]
on_predict_start: A, B, C
on_predict_end:   A, B, C

並行性

Agent と Router は多数のスレッドから安全に呼べます。各呼び出しが独自の PredictContext を作るので、コンテキストがリクエストをまたいで漏れることはありません。唯一の共有状態はフックオブジェクト自身なので、スレッドセーフでないフックは自分の状態を保護するか、hooks_concurrent=False でインストールする必要があります。

hooks_concurrent=True (default)      hooks_concurrent=False
  thread 1 ─┐                          thread 1 ─┐
  thread 2 ─┼─ hooks run in parallel   thread 2 ─┼─ one hook at a time
  thread 3 ─┘                          thread 3 ─┘   (RLock)

hooks_concurrent=False が直列化するのは各フックの呼び出しであり、呼び出し全体ではありません。2 つの呼び出しはイベントの合間で依然として交互に進めます。再入可能なロックを使うので、フックはデッドロックせずに同じ Agent/Router へ呼び戻せます。

関連項目