8.1.9. Example: Connecting Compiled Functions With IOSpec Constraints

ホストへのコピーや DRAM 間のレイアウト変換を行わずに、あるコンパイル済み関数のデバイス上の出力を別のコンパイル済み関数へ渡す方法を示すサンプルプログラム

実行方法

$ cd /opt/pfn/pfcomp/codegen/MLSDK/examples/
$ ./exec_with_env.sh python3 io_spec_constraints.py --device mncore2:auto

想定出力

producer -> consumer direct-use succeeded

仕組み

まず、 consumer の望ましい入力 IOSpec を利用できるようにするため、 consumer をコンパイルします。次に、 producer の出力名をキー、対応する consumer の入力 IOSpec を値とする io_spec_constraints マッピングを指定して、 producer をコンパイルします:

io_spec_constraints={
    "shared": compiled_consumer.input_specs["shared"],
}

IOSpec 制約にはレイアウトや dtype などの互換性メタデータが含まれますが、元の IOSpec のデバイスバッファーやアドレスは再利用しません。 producer の出力 TensorProxy を consumer に渡すと、ランタイムは IOSpec に互換性があることを検証し、 consumer の入力を producer の出力バッファーに再配置します。したがって、中間的なホスト転送やデバイス間のレイアウト変換は不要です。

マッピングのキーと元の IOSpec 名は同一である必要はありません。これにより、異なる名前を持つ関数の入出力を明示的に接続できます。

サンプルプログラム

リスト 8.9 /opt/pfn/pfcomp/codegen/MLSDK/examples/io_spec_constraints.py
 1"""Connect compiled functions without a device-to-device layout conversion.
 2
 3The consumer is compiled first. The producer then uses the consumer input IOSpec
 4as a compile-time constraint for its output. At execution time, relocation lets
 5the consumer read the producer output buffer directly; no predefined device
 6buffer or host copy is needed.
 7"""
 8
 9import argparse
10
11import torch
12from mlsdk import Context, MNDevice, storage
13
14TensorDict = dict[str, torch.Tensor]
15
16
17def parse_args() -> argparse.Namespace:
18    parser = argparse.ArgumentParser()
19    parser.add_argument("--device", default="mncore2:auto")
20    return parser.parse_args()
21
22
23def main() -> None:
24    args = parse_args()
25    context = Context(MNDevice(args.device))
26
27    def producer(inputs: TensorDict) -> TensorDict:
28        return {"shared": inputs["x"] * 2}
29
30    def consumer(inputs: TensorDict) -> TensorDict:
31        return {"result": inputs["shared"] + 1}
32
33    sample = torch.randn(32, 64)
34    compile_options = {"float_dtype": "float"}
35
36    # Compile the consumer first, because it is the side whose preferred input
37    # layout the producer output must satisfy.
38    compiled_consumer = context.compile(
39        consumer,
40        {"shared": sample},
41        storage.path("/tmp/io_spec_constraints_consumer"),
42        options=compile_options,
43        training=False,
44    )
45
46    # The mapping key is a producer input/output name. The value can come from a
47    # differently named function IO; using the same name is the common case.
48    shared_names = {"shared"}
49    compiled_producer = context.compile(
50        producer,
51        {"x": sample},
52        storage.path("/tmp/io_spec_constraints_producer"),
53        options=compile_options,
54        training=False,
55        io_spec_constraints={
56            name: compiled_consumer.input_specs[name] for name in shared_names
57        },
58    )
59
60    producer_outputs = compiled_producer({"x": torch.ones_like(sample)})
61
62    # producer_outputs["shared"] is a TensorProxy. Since the two IOSpecs are
63    # compatible, the runtime relocates the consumer input to that device buffer
64    # instead of compiling or executing a DRAM-to-DRAM layout conversion.
65    outputs = compiled_consumer(producer_outputs)
66    result = outputs["result"].cpu()
67
68    assert compiled_producer.output_specs["shared"].is_memcopyable_to(
69        compiled_consumer.input_specs["shared"]
70    )
71    assert torch.allclose(result, torch.full_like(result, 3))
72    print("producer -> consumer direct-use succeeded")
73
74
75if __name__ == "__main__":
76    main()