From 6b701dbea9482a4bbcc29f31a8752d6827c645ee Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 5 Jan 2022 09:44:19 -0800 Subject: [PATCH] [fud] Add save_temps option to Xilinx synthesis (#851) * First attempt at save_temps * Fix abspath invocation * Document save_temps * Fix read_file output function * Little docs fixes * 2 minutes turns out to be what it takes to crash in synthesis. 5 minutes is about what it takes to successfully produce an `xclbin`. * The fud invocation should use `-o` instead of `--to` because the output file is binary. (fud will crash when trying to decode it to a printable string.) * Fix close_and_get in SSH context * Rename `keep` to `keep_tmpdir` * Comment about save_temps --- docs/fud/synthesis.md | 9 +++++++-- fud/fud/stages/remote_context.py | 14 ++++++++------ fud/fud/stages/xilinx/xclbin.py | 21 ++++++++++++++------- fud/fud/utils.py | 25 +++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/docs/fud/synthesis.md b/docs/fud/synthesis.md index 15c3daa678..c8d24699d9 100644 --- a/docs/fud/synthesis.md +++ b/docs/fud/synthesis.md @@ -88,9 +88,14 @@ Hopefully someone will figure this out and document it in the future. The first step in the Xilinx toolchain is to generate [an `xclbin` executable file][xclbin]. Here's an example of going all the way from a Calyx program to that: - fud e --to xclbin examples/futil/dot-product.futil + fud e examples/futil/dot-product.futil -o foo.xclbin -On our machines, compiling even a simple example like the above for simulation takes about 2 minutes, end to end. +On our machines, compiling even a simple example like the above for simulation takes about 5 minutes, end to end. +A failed run takes about 2 minutes to produce an error. + +By default, the Xilinx tools run in a temporary directory that is deleted when `fud` finishes. +To instead keep the sandbox directory, use `-s xclbin.save_temps true`. +You can then find the results in a directory named `fud-out-N` for some number `N`. ### How it Works diff --git a/fud/fud/stages/remote_context.py b/fud/fud/stages/remote_context.py index e55690f77b..0df6afde8b 100644 --- a/fud/fud/stages/remote_context.py +++ b/fud/fud/stages/remote_context.py @@ -120,10 +120,11 @@ def run_remote(client: SourceType.UnTyped, tmpdir: SourceType.String): run_remote(client, tmpdir) - def _close(self, client, remote_tmpdir): + def _close(self, client, remote_tmpdir, keep_tmpdir=False): """Close the SSH connection to the server. - Also removes the remote temporary directory. + Also removes the remote temporary directory, unless the + `keep_tmpdir` flag is set. """ @self.stage.step() @@ -131,7 +132,8 @@ def finalize_ssh(client: SourceType.UnTyped, tmpdir: SourceType.String): """ Remove created temporary files and close ssh connection. """ - client.exec_command(f"rm -r {tmpdir}") + if not keep_tmpdir: + client.exec_command(f"rm -r {tmpdir}") client.close() finalize_ssh(client, remote_tmpdir) @@ -159,7 +161,7 @@ def copy_back( copy_back(client, remote_tmpdir, local_tmpdir) self._close(client, remote_tmpdir) - def close_and_get(self, client, remote_tmpdir, path): + def close_and_get(self, client, remote_tmpdir, path, keep_tmpdir=False): """Close the SSH connection and retrieve a single file. Produces the resulting downloaded file. @@ -176,8 +178,8 @@ def fetch_file( dest_path = tmpfile.name with self.SCPClient(client.get_transport()) as scp: scp.get(src_path, dest_path) - return dest_path.open("rb") + return Path(dest_path) local_path = fetch_file(client, remote_tmpdir) - self._close(client, remote_tmpdir) + self._close(client, remote_tmpdir, keep_tmpdir=keep_tmpdir) return local_path diff --git a/fud/fud/stages/xilinx/xclbin.py b/fud/fud/stages/xilinx/xclbin.py index 8c84583058..22a04aa472 100644 --- a/fud/fud/stages/xilinx/xclbin.py +++ b/fud/fud/stages/xilinx/xclbin.py @@ -5,7 +5,7 @@ from fud.stages import Source, SourceType, Stage from fud.stages.remote_context import RemoteExecution from fud.stages.futil import FutilStage -from fud.utils import TmpDir, shell +from fud.utils import TmpDir, FreshDir, shell class XilinxStage(Stage): @@ -40,6 +40,10 @@ def __init__(self, config): self.remote_exec = RemoteExecution(self) self.temp_location = self.config["stages", self.name, "temp_location"] + # As a debugging aid, the pass can optionally preserve the + # (local or remote) sandbox where the Xilinx commands ran. + self.save_temps = bool(self.config["stages", self.name, "save_temps"]) + self.mode = self.config["stages", self.name, "mode"] self.device = self.config["stages", self.name, "device"] @@ -75,7 +79,10 @@ def copy_file( """Copy an input file.""" shutil.copyfile(src_path, Path(tmpdir) / dest_path) - tmpdir = Source(TmpDir(), SourceType.Directory) + tmpdir = Source( + FreshDir() if self.save_temps else TmpDir(), + SourceType.Directory, + ) for src_path, dest_path in input_files.items(): if not isinstance(src_path, Source): src_path = Source(src_path, SourceType.Path) @@ -173,9 +180,9 @@ def compile_xclbin(client: SourceType.UnTyped, tmpdir: SourceType.String): def read_file( tmpdir: SourceType.Directory, name: SourceType.String, - ) -> SourceType.Stream: + ) -> SourceType.Path: """Read an output file.""" - return Path(tmpdir.name) / name.data + return Path(tmpdir.name) / name if self.remote_exec.use_ssh: self.remote_exec.import_libs() @@ -200,14 +207,14 @@ def read_file( compile_xclbin(client, tmpdir) if self.remote_exec.use_ssh: - xclbin = self.remote_exec.close_and_get( + return self.remote_exec.close_and_get( client, tmpdir, "xclbin/kernel.xclbin", + keep_tmpdir=self.save_temps, ) else: - xclbin = read_file( + return read_file( tmpdir, Source("xclbin/kernel.xclbin", SourceType.String), ) - return xclbin diff --git a/fud/fud/utils.py b/fud/fud/utils.py index b97dd53d80..7cf8773cc4 100644 --- a/fud/fud/utils.py +++ b/fud/fud/utils.py @@ -71,6 +71,8 @@ def remove(self): class TmpDir(Directory): + """A temporary directory that is automatically deleted.""" + def __init__(self): self.tmpdir_obj = TemporaryDirectory() self.name = self.tmpdir_obj.name @@ -82,6 +84,29 @@ def __str__(self): return self.name +class FreshDir(Directory): + """A new empty directory for saving results into. + + The directory is created in the current working directory with an + arbitrary name. This way, `FreshDir` works like `TmpDir` except the + directory is not automatically removed. (It can still be manually + deleted, of course.) + """ + + def __init__(self): + # Select a name that doesn't exist. + i = 0 + while True: + name = "fud-out-{}".format(i) + if not os.path.exists(name): + break + i += 1 + + # Create the directory. + os.mkdir(name) + self.name = os.path.abspath(name) + + class Conversions: @staticmethod def path_to_directory(data):