Here are a couple of examples of what names numba generates for this or that compile result :
from numba import float64, int64, njit
from numba.core.registry import CPUDispatcher
from numba.core.typing.templates import Signature
euler_s = float64(float64)
fines_s = int64(int64)
@njit(euler_s)
def euler(precision):
return 2.712 + precision
@njit(fines_s)
def fines(shift):
return 137 + shift
def get_symbol_name(func: CPUDispatcher | callable, sig: Signature):
cres = func.get_compile_result(sig)
llvm_sym_name = cres.fndesc.llvm_cfunc_wrapper_name
return llvm_sym_name
if __name__ == "__main__":
# mangled name pattern: cfunc._Z...<module><name>v<uid><abi tags><args types>
print(get_symbol_name(euler, euler_s)) # cfunc._ZN8__main__5eulerB2v1B38c8tJTIeFIjxB2IKSgI4CrvQClQZ6FczSBAA_3dEd
print(get_symbol_name(fines, fines_s)) # cfunc._ZN8__main__5finesB2v2B38c8tJTIeFIjxB2IKSgI4CrvQClQZ6FczSBAA_3dEx
Notice that I have two jitted functions here, and accordingly the mangled names for the corresponding overloads acquire process-specific v ID bits, see …eulerB2v1B38… and …finesB2v2B38… where ID is an integer which is counting functions that are jitted in this process.
What purpose do these v ID bits serve? Isn’t the mangled name without them sufficient to uniquely identify the function?
In our project, we were going to use `llvm_cfunc_wrapper_name`` as it is generated by numba, but we can’t, because of this process-specific contamination of this name. When we have multiple parallel processes, we run into the situation where caller invokes one name but callee is compiled and cached in a different process with another name - distinct only in that v bit.
We address this by cooking up our own names and registering them in LLVM symbol table. But it would be nice not to add another entry for the same function value, provided a predictable key to retrieve that function from the table existed.
I think to disentangle same function names. I actually have the issue that it’s actually not unique in fork: Compile error when loading code cached by a fork · Issue #10486 · numba/numba · GitHub
Sorry if talking about something else, pattern matched it from skimming
Thanks for the comment. It’s interesting - because we seem to be implying the opposite status of this counter-generated function ID: I am questioning whether it’s needed at all to disambiguate the already unique mangled names, and you are observing that it is actually not adequate enough for the task of disambiguation!
To the latter’s end, I looked briefly at your issue report. In your make_cached_op can you bake the content / code text / etc. of the wrapped function into the wrapping function that you return? Otherwise, as you also point out, regardless of the implementation you wrap, the wrapping jitted function will have the same name. Declaring more that one return type on the same function seems to be the essence of that problem, quite unrelated to other specifics of your setup, which possibly is not even intended to be cured by unique IDs (even if they continued being unique across multiple processes). Please correct me if I am misunderstanding.
Yes, that’s how I read it too
We do that for the outer functions we actually cache (there’s a context based hash we create), but it would be a lot of work to uniquely name every intermediate helper those functions themselves use internally (recursing into every inner function and so on). The problem is not just one level deep.
And no way I can think of for controlling external code provided by users of the package. We can only control / rename the final thing they return to us.
Right, I do get it that if a function A calls a function B (and so on), then for the purpose of the A’s (or its wrapper’s) hashed name technically we need to hash in all the details of the called functions (B,…).
However, I am not sure I quite see how this is pertinent to the discussion of the counted ID (and its contribution to the LLVM symbol name) not being consistent across processes.
In other words, even if the counted ID was not inconsistent across processes, the essence of the called functions B,… would still not be captured in the context of the reproducer you provided, no?
The issue I was getting was not with function A (I know exactly what makes this function unique), it was with internal function B (which I may not even know – or care – exists), having the same name across different versions of function A. So I have non-mangled A1, A2, …, but each produce a mangled function B that is actually incompatible.
The ID in my purpose is just to give unique name to those function B. And my problem is multi process specifically. In process 1, numba finds function B and gives it a unique id, but in process 2 in finds another function B and reuses the same id, because the counter starts fresh in every process. It’s too much trouble for me to make each function B have unique name, I already did all the work to say whether A1 == A2 or not, and I only asked numba to cache and disambiguate those.
If you removed the unique_id, I would see this problem even within process.
I should say I am not super deep in what’s going on here with the unique_ids, as I prefaced on that issue I may be going out of the supported functionality of numba.
I would be interested in seeing a reproducer (a sketch thereof; I can assume / arrange the uID ripped out of numba’s internals) of a one-process problem here, if you get a chance to put one together.
Apologies for Claude over-verbosity as always, but is this what you wanted?
Here it is, does this speak to your question?
"""Remove numba's process-local uid from the mangled name: two distinct closures
collide onto one symbol -> wrong result, in a single process."""
import numba
from numba.core import funcdesc, itanium_mangler
def mangler_without_uid(name, argtypes, *, abi_tags=(), uid=None):
return itanium_mangler.mangle(name, argtypes, abi_tags=abi_tags, uid=None)
funcdesc.default_mangler = mangler_without_uid
def make_adder(k):
@numba.njit
def fn(x):
return x + k
return fn
add5 = make_adder(5)
add100 = make_adder(100)
# Each dispatcher JITs into its own module, so on its own it works fine.
assert add5(0) == 5
assert add100(0) == 100
@numba.njit
def driver(x):
# referencing both pulls the two colliding symbols into one module
return add5(x) * 1000 + add100(x)
assert driver(0) == 5100, f"MISCOMPILE: got {driver(0)}, expected 5100"
Nice, thanks. I see how this reproduces your observation.
TBH, I think I wouldn’t necessarily even (if I didn’t know about unique-id getting incorporated into the mangled symbol name) expect numba to disambiguate the factory-made function distinct in the baked-in parameter `k` in this case…
So this is just how I see it, but if I forget about programming aspect of this exercise for a second, what we essentially have here is manufacturing two functions which secretly depend on different parameters with no notational indication of this parameter dependence, no?
Just mathematically, we would usually have something like “f_k(x)”, I suppose, so the function’s identity is parameterized with the specific value of “k”. Going back to the programming aspect of it, having the value of “k” hashed into the manufactured name of the function appears to be a direct counterpart of the “_k” index.