ArkorAlpha

ライフサイクルコールバック

バックエンドからのストリーム中に Arkor が発火する 5 つのコールバック。

コールバックは createTrainer({ callbacks: { ... } }) の下に渡します。バックエンドの SSE イベントストリームから trainer.wait() 内でディスパッチされて動きます。

シグネチャー

interface TrainerCallbacks {
  onStarted:    (ctx: { job: TrainingJob }) => unknown | Promise<unknown>;
  onLog:        (ctx: TrainingLogContext) => unknown | Promise<unknown>;
  onCheckpoint: (ctx: CheckpointContext) => unknown | Promise<unknown>;
  onCompleted:  (ctx: { job: TrainingJob; artifacts: unknown[] }) => unknown | Promise<unknown>;
  onFailed:     (ctx: { job: TrainingJob; error: string }) => unknown | Promise<unknown>;
}

interface 自体は 5 プロパティすべて必須ですが、createTrainercallbacks フィールドは Partial<TrainerCallbacks> 型なので、必要なイベントだけ指定すれば OK です。コールバックの戻り値は破棄されます(unknown | Promise<unknown> という戻り値型は、コールバックから値を return しても TypeScript が文句を言わない、というだけの意味です)。

それぞれのコールバックがいつ発火するか

trainer.start()    ジョブを投入し { jobId } を返す。コールバックはまだ。


trainer.wait()     SSE ストリームを開く。コールバックはここから発火。


onStarted          1 回。`training.started` イベント時
onLog              多数回。メトリックスフレームごとに 1 回
onCheckpoint       数回。チェックポイントアップロードごとに 1 回
onCompleted        1 回。`training.completed` 時
        ── または ──
onFailed           1 回。`training.failed`(バックエンド報告の失敗)時

start() を呼んで wait() を呼ばないと、コールバックは決して動きません。arkor start は両方呼んでくれます。プログラムから呼び出す側も同じことをしてください。

パラメーター

onStarted({ job })

SSE ストリームが training.started を報告したときに発火します。ログ行や「学習開始しました」通知に使ってください。

onStarted: ({ job }) => {
  // job: TrainingJob (id, name, status, config, ...)
}

onLog({ step, loss, evalLoss, learningRate, epoch, samplesPerSecond, job })

学習が進むにつれて繰り返し発火します。各数値フィールドは number | null: バックエンドはあるステップで持っているフィールドだけ埋めます(なので evalLoss は eval 以外のステップで null、learningRate は LR スケジューラ更新の合間に null、など)。

onLog: ({ step, loss, evalLoss }) => {
  if (loss !== null) {
    forwardToMetrics({ step, loss, evalLoss });
  }
}

よくある用途は次のとおりです。自前のパイプライン(PostHog、Datadog)へのメトリックス転送、早期発散の検知、カスタム Early Stopping(指標が悪化したら学習を自動で打ち切るパターン。詳しくは Early Stopping レシピ を参照)の実装。Early Stopping を実装する際は、abortSignal の Abort がローカルの wait() を止めるだけである点に注意してください。バックエンドの GPU を実際に止めるには、その後 trainer.cancel() を呼ぶ必要があります。

onCheckpoint({ step, adapter, job, infer, artifacts })

学習中にバックエンドでアダプターチェックポイントが保存されたときに発火します。adapter{ kind: "checkpoint", jobId, step }infer の詳細は infer ページにあります。要するにチャット形式のリクエストを取り、生の Response を返す関数です。

onCheckpoint: async ({ step, infer }) => {
  const res = await infer({
    messages: [{ role: "user", content: "Can't log in" }],
  });
  const sample = await res.text();
  // モデルが順調かどうか判定
}

TypeScript でファインチューニングする意義の多くはここにあります。学習途中のモデルに対して、学習完了を待たずに用意したプロンプトを試せます。

onCompleted({ job, artifacts })

成功時に 1 回発火。artifactsunknown[]: バックエンドが送った生の artifact リスト。スキーマは進化するため SDK で絞り込みません。

onCompleted: ({ job, artifacts }) => {
  saveAdapterId({ jobId: job.id, count: artifacts.length });
}

onFailed({ job, error })

バックエンドからの失敗報告で 1 回発火。errorstring(バックエンドが送ったメッセージ)であり、Error インスタンスではありません。

onFailed: ({ job, error }) => {
  // error: string
}

onFailedバックエンド側の 失敗専用です。他のコールバック内で投げられた例外は onFailed には届きません。何が起きるかは下記の 例外ハンドリング を参照してください。

振る舞い

順序

各コールバックは次のイベントがディスパッチされる前に await されます。Promise を返してよく(DB への書き込み、Slack への投稿、infer の呼び出しなど)、SDK は次のフレーム処理前にそれを待ちます。同一トレーナーに対する並行ディスパッチはありません。

例外ハンドリング

コールバック内で throw すると、その場でエラーとともに wait() が reject します。SDK はコールバックの失敗とトランスポートの失敗を区別します。throw されたコールバックは SSE 再接続ハンドラーには ルートされない ので、その失敗を黙って飛ばして続行することはありません。

これが重要なのは、コールバックが実行される時点で、その失敗イベントの Last-Event-ID は既に進んでいるためです。もし throw をリトライしてしまうと、再接続はそのイベントの から再開してイベントをスキップし、エラーを握り潰し、(終端の training.completed イベントの場合は)まるで何も生成しなかったかのように空の artifactswait() を解決してしまいます。代わりに reject することで、対処できる場所に失敗が表面化します。

再接続ハンドラーは純粋なトランスポート失敗(接続断や一時的な 5xx)専用です。それらは指数バックオフでリトライされますが、throw されたコールバックはリトライされません。

決定的かつ非致命的なエラーハンドリングが必要なら、コールバック内で catch してください(下の 2 つ目の例を参照)。そうすれば、回復可能な副作用の失敗(不安定な Slack 投稿、メトリクス書き込みなど)で実行全体が中断されることはありません。

最小例: すべてのイベントをログに出す。

const trainer = createTrainer({
  name: "support-bot-v1",
  model: "unsloth/gemma-4-E4B-it",
  dataset: { type: "huggingface", name: "arkorlab/triage-demo" },
  callbacks: {
    onStarted: ({ job }) => console.log(`run ${job.id} accepted`),
    onLog: ({ step, loss }) => {
      if (loss !== null) console.log(`step=${step} loss=${loss.toFixed(4)}`);
    },
    onCheckpoint: async ({ step, infer }) => {
      const res = await infer({
        messages: [{ role: "user", content: "Hello" }],
      });
      console.log(`ckpt @ ${step}:`, await res.text());
    },
    onCompleted: ({ job }) => console.log(`run ${job.id} done`),
    onFailed: ({ error }) => console.error(`failed: ${error}`),
  },
});

await trainer.start();
await trainer.wait();

コールバック内で catch して回復可能な失敗をローカルに閉じ込め、wait() が reject しないようにする:

onCheckpoint: async ({ step, infer }) => {
  try {
    await sendToReview({ step, sample: await (await infer({ ... })).text() });
  } catch (err) {
    // ログ/メトリックス/コールバックの外側で trainer.cancel() を呼んで自分で学習を失敗させるか判断
  }
}

型定義

interface TrainingLogContext {
  step: number;
  loss: number | null;
  evalLoss: number | null;
  learningRate: number | null;
  epoch: number | null;
  samplesPerSecond: number | null;
  job: TrainingJob;
}

interface CheckpointContext {
  step: number;
  adapter: { kind: "checkpoint"; jobId: string; step: number };
  job: TrainingJob;
  infer: (args: InferArgs) => Promise<Response>;
  artifacts?: unknown[];
}

TrainingLogContextCheckpointContextarkor から名前付きでエクスポートされていません。自分のコードで型付きコールバックパラメーターが必要なら、インラインで同じ形を定義してください。

関連項目