1+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2+ # All rights reserved.
13# Copyright 2023-2026 Arm Limited and/or its affiliates.
24#
35# This source code is licensed under the BSD-style license found in the
46# LICENSE file in the root directory of this source tree.
57
68
9+ import hashlib
710import os
811import struct
912import tempfile
1013
14+ from collections .abc import Mapping
15+ from dataclasses import dataclass
1116from typing import List
1217
1318import numpy as np
2025 has_vela = False
2126
2227
28+ _BLOCK_NAME_LENGTH = 16
29+ _BLOCK_HEADER = struct .Struct (f"<{ _BLOCK_NAME_LENGTH } sIB11s" )
30+ _BLOCK_ALIGNMENT = 16
31+ _STREAM_HEADER = "vela_bin_stream"
32+ _STREAM_FOOTER = "vela_end_stream"
33+ _EXTERNAL_REFERENCE = 1
34+ _RESERVED_BYTES = b"\x00 " * 11
35+
36+
37+ @dataclass (frozen = True )
38+ class VelaExternalBlock :
39+ """Named-data payload emitted by Vela compilation."""
40+
41+ key : str
42+ payload : bytes
43+ alignment : int
44+ placement : str
45+
46+
47+ @dataclass (frozen = True )
48+ class VelaCompileResult :
49+ """Binary stream and external payloads produced by Vela."""
50+
51+ processed_bytes : bytes
52+ external_blocks : tuple [VelaExternalBlock , ...] = ()
53+
54+
2355def _as_int32 (value , name : str ) -> int :
2456 """Convert numpy scalars to signed int32 with a clear error on overflow."""
2557 arr = np .asarray (value )
@@ -65,16 +97,18 @@ def vela_compile(
6597 args : List [str ],
6698 verbose : bool = False ,
6799 intermediate_path : str | None = None ,
68- ):
100+ block_placements : Mapping [str , str ] | None = None ,
101+ ) -> VelaCompileResult :
69102 """Compile a TOSA graph to a binary stream for ArmBackendEthosU using
70103 Vela.
71104 """
72105 if not has_vela :
73106 raise RuntimeError (
74107 "ethos-u-vela pip package couldn't be imported. Make sure it's installed!"
75108 )
109+ resolved_block_placements : Mapping [str , str ] = block_placements or {}
76110
77- def run (dir : str ) -> bytes :
111+ def run (dir : str ) -> VelaCompileResult :
78112 tosaname = "out.tosa"
79113 tosa_path = os .path .join (dir , tosaname )
80114 with open (tosa_path , "wb" ) as f :
@@ -90,11 +124,10 @@ def run(dir: str) -> bytes:
90124
91125 np_path = os .path .join (dir , "output" , "out_vela.npz" )
92126
93- blocks = b""
94127 with np .load (np_path , allow_pickle = False ) as data :
95128 # Construct our modified output_blocks with data in a form easily
96129 # digested on the device side
97- bin_blocks = {"vela_bin_stream" : b"" }
130+ bin_blocks = {_STREAM_HEADER : b"" }
98131
99132 # copy command data through unmodified
100133 bin_blocks ["cmd_data" ] = data ["cmd_data" ].tobytes ()
@@ -114,28 +147,58 @@ def run(dir: str) -> bytes:
114147 bin_blocks ["inputs" ] = vela_bin_pack_io ("input" , data )
115148 bin_blocks ["outputs" ] = vela_bin_pack_io ("output" , data )
116149
117- bin_blocks ["vela_end_stream" ] = b""
150+ bin_blocks [_STREAM_FOOTER ] = b""
151+
152+ unknown_blocks = resolved_block_placements .keys () - bin_blocks .keys ()
153+ if unknown_blocks :
154+ raise ValueError (
155+ "External Vela block placements reference blocks that were not "
156+ f"emitted: { sorted (unknown_blocks )} "
157+ )
118158
119159 # Emit the NPZ regions as:
120160 # - 16 byte block name null terminated string (padded to 16 if name shorter)
121- # - 4 bytes of int32 block length and 12 bytes of 0's
161+ # - 4 bytes of int32 block length, 1 byte external flag, and 11 reserved bytes
122162 # - block data (padded to 16 byte alignment at end)
123163 # Repeat for all blocks
164+ blocks = b""
165+ external_blocks : list [VelaExternalBlock ] = []
124166 for key in bin_blocks .keys ():
125167 block_name = bytes (key , "utf8" )[:15 ]
126168 block_name = block_name + b"\x00 " * (16 - len (block_name ))
169+ block_data = bin_blocks [key ]
170+ placement = resolved_block_placements .get (key )
171+ external_reference = 0
172+ if placement is not None :
173+ digest = hashlib .sha256 (
174+ placement .encode ("ascii" ) + b"\0 " + block_data
175+ ).hexdigest ()
176+ external_blocks .append (
177+ VelaExternalBlock (
178+ key = digest ,
179+ payload = block_data ,
180+ alignment = _BLOCK_ALIGNMENT ,
181+ placement = placement ,
182+ )
183+ )
184+ block_data = digest .encode ("ascii" )
185+ external_reference = _EXTERNAL_REFERENCE
127186
128187 # We need the acual unpadded block lengths for hw setup
129- block_length_bytes = struct .pack ("<iiii" , len (bin_blocks [key ]), 0 , 0 , 0 )
188+ block_header = _BLOCK_HEADER .pack (
189+ block_name ,
190+ len (block_data ),
191+ external_reference ,
192+ _RESERVED_BYTES ,
193+ )
130194
131195 # Pad block data to multiple of 16 bytes
132- block_data = bin_blocks [key ]
133196 block_data = block_data + b"\x00 " * (15 - (len (block_data ) - 1 ) % 16 )
134197
135- block = block_name + block_length_bytes + block_data
198+ block = block_header + block_data
136199 blocks = blocks + block
137200
138- return blocks
201+ return VelaCompileResult ( blocks , tuple ( external_blocks ))
139202
140203 if intermediate_path is not None :
141204 return run (intermediate_path )
0 commit comments