5252#include < memory>
5353#include < optional>
5454#include < set>
55+ #include < span>
5556#include < sstream>
5657#include < string>
5758#include < unordered_map>
@@ -111,29 +112,93 @@ class Graph : public INode {
111112 return ok ();
112113 }
113114
114- // Compiles the graph using IREE compiler and sets up the IREE VM
115- // context for future g->execute calls.
115+ // Compiles the graph using IREE compiler and sets up the IREE VM context for
116+ // future g->execute calls. This is the default JIT convenience API.
117+ //
118+ // This is equivalent to:
119+ // compileToArtifact(handle.getBackend(), remove)
120+ // loadFromArtifact(handle, vmfbBytes)
121+ //
122+ // The compiled artifact is backend-specific and is immediately loaded using
123+ // the same backend from `handle`.
116124 //
117125 // Set `remove = true` to remove compilation artifacts (cache files) when
118126 // this `Graph` instance goes out of scope.
119127 ErrorObject compile (const Handle &handle, bool remove = false ) {
120128 FUSILLI_LOG_LABEL_ENDL (" INFO: Compiling Graph" );
129+ FUSILLI_ASSIGN_OR_RETURN (auto vmfbBytes,
130+ compileToArtifact (handle.getBackend (), remove));
131+ return loadFromArtifact (handle, vmfbBytes);
132+ }
133+
134+ // Compiles the graph using IREE compiler to produce a backend-specific VMFB
135+ // artifact. This does not create any runtime state; call
136+ // `loadFromArtifact()` before executing.
137+ //
138+ // If callers need artifacts for multiple backends, they should keep or
139+ // serialize each returned VMFB byte buffer before compiling the next backend.
140+ // Fusilli owns only the in-process `cache_` for the most recent compile-side
141+ // artifact; it does not provide persistent artifact storage or invalidation.
142+ //
143+ // Compiling a new artifact does not load or unload runtime state. If this
144+ // `Graph` already has an artifact loaded, that artifact remains executable
145+ // until a later `loadFromArtifact()` replaces it.
146+ //
147+ // Set `remove = true` to remove compilation artifacts (cache files) when
148+ // this `Graph` instance goes out of scope.
149+ ErrorOr<std::vector<uint8_t >> compileToArtifact (Backend backend,
150+ bool remove = false ) {
151+ FUSILLI_LOG_LABEL_ENDL (" INFO: Compiling Graph to artifact" );
121152 FUSILLI_RETURN_ERROR_IF (!isValidated_, ErrorCode::NotValidated,
122153 " Graph must be validated before being compiled" );
123154
124155 // Generate MLIR assembly for this graph.
125156 FUSILLI_ASSIGN_OR_RETURN (std::string generatedAsm, emitAsm ());
126157
127158 // Compile using IREE compiler or reuse cached artifact.
128- FUSILLI_ASSIGN_OR_RETURN (auto vmfbPath,
129- getCompiledArtifact (handle , generatedAsm, remove));
159+ FUSILLI_ASSIGN_OR_RETURN (
160+ auto vmfbPath, getCompiledArtifact (backend , generatedAsm, remove));
130161
131162 FUSILLI_LOG_LABEL_ENDL (" INFO: Compiled Graph cached at \" " +
132163 vmfbPath.string () + " \" " );
133164
134- // Create per-graph IREE VM context and load the compiled artifact.
135- FUSILLI_CHECK_ERROR ( createVmContext (handle, vmfbPath. string ()));
165+ return readFileBytes (vmfbPath);
166+ }
136167
168+ // Loads a compiled VMFB artifact onto the device owned by `handle`, creating
169+ // per-graph runtime state and querying workspace size. This does not require
170+ // the artifact to have been produced by this `Graph` instance.
171+ //
172+ // The artifact must be compatible with `handle.getBackend()`. A VMFB compiled
173+ // for one backend is not a portable artifact for another backend.
174+ //
175+ // A `Graph` owns one loaded runtime state at a time. Loading a new artifact
176+ // replaces any previously loaded VM context, function, workspace size, and VM
177+ // input list capacity. If loading fails, the `Graph` is left without loaded
178+ // runtime state. To keep multiple backends loaded concurrently, use one
179+ // `Graph` instance per backend artifact. To switch backends on one `Graph`,
180+ // call `loadFromArtifact()` with the selected backend artifact before
181+ // `execute()`.
182+ ErrorObject loadFromArtifact (const Handle &handle,
183+ std::span<const uint8_t > vmfbBytes) {
184+ FUSILLI_LOG_LABEL_ENDL (" INFO: Loading compiled artifact into VM context" );
185+ FUSILLI_RETURN_ERROR_IF (
186+ !isValidated_, ErrorCode::NotValidated,
187+ " Graph must be validated before loading a compiled artifact" );
188+ std::vector<uint8_t > artifactBytes (vmfbBytes.begin (), vmfbBytes.end ());
189+ // Loading replaces the currently executable artifact. Clear first so a
190+ // failed load cannot leave execute() using stale runtime state from an
191+ // older artifact.
192+ clearRuntimeState ();
193+ loadedArtifactBytes_ = std::move (artifactBytes);
194+ ErrorObject status = createVmContext (handle);
195+ if (isError (status)) {
196+ // createVmContext may have partially populated runtime state before
197+ // failing; leave the graph in a clean "not loaded" state.
198+ clearRuntimeState ();
199+ return status;
200+ }
201+ loadedBackend_ = handle.getBackend ();
137202 return ok ();
138203 }
139204
@@ -235,7 +300,7 @@ class Graph : public INode {
235300 std::shared_ptr<Buffer>> &variantPack,
236301 const std::shared_ptr<Buffer> &workspace) const ;
237302
238- // Delete copy constructors, keep default move constructor and destructor .
303+ // Delete copy constructors and keep default move operations .
239304 Graph (const Graph &) = delete ;
240305 Graph &operator =(const Graph &) = delete ;
241306 Graph (Graph &&) noexcept = default ;
@@ -354,11 +419,11 @@ class Graph : public INode {
354419 // TODO(#13): Make this private. It is public for now to aid testing and
355420 // debuggability, however the intended user facing API is `Graph::compile()`.
356421 ErrorOr<std::filesystem::path>
357- getCompiledArtifact (const Handle &handle , const std::string &generatedAsm,
422+ getCompiledArtifact (Backend backend , const std::string &generatedAsm,
358423 bool remove, std::optional<bool > *reCompiled = nullptr ) {
359424 // Check for cache hit.
360425 FUSILLI_ASSIGN_OR_RETURN (bool cacheValid,
361- validateCache (handle , generatedAsm));
426+ validateCache (backend , generatedAsm));
362427 if (cacheValid) {
363428 if (reCompiled)
364429 *reCompiled = false ;
@@ -367,7 +432,7 @@ class Graph : public INode {
367432 // (Re)generate cache.
368433 FUSILLI_ASSIGN_OR_RETURN (
369434 auto generatedCache,
370- generateCompiledArtifact (handle , generatedAsm, remove));
435+ generateCompiledArtifact (backend , generatedAsm, remove));
371436 cache_ = std::move (generatedCache);
372437 if (reCompiled)
373438 *reCompiled = true ;
@@ -397,8 +462,16 @@ class Graph : public INode {
397462
398463private:
399464 // Definition in `fusilli/backend/runtime.h`.
400- ErrorObject createVmContext (const Handle &handle,
401- const std::string &vmfbPath);
465+ ErrorObject createVmContext (const Handle &handle);
466+
467+ void clearRuntimeState () {
468+ vmFunction_.reset ();
469+ vmContext_.reset ();
470+ workspaceSize_.reset ();
471+ loadedBackend_.reset ();
472+ loadedArtifactBytes_.clear ();
473+ vmInputListCapacity_ = 0 ;
474+ }
402475
403476 // Queries the required transient/workspace buffer size from the compiled
404477 // module. Returns the size in bytes, or 0 if no transients are needed.
@@ -410,8 +483,8 @@ class Graph : public INode {
410483 // `remove = true` to remove cache files when returned `CachedAssets` lifetime
411484 // ends.
412485 ErrorOr<CachedAssets>
413- generateCompiledArtifact (const Handle &handle ,
414- const std::string &generatedAsm, bool remove) {
486+ generateCompiledArtifact (Backend backend, const std::string &generatedAsm ,
487+ bool remove) {
415488 FUSILLI_LOG_LABEL_ENDL (" INFO: Generating compiled artifacts" );
416489
417490 // Create cache files.
@@ -448,16 +521,16 @@ class Graph : public INode {
448521 if (checkCompileBackendEnv ()) {
449522 // Use CompileCommand (CLI).
450523 CompileCommand cmd = CompileCommand::build (
451- handle. getBackend () , cache.input , cache.output , cache.statistics );
524+ backend , cache.input , cache.output , cache.statistics );
452525 FUSILLI_CHECK_ERROR (cmd.writeTo (cache.command ));
453526 FUSILLI_LOG_LABEL_ENDL (" INFO: iree-compile command (CLI)" );
454527 FUSILLI_LOG_ENDL (cmd.toString ());
455528 FUSILLI_CHECK_ERROR (cmd.execute ());
456529 } else {
457530 // Use CompileSession (C API) - DEFAULT.
458531 FUSILLI_ASSIGN_OR_RETURN (CompileSession session,
459- CompileSession::build (handle. getBackend () ,
460- cache.input , cache. output ,
532+ CompileSession::build (backend, cache. input ,
533+ cache.output ,
461534 cache.statistics ));
462535 FUSILLI_CHECK_ERROR (session.writeTo (cache.command ));
463536 FUSILLI_LOG_LABEL_ENDL (" INFO: iree-compile command (C API)" );
@@ -473,8 +546,8 @@ class Graph : public INode {
473546 // - Graph name (and therefore cache path) has changed
474547 // - Generated assembly differs
475548 // - Compile commands have changed
476- // - Handle/backend (and therefore compile command) has changed
477- ErrorOr<bool > validateCache (const Handle &handle ,
549+ // - Backend (and therefore compile command) has changed
550+ ErrorOr<bool > validateCache (Backend backend ,
478551 const std::string &generatedAsm) {
479552 FUSILLI_LOG_LABEL_ENDL (" INFO: Validating cache" );
480553
@@ -544,13 +617,13 @@ class Graph : public INode {
544617 if (checkCompileBackendEnv ()) {
545618 // Use CompileCommand (CLI).
546619 CompileCommand cmd =
547- CompileCommand::build (handle. getBackend () , input, output, statistics);
620+ CompileCommand::build (backend , input, output, statistics);
548621 cmdString = cmd.toString ();
549622 } else {
550623 // Use CompileSession (C API) - DEFAULT.
551- FUSILLI_ASSIGN_OR_RETURN (auto session,
552- CompileSession::build (handle. getBackend (), input ,
553- output, statistics));
624+ FUSILLI_ASSIGN_OR_RETURN (
625+ auto session ,
626+ CompileSession::build (backend, input, output, statistics));
554627 cmdString = session.toString ();
555628 }
556629
@@ -618,10 +691,14 @@ class Graph : public INode {
618691 // This is set after `validate()` is run at least once successfully.
619692 bool isValidated_ = false ;
620693
621- // Required workspace buffer size in bytes. Set during createVmContext()
622- // by querying the iree.abi.transients.size.constant attribute.
623- // std::nullopt indicates the graph has not been compiled yet.
624- std::optional<size_t > workspaceSize_;
694+ // Bytes backing the currently loaded VMFB artifact. IREE's bytecode module
695+ // may retain references to this archive, so keep it alive for as long as the
696+ // VM context is alive.
697+ //
698+ // Keep this declared before vmContext_: members destruct in reverse
699+ // declaration order, and the context must be released before the backing VMFB
700+ // bytes.
701+ std::vector<uint8_t > loadedArtifactBytes_;
625702
626703 // IREE VM context lifetime managed by the `Graph` object
627704 // (deleted when the `Graph` object goes out of scope).
@@ -631,11 +708,22 @@ class Graph : public INode {
631708 // Avoids repeated function lookup on every execute() call.
632709 std::optional<iree_vm_function_t > vmFunction_;
633710
711+ // Required workspace buffer size in bytes. Set during createVmContext()
712+ // by querying the iree.abi.transients.size.constant attribute.
713+ // std::nullopt indicates the graph has not been compiled yet.
714+ std::optional<size_t > workspaceSize_;
715+
634716 // Pre-computed VM input list capacity for iree_vm_list_create().
635717 // Set during createVmContext() to avoid recomputing on every execute().
636718 iree_host_size_t vmInputListCapacity_ = 0 ;
637719
638- // Cache set by `getCompiledArtifact()`.
720+ // Backend for the currently loaded runtime state. `execute()` requires a
721+ // handle for this backend because VMFB artifacts are backend-specific.
722+ std::optional<Backend> loadedBackend_;
723+
724+ // Compile-side cache set by `getCompiledArtifact()`. The AOT
725+ // `loadFromArtifact()` API uses caller-provided VMFB bytes directly and does
726+ // not consult this cache.
639727 //
640728 // Note: new instances should always re-generate cache even if the results
641729 // could be read from the file system. Old results may have been generated
0 commit comments