8.1.9. Example: Connecting Compiled Functions With IOSpec Constraints
A sample program demonstrating how to pass a device-resident output from one compiled function to another without a host copy or a DRAM-to-DRAM layout conversion.
Execution Method
$ cd /opt/pfn/pfcomp/codegen/MLSDK/examples/
$ ./exec_with_env.sh python3 io_spec_constraints.py --device mncore2:auto
Expected Output
producer -> consumer direct-use succeeded
How It Works
The consumer is compiled first so that its preferred input IOSpec is available.
The producer is then compiled with an io_spec_constraints mapping whose key
is the producer output name and whose value is the corresponding consumer input
IOSpec:
io_spec_constraints={
"shared": compiled_consumer.input_specs["shared"],
}
An IOSpec constraint carries compatibility metadata such as layout and dtype,
but does not reuse the source IOSpec’s device buffer or address. When the
producer output TensorProxy is passed to the consumer, the runtime verifies
that the IOSpecs are compatible and relocates the consumer input to the producer
output buffer. Therefore, no intermediate host transfer or device-to-device
layout conversion is required.
The mapping key and the source IOSpec name do not need to be identical. This allows differently named function IOs to be connected explicitly.
Sample Program
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()