Skip to content

Commit 331b833

Browse files
authored
[python] Unify caching and key cache on CompileTarget hash (NVIDIA#4859)
Finally! Caching for Python done right 🙃 This builds on top of @atgeller's work in NVIDIA#4607, but using the CompileTarget now that it's available. We store the hashes of the compilation options that affect the compiled output within `CompiledModule`. We then determine whether the cached module can be reused by just comparing those hashes to the ones obtained for the new module. We distinguish three cases when it comes to caching: 1. When `CompileTarget.fullySpecialize == true`, we disable caching. This is the case where arguments get inlined into the IR, so keeping caches here would require hashing the runtime arguments. We currently don't support that. This mostly affects remote devices, so not having caching here is reasonable (and the current behaviour already). 2. When `CompileTarget.fullySpecialize == false` and there are no captured kernels, we key the cache on a hash of `CompileTarget`. This is the 'best' case, where the compilation options within `CompileTarget` fully determine the output of the compilation. 3. When `CompileTarget.fullySpecialize == false` and there are captured kernels, we key the cache on a pair of hashes: the hash of `CompileTarget` (as in 2) and the hash of the module IR itself. This detects changes to the captured kernels, as the captured targets are inlined into the IR before computing the hash. This should also subsume the second level of caching we were performing within the ExecutionContext. I'm going to run some benchmarks to make sure this hasn't regressed. --------- Signed-off-by: Luca Mondada <luca@mondada.net>
1 parent 2802275 commit 331b833

15 files changed

Lines changed: 259 additions & 129 deletions

File tree

cudaq/include/cudaq/Target/CompileTarget.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ class CompileTarget {
2121
/// Hook to update the pass pipeline before compilation.
2222
virtual void updatePassPipeline(std::string &passPipeline) const {}
2323

24+
/// Return a hash of this target's configuration, used as a cache key to
25+
/// decide whether a previously compiled module can be reused.
26+
///
27+
/// A hash of 0 disables caching.
28+
///
29+
/// TODO: CompileTarget should be made non-virtual. Once it is, this method
30+
/// becomes unnecessary and callers can use std::hash<CompileTarget> directly.
31+
virtual std::size_t hash() const;
32+
2433
/// Whether to recompile the kernel in the presence of an AOT-compiled module.
2534
///
2635
/// If this is `false` and an AOT-compiled kernel (in the form of a function
@@ -137,3 +146,8 @@ class CompileTarget {
137146
};
138147

139148
} // namespace cudaq
149+
150+
template <>
151+
struct std::hash<cudaq::CompileTarget> {
152+
std::size_t operator()(const cudaq::CompileTarget &t) const noexcept;
153+
};

cudaq/lib/Target/CompileTarget.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "cudaq/Target/CompileTarget.h"
1010
#include "cudaq/runtime/logger/logger.h"
1111
#include <cctype>
12+
#include <functional>
1213

1314
/// Replace `%KEY%` and `%KEY:default%` placeholders from runtime options.
1415
static void substitutePipelinePlaceholders(
@@ -97,3 +98,41 @@ cudaq::CompileTarget::CompileTarget(
9798
CUDAQ_INFO("{:<27} {}\n", "disable_qubit_mapping:", "true");
9899
}
99100
}
101+
102+
template <typename T>
103+
inline void hash_combine(std::size_t &seed, const T &val) {
104+
std::hash<T> hasher;
105+
seed ^= hasher(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
106+
}
107+
108+
template <typename... Args>
109+
inline std::size_t hash_val(const Args &...args) {
110+
std::size_t seed = 0;
111+
(hash_combine(seed, args), ...);
112+
return seed;
113+
}
114+
115+
std::size_t std::hash<cudaq::CompileTarget>::operator()(
116+
const cudaq::CompileTarget &t) const noexcept {
117+
std::size_t seed = hash_val(
118+
t.pipelineConfig.overridePassPipeline, t.pipelineConfig.highLevelPipeline,
119+
t.pipelineConfig.midLevelPipeline, t.pipelineConfig.lowLevelPipeline,
120+
t.pipelineConfig.codegenTranslation, t.pipelineConfig.postCodeGenPasses,
121+
t.pipelineConfig.skipTargetLoweringPipeline,
122+
t.pipelineConfig.disableQubitMapping,
123+
t.pipelineConfig.replaceStateWithKernel, t.pipelineConfig.addMeasurements,
124+
t.overrideAOTCompilation, t.emulate, t.warnNamedMeasurements,
125+
t.supportConditionalsOnMeasureResults, t.supportDeviceCalls,
126+
t.storeReorderIdx, t.emitResourceCounts, t.emitJit, t.emitTargetCode,
127+
t.fullySpecialize, t.isLocalSimulator, t.argumentSynthChangeSemantics);
128+
129+
// Optional spin observable: include its string representation when present.
130+
if (t.pauliTermSplitObservable)
131+
hash_combine(seed, t.pauliTermSplitObservable->to_string());
132+
133+
return seed;
134+
}
135+
136+
std::size_t cudaq::CompileTarget::hash() const {
137+
return std::hash<cudaq::CompileTarget>{}(*this);
138+
}

python/cudaq/runtime/observe.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,11 @@ def __broadcastObserve(kernel, spin_operator, *args, shots_count=0, qpu_id=0):
3838
ctx.kernelName = kernel_name
3939
has_vector_args = isa_kernel_decorator(kernel) and any(
4040
hasattr(a, 'shape') and len(a.shape) == 2 for a in args)
41-
if has_vector_args:
42-
ctx.allowJitEngineCaching = True
43-
ctx.useParametricJit = True
4441
policy = cudaq_runtime.ObservePolicy(ctx, kernel_name, spin_operator)
4542
for i, a in enumerate(argSet):
4643
ctx.batchIteration = i
4744
results.append(
4845
cudaq_runtime.launch_observe(policy, ctx, lambda a=a: kernel(*a)))
49-
if has_vector_args:
50-
ctx.unset_jit_engine()
5146
return results
5247

5348

@@ -171,7 +166,6 @@ def __computeTermExpectation(term, observe_result):
171166
else:
172167
ctx = cudaq_runtime.ExecutionContext('observe', 0, qpu_id)
173168
ctx.setSpinOperator(localOp)
174-
ctx.allowJitEngineCaching = True
175169
if num_trajectories is not None:
176170
if noise_model is None:
177171
raise RuntimeError(
@@ -202,7 +196,6 @@ def __computeTermExpectation(term, observe_result):
202196
results.append(
203197
cudaq_runtime.ObserveResult(exp_val, op,
204198
observeResult.counts()))
205-
ctx.unset_jit_engine()
206199

207200
if noise_model != None:
208201
cudaq_runtime.unset_noise()

python/cudaq/runtime/sample.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ def sample(kernel,
166166
ctx = cudaq_runtime.ExecutionContext("sample", shots_count)
167167
ctx.kernelName = kernel_name
168168
ctx.explicitMeasurements = explicit_measurements
169-
ctx.allowJitEngineCaching = True
170169
policy = cudaq_runtime.SamplePolicy(ctx, kernel_name,
171170
explicit_measurements)
172171

@@ -194,7 +193,6 @@ def sample(kernel,
194193
"results when executed. Exiting shot loop to avoid infinite "
195194
"loop.")
196195
break
197-
ctx.unset_jit_engine()
198196
return counts
199197
finally:
200198
if set_noise_for_call:

python/runtime/common/py_ExecutionContext.cpp

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,8 @@ void bindExecutionContext(nanobind::module_ &mod) {
4242
&cudaq::ExecutionContext::numberTrajectories)
4343
.def_rw("explicitMeasurements",
4444
&cudaq::ExecutionContext::explicitMeasurements)
45-
.def_rw("allowJitEngineCaching",
46-
&cudaq::ExecutionContext::allowCompiledModuleCaching)
47-
.def_rw("useParametricJit", &cudaq::ExecutionContext::useParametricJit)
4845
.def_ro("invocationResultBuffer",
4946
&cudaq::ExecutionContext::invocationResultBuffer)
50-
.def("unset_jit_engine",
51-
[&](cudaq::ExecutionContext &execCtx) {
52-
if (execCtx.cachedCompiledModule) {
53-
execCtx.cachedCompiledModule = std::nullopt;
54-
execCtx.allowCompiledModuleCaching = false;
55-
}
56-
})
5747
.def("setSpinOperator",
5848
[](cudaq::ExecutionContext &ctx, cudaq::spin_op &spin) {
5949
ctx.spin = spin;

python/runtime/cudaq/algorithms/py_run.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ static detail::RunResultSpan
7373
pyRunTheKernel(const std::string &name, quantum_platform &platform,
7474
mlir::ModuleOp mod, CompiledModule *compiled,
7575
std::size_t shots_count, std::size_t qpu_id,
76-
OpaqueArguments &opaques, bool allowCaching) {
76+
OpaqueArguments &opaques) {
7777
if (!name.ends_with(".run"))
7878
throw std::runtime_error("`cudaq.run` only supports runnable kernels.");
7979
// Set the `run` attribute on the module to indicate this is a run context
@@ -101,7 +101,7 @@ pyRunTheKernel(const std::string &name, quantum_platform &platform,
101101
[[maybe_unused]] auto result =
102102
clean_launch_module(name, mod, opaques, compiled);
103103
},
104-
platform, name, name, shots_count, layoutInfo, qpu_id, allowCaching);
104+
platform, name, name, shots_count, layoutInfo, qpu_id);
105105

106106
return results;
107107
}
@@ -138,7 +138,7 @@ run_impl(const std::string &shortName, MlirModule module,
138138
{
139139
nanobind::gil_scoped_release release;
140140
span = pyRunTheKernel(shortName, platform, mod, compiled, shots_count,
141-
qpu_id, opaques, true);
141+
qpu_id, opaques);
142142
}
143143
auto results = pyReadResults(span, mod, shots_count, shortName);
144144

@@ -217,7 +217,7 @@ run_async_impl(const std::string &shortName, MlirModule module,
217217
platform.set_noise(&noise_model.value());
218218
try {
219219
auto span = pyRunTheKernel(name, platform, mod, nullptr,
220-
shots_count, qpu_id, opaques, false);
220+
shots_count, qpu_id, opaques);
221221
sp.set_value(span);
222222
ep.set_value("");
223223
} catch (std::runtime_error &e) {

python/runtime/cudaq/platform/py_alt_launch_kernel.cpp

Lines changed: 61 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
#include "mlir/Dialect/Func/IR/FuncOps.h"
4141
#include "mlir/ExecutionEngine/OptUtils.h"
4242
#include "mlir/IR/Builders.h"
43+
#include "mlir/IR/OperationSupport.h"
4344
#include "mlir/InitAllPasses.h"
4445
#include "mlir/Parser/Parser.h"
4546
#include "mlir/Target/LLVMIR/Dialect/LLVMIR/LLVMToLLVMIRTranslation.h"
@@ -649,68 +650,39 @@ static void appendTheResultValue(ModuleOp module, const std::string &name,
649650
runtimeArgs.emplace_back(buf, [](void *ptr) { std::free(ptr); });
650651
}
651652

652-
/// In a sample launch context, the (`JIT` compiled) CompiledModule may be
653-
/// cached so that it can be called many times in a loop without being
654-
/// recompiled. This exploits the fact that the arguments processed at the
655-
/// sample callsite are invariant by the definition of a `CUDA-Q` kernel.
656-
template <std::invocable F>
657-
requires std::is_invocable_r_v<cudaq::CompiledModule, F>
658-
static cudaq::CompiledModule with_compiled_module_cache(F &&f) {
659-
auto *currentExecCtx = cudaq::getExecutionContext();
660-
661-
auto getCache = [currentExecCtx]() -> std::optional<cudaq::CompiledModule> {
662-
if (currentExecCtx && currentExecCtx->allowCompiledModuleCaching)
663-
return currentExecCtx->cachedCompiledModule;
664-
return std::nullopt;
665-
};
666-
auto saveCache = [currentExecCtx](cudaq::CompiledModule compiled) {
667-
if (currentExecCtx && currentExecCtx->allowCompiledModuleCaching) {
668-
if (!currentExecCtx->cachedCompiledModule)
669-
currentExecCtx->cachedCompiledModule = compiled;
670-
}
671-
};
653+
/// Compute a hash of the IR given by the `ModuleOp`.
654+
static std::size_t hashModuleOp(ModuleOp mod) {
655+
llvm::hash_code h{0};
656+
mod.walk([&h](Operation *op) {
657+
h = llvm::hash_combine(h, OperationEquivalence::computeHash(op));
658+
});
659+
return static_cast<std::size_t>(h);
660+
}
672661

673-
auto cachedModule = getCache();
674-
if (cachedModule)
675-
return *cachedModule;
676-
auto compiled = f();
677-
saveCache(compiled);
678-
return compiled;
662+
/// Obtain a fresh `CompileTarget` for the current execution context.
663+
static std::unique_ptr<cudaq::CompileTarget> getCompileTargetImpl() {
664+
auto *ctx = cudaq::getExecutionContext();
665+
if (!ctx)
666+
return cudaq::get_compile_target(cudaq::other_policies{});
667+
668+
return cudaq::policies::withPolicy(ctx->name, [&](auto policy) {
669+
using Policy = std::decay_t<decltype(policy)>;
670+
if constexpr (std::is_same_v<Policy, cudaq::observe_policy>) {
671+
policy.spin = ctx->spin.value();
672+
}
673+
return cudaq::get_compile_target(policy);
674+
});
679675
}
680676

681677
static cudaq::CompiledModule
682678
compileModuleImpl(const std::string &name, ModuleOp mod,
683-
const std::vector<void *> &rawArgs, bool isEntryPoint) {
679+
const std::vector<void *> &rawArgs, bool isEntryPoint,
680+
std::unique_ptr<cudaq::CompileTarget> target = nullptr) {
681+
if (!target)
682+
target = getCompileTargetImpl();
684683
cudaq::SourceModule src{name, mod.getAsOpaquePointer()};
685-
686-
// Only cache on local simulators
687-
auto cacheable =
688-
cudaq::is_simulator_platform() && !cudaq::is_emulated_platform();
689-
690-
auto compile = [&]() {
691-
cudaq::CompiledModule compiled;
692-
auto *ctx = cudaq::getExecutionContext();
693-
if (!ctx) {
694-
auto target = cudaq::get_compile_target(cudaq::other_policies{});
695-
return cudaq_internal::compiler::compileModule(std::move(target), src,
696-
{rawArgs}, isEntryPoint);
697-
}
698-
699-
return cudaq::policies::withPolicy(ctx->name, [&](auto policy) {
700-
using Policy = std::decay_t<decltype(policy)>;
701-
if constexpr (std::is_same_v<Policy, cudaq::observe_policy>) {
702-
policy.spin = ctx->spin.value();
703-
}
704-
auto target = cudaq::get_compile_target(policy);
705-
return cudaq_internal::compiler::compileModule(std::move(target), src,
706-
{rawArgs}, isEntryPoint);
707-
});
708-
};
709-
710-
if (!cacheable) {
711-
return compile();
712-
}
713-
return with_compiled_module_cache(compile);
684+
return cudaq_internal::compiler::compileModule(std::move(target), src,
685+
{rawArgs}, isEntryPoint);
714686
}
715687

716688
// Launching the module \p mod will modify its content, such as by argument
@@ -720,38 +692,48 @@ static cudaq::KernelThunkResultType
720692
pyLaunchModule(const std::string &name, ModuleOp mod,
721693
cudaq::CompiledModule *cachedModule,
722694
const std::vector<void *> &rawArgs) {
723-
bool isCachable = [&]() {
724-
// Must have a slot to read/write the cache from. Callers opt out of the
725-
// cache by passing nullptr.
726-
if (!cachedModule)
727-
return false;
728-
auto &platform = cudaq::get_platform();
729-
// Must be local simulator
730-
if (!platform.is_simulator() || platform.is_emulated())
731-
return false;
695+
auto target = getCompileTargetImpl();
696+
auto targetHash = target->hash();
697+
698+
// We don't cache kernels that inline all arguments, as any change to the
699+
// runtime arguments would invalidate the cache. Currently, synthesis is
700+
// all-or-nothing, but if arg-by-arg synthesis is supported, then that will
701+
// need to be detected.
702+
bool cacheable = cachedModule && !target->fullySpecialize && targetHash != 0;
703+
704+
// Normally, we assume that the module IR is constant given the uniqued name.
705+
// However, kernels that capture other kernels inline captured kernels into
706+
// the module, so we need to handle this case specially.
707+
bool hasCaptures = [&]() {
732708
auto func = cudaq::getKernelFuncOp(mod, name);
733-
// TODO: currently, synthesis is all-or-nothing, but if arg-by-arg
734-
// synthesis is supported, then that will need to be detected
735-
if (cudaq::opt::factory::isFullySynthesized(func))
736-
return false;
737-
// Caching for kernels with lifted arguments is not currently supported.
738709
for (unsigned i = 0; i < func.getNumArguments(); ++i)
739710
if (func.getArgAttr(i, "quake.pylifted"))
740-
return false;
741-
return true;
711+
return true;
712+
return false;
742713
}();
743-
744-
// Cache hit only if the cached module's entry point matches this launch's.
745-
// Notably, run has a different entry point so can't share a cache with
746-
// other launch modes.
747-
if (isCachable && cachedModule->getName() == name)
714+
// Hash detects changes to callables as they have been merged into `mod`.
715+
std::size_t moduleHash = (cacheable && hasCaptures) ? hashModuleOp(mod) : 0;
716+
717+
// Cache hit: same kernel, same target configuration, same module content.
718+
if (cacheable && cachedModule->getName() == name &&
719+
cachedModule->getMetadata().targetHash == targetHash &&
720+
cachedModule->getMetadata().moduleHash == moduleHash) {
721+
CUDAQ_INFO("Reusing cached module with name {} and hash ({}, {})", name,
722+
targetHash, moduleHash);
748723
return cudaq::streamlinedLaunchModule(*cachedModule, rawArgs);
724+
}
749725

726+
CUDAQ_INFO("Compiling module {}", name);
750727
mlir::OwningOpRef<ModuleOp> clone = mod.clone();
751-
auto compiled = compileModuleImpl(name, clone.get(), rawArgs, true);
728+
auto compiled =
729+
compileModuleImpl(name, clone.get(), rawArgs, true, std::move(target));
752730
auto res = cudaq::streamlinedLaunchModule(compiled, rawArgs);
753-
if (isCachable)
731+
if (cacheable) {
732+
CUDAQ_INFO("Caching module {} with hash ({}, {})", name, targetHash,
733+
moduleHash);
734+
compiled.setCacheKey(targetHash, moduleHash);
754735
*cachedModule = std::move(compiled);
736+
}
755737
return res;
756738
}
757739

0 commit comments

Comments
 (0)