コンテンツにスキップ

シリアライズできない値の受け渡し (Passing Values That Cannot Be Serialized)

一部の値はデータ(JSON など)に変換できません。Diffusers パイプライン、潜在テンソル(Latent Tensor)、開かれたファイルハンドル、アクティブなドライバーなど — これらを表現する JSON は存在しません。ライブラリがワーカーサブプロセス内で分離実行されている場合(ワーカーによるノードの分離 を参照)、パラメータ値はオーケストレーターとワーカーの間を JSON として移動するため、ノード間でこれらのオブジェクトを受け渡すには特別な仕組みが必要です。

結論から言えば: 生成側の出力パラメータに serializable=False を指定し、オブジェクトを代入します。消費側(受け取り側)は何も宣言せず、通常どおりオブジェクトを読み取ります。

class LoadPipeline(ControlNode):
    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)
        self.add_parameter(
            Parameter(
                name="pipeline",
                output_type="Pipeline",
                tooltip="ロードされたパイプライン",
                serializable=False,
                allowed_modes={ParameterMode.OUTPUT},
            )
        )

    def process(self) -> None:
        self.parameter_output_values["pipeline"] = load_pipeline(...)


class Generate(ControlNode):
    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)
        # 消費側では特別な宣言は不要です。
        self.add_parameter(Parameter(name="pipeline", input_types=["Pipeline"], tooltip="実行するパイプライン"))

    def process(self) -> None:
        pipeline = self.get_parameter_value("pipeline")  # 実際のオブジェクトが取得される
        ...

内部で実際に起きていること (What actually happens)

オブジェクトはそれを生成したプロセス内にそのまま保持されます。値がプロセスの境界を越えようとしたとき、エンジンは不透明な キー (Key)(短い文字列)に置き換えて送信します。消費側のノードがパラメータを読み取ると、そのキーが再び実際のオブジェクトに解決されます。

把握しておくべき 3 つの帰結:

  • ノード自身の辞書には常に実物が格納されます。 パイプラインを parameter_output_values["pipeline"] に代入してすぐに読み戻せば、キーではなくパイプラインそのものが取得できます。書き込み時に何かが置き換わることはありません。
  • プロセスから外に出ないグラフでは、この処理は一切発生しません。 ライブラリが Shared(共有)モードで実行されている場合、値は常に参照渡し(by reference)されます。
  • 宣言が必要なのは生成側(Producer)のみです。 キーは何も宣言していない消費側への接続を通じて移動するため、上記の消費側パラメータは通常の Parameter のままで構いません。

serializable=False の意味 (What serializable=False means)

これは、保存されたワークフローファイルから値を除外します。出力パラメータに設定した場合、データ以外の値を生成元プロセス内に保持し、プロセスの境界を越えてキーのみを送信します。宣言されたパラメータであっても、プレーンなデータはそのままデータとして送信されます:

serializable=False 出力の値 境界を越えるもの 理由
パイプライン、テンソル、ドライバー キー(オブジェクトはプロセス内に保持) データ形式が存在しないため
API キー文字列 文字列そのもの 正常に送信可能であり、相手側でキーを解決できないため
数値の dict、文字列のリスト 値そのもの すでにデータ形式であるため
ImageUrlArtifact キー(オブジェクトはプロセス内に保持) 下記を参照

最後の行は意外に思われるかもしれません。ライブラリが独自に定義したアーティファクトは、フィールドの辞書にアンラップ(非構造化)され、転送途中でペイロードが失われるため、エンジンは往復処理のリスクを冒しません(パラメータを宣言した場合、オブジェクトはプロセス内に保持されます)。アーティファクトをデータとして移動させたい場合(URL を含むものなどは通常これに該当します)は、パラメータを宣言しないでください(serializable=False を付与しない)。未宣言のアーティファクトは、これまで通りシリアライズおよび再構築されます。

実行間で高コストなリソースを再利用する

上記はノード間を流れる値に関するものです。一度構築して再利用したい「リソース」(ロードに 30 秒かかるパイプラインなど)には、再度導出可能なキーが必要となるため、ノード上に独自の軽量 API が用意されています:

def process(self) -> None:
    key = self.local_objects.key_for(self._config_hash())
    pipeline = self.local_objects.get(key)
    if pipeline is None:
        pipeline = build_pipeline(...)
        self.local_objects.put(pipeline, key=self._config_hash(), on_drop=release_vram)
    ...
呼び出し 動作
local_objects.put(value, *, key, on_drop=None) 指定したキーで value を保持し、完全な名前空間付きキーを返します
local_objects.get(key) オブジェクトを取得(このプロセスで保持されていない場合は None)
local_objects.key_for(suffix) 何も格納せずに、選択したサフィックスに対する完全なキーを返します
local_objects.drop(key) オブジェクトを 1 つ解放(保持されていたかどうかを返します)
local_objects.drop_all() このプロセスでライブラリが保持しているすべてのオブジェクトを解放(「キャッシュクリア」ノードが呼び出す処理)

既に使用されているキーに対して put を行うと、同じオブジェクトでない限り、以前存在していたオブジェクトが解放されます — したがって、変更されていないハッシュ値の下で再構築を行っても、古いオブジェクトが孤立して残ることはありません。

オブジェクトが保持するリソースの解放

最後の Python 参照を破棄しても、VRAM が自動的に解放されるわけではありません。パラメータに on_local_object_drop を渡す(または put に on_drop を渡す)と、オブジェクトが解放されたときにエンジンがそれを呼び出します:

Parameter(
    name="pipeline",
    output_type="Pipeline",
    tooltip="ロードされたパイプライン",
    serializable=False,
    allowed_modes={ParameterMode.OUTPUT},
    on_local_object_drop=lambda pipeline: pipeline.to("cpu"),
)

このフックはキャッシュに属しているため、キャッシュがオブジェクトを手放したときに実行されます:

  • ノードが再実行 され、キャッシュがそのパラメータに対して新しいオブジェクトを受け取ったとき(以前保持していたものが解放されます);
  • ノードが削除 され、そのオブジェクトを参照するものが他になくなったとき;
  • ライブラリがアンロード されたとき、またはすべてのライブラリが再読み込みされたとき;
  • ワークフローが閉じられたか、クリアされたとき。

1 つのオブジェクトが 2 つの出力に接続されている場合でも、オブジェクトごとに 1 回だけ実行されます。

このフックが 対象としない のは、キャッシュに到達しなかったオブジェクトです。ライブラリが Shared モードで実行されている場合、プロセスの境界が存在しないため何もキャッシュされず、値は常に参照渡しされます — キャッシュが解放すべきものは存在しないため、リソースの解放は従来通り開発者自身が行う必要があります。実行の途中で上書きされたオブジェクトも同様です(ノードの処理完了時にパラメータが保持しているものだけがキャッシュに入ります)。また、現行の実装では単一のライブラリの更新や Git 参照の切り替えによってワーカーが再起動されるわけではないため、そのワーカーが保持しているオブジェクトは新しいコードにも存続します。

できないこと (What you cannot do)

リストや辞書パラメータは値を保持できません。 ParameterList と ParameterDictionary は子要素から値を構築するため、保持すべき単一のオブジェクトが存在せず、解放フックを配置する場所もありません。これらに serializable=False を宣言しても、保存されたワークフローから除外するという従来の動作を行うだけであり、キャッシュ機能は追加されません。

バッチ全体を serializable=False とマークされた通常の Parameter で出力してください — テンソルのリストは保持の観点からは 1 つのオブジェクトとみなされるため、正常に動作します。保持された値を 消費 する ParameterList は問題ありません(各行が独自のキーを持ち、コンテナ上の get_parameter_value でオブジェクトが取得できます。保持された値に対して get_parameter_list_value を使用すると、イテラブルなものが平坦化されテンソルのリストが行ごとに分解されてしまうため、get_parameter_value を優先してください)。

オブジェクトはそれを生成したプロセスから外に出ることはできません。 キャッシュはライブラリではなくワーカーに属しています。1 つのワーカーが複数のライブラリをホストしてキャッシュを共有できるため、同じワーカーに共存するライブラリであれば渡されたキーを正常に解決できます。動作しないのは、別の プロセス(別のワーカーやオーケストレーター)からキーを読み取ろうとする場合です:

Attempted to read the value for parameter 'pipeline' on node 'Generate'. Failed due to: it is held in another process, and an object cannot leave the process that built it. Read it from a node that runs in the same place as the one that made it, or have that node output a saved file instead.

2 つのライブラリがワーカーを共有するのは、どちらも独自の実行 venv を宣言していない場合に限られますが、これはグラフの作成者には見えません。したがって、別の ライブラリのノードにオブジェクトを渡すライブラリを提供する場合は、同一ワーカーでの共存に依存せず、ファイルをディスクに書き出してそのパスや URL を渡してください。自分自身のライブラリ内であれば、常に同一のワーカー内で実行されます。

put を通じて指定したキーは、ワーカー内でライブラリごとに名前空間が分離されるため、同じワーカー内の他のライブラリが自身のキャッシュに同じ文字列を使用しても衝突することはありません。

入力パラメータがキャッシュされることはありません。 入力をキャッシュすると、相手側に解決手段のないキーが発行されてしまいます(ノードが別の場所で実行されている間、オブジェクトは送信側プロセスに存在するため)。キャッシュに入るのは出力パラメータのみです。したがって、ワーカー境界を越えるノードに入力値として渡されたオブジェクトは、トランスポート層が処理できる形に変換されます。別のプロセスでオブジェクトが必要な場合は、それを生成するノードを同じライブラリ内で実行させるか、ファイルを出力してください。

再読み込み(リロード)をまたいで保持されるものはありません。 キーは実行中のプロセス内のメモリを参照しているためです。

発生する可能性のあるエラーとその意味

メッセージに含まれる文字列 意味
it is no longer available, which happens after the workflow is reloaded or the node that made it is re-run キーはこのプロセスで発行されたものですが、参照先はすでに解放されています。生成側ノードを再実行してください。これは設計通りの動作であり、オブジェクトの消失ではありません
held in another process オブジェクトを構築したプロセス以外の場所から読み取ろうとしています — 多くの場合、ワーカー内ではなくオーケストレーター内で実行されるバリデーションや値フックからの読み取りが原因です
nothing is connected to it 入力パラメータが接続されていません

2 つ目のエラーは安全のための仕様です。キーは割り当てごとに一意であるため、前回の実行時のキーを保持している消費側が、渡されていない より新しい オブジェクトを誤って暗黙的に解決してしまうのを防ぎます。

保存とメタデータ (Saving and metadata)

保持されている値が保存されたワークフローに書き込まれることはありません。そのような値を持つノードは再読み込み時に UNRESOLVED 状態で復帰するため、生成側ノードが再実行されて値が再生成されます。ワークフローのメタデータや画像のサイドカーファイルには、キーが記録されるのではなく、パラメータが除外(omitted)されたことが報告されます。開発者がこのために特別な処理を行う必要はありません(serializable=False 宣言がすべてを処理します)。

チェックリスト (Checklist)

  • 生成側の出力パラメータが serializable=False と宣言されており、単純な参照破棄以上のクリーンアップが必要な場合は on_local_object_drop が指定されているか。
  • 消費側のパラメータには何も特別な宣言をしていないか。
  • 生成側と消費側が同じワーカー内で実行されているか(同一ライブラリ内であれば自動的)。
  • URL を含むオブジェクト(ImageUrlArtifact など)は、データとして移動できるように未宣言(serializable=False を付与しない)のままにしているか。
  • ノードが Shared モードで実行される可能性がある場合、解放フックに依存していないか(Shared モードでは何もキャッシュされないため、フックも解放されません)。
  • バッチ処理は ParameterList 出力ではなく、通常のパラメータとして出力しているか。