Show HN: Python at the Speed of Rust (blog.fxn.ai)
I’ve been very interested in solving this problem with a great developer experience. Over time, I gradually realized that the highest-impact thing to have was a way to go from existing Python code to a self-contained native binary—in other words, a Python compiler.
I was already pretty familiar with a successful attempt: when Apple introduced armv8 on the iPhone 5s, they quickly mandated 64-bit support for all apps. Unity—where I had been programming since I was 11—kinda got f*cked because they used Mono to run developers’ C# code, and Mono didn’t support 64-bit ARM. Unity ended up building IL2CPP, which transpiles the C# intermediate language into C++, then cross-compiles it. Till date, this is perhaps the most amazing technical feat Unity has achieved imo.
I set out to build something similar, but this time starting from Python. It’s a pretty difficult problem, given the dynamic nature of the language. The key unlock was the PyTorch 2.0 release, where they pioneered the use of symbolic tracing to power `torch.compile`. In a nutshell, they register a callback with the Python interpreter (using CPython’s frame evaluation API), run a function with fake inputs, and record an IR graph of everything that happened in the function.
Once you have an IR graph, you can lower it to C++/Rust code, operation-by-operation, by propagating type information throughout the program (see the blog post for an example). And now is the perfect time to have this infrastructure, because LLMs can do all the hard work of writing and validating the required operations in native code.
Anyway, I wanted to share the proof-of-concept and gather feedback. Using Function is pretty simple, just decorate a module-level function with `@compile` then use the CLI to compile it: `fxn compile module.py` .
TL;DR: Get Rust performance without having to learn Rust ;)
40 comments
[ 4.4 ms ] story [ 83.3 ms ] threadMore fundamental than that is that Mojo has somewhat of a built-in assumption (I'm a bit more skeptical) that they can outperform silicon OEMs like Nvidia using MLIR (Mojo is designed as a front-end to MLIR). We have a much more conservative view: we'll rely on silicon OEMs to give us libraries we can use to accelerate things like inference; and we'll provide devs the ability to inspect and modify the generated code (C++ atm, Rust soon).
TL;DR: Devs don't have to learn anything with Function. Devs have to learn Mojo and MLIR to use Modular's offerings properly.
Mojo has a cold-start problem here, cos there isn't enough Mojo code in the wild for LLMs to be great at writing it in volume (compare this to C++ and Rust).
For language coverage, Function currently doesn't support classes (on the roadmap) or lambda expressions (much harder). But these are the main limitations.
For library coverage, this is where things get incredibly hairy. If a library isn't pure Python (i.e. most of the important libs), we can't compile them. We instead have to reimplement the library's functionality. For now, we plan on using LLMs to automate this as much as possible.
If the library is written in C, like Numpy, can't you just link against it?
First, Function is designed to be truly cross-platform but libraries like Numpy aren't compiled for say WebAssembly.
Second, the native libraries are usually built around CPython interop (i.e. the C API expects to interact with the CPython interpreter). Function does not (and will never) have a CPython interpreter (we generate full AOT compiled code).
It's possible to hack Function to perform a direct call and avoid (most of) the overhead, but in 99.99% of use cases this won't matter cos most processing time will be spent in the body of the function, not the scaffolding that Function uses.
You don't have to take my word for it: you can pull the C++ source code that we generate from a given Python function and inspect it yourself: https://docs.fxn.ai/predictors/compiler#compiling-binaries
The introduction could benefit from discussing a less toyish problem.
Beyond that, we also compile the generated native code in the cloud so that devs don't have to have a cross-compiler installed on their system (i.e. Clang, Xcode, MSVC, and so on).
If I start pulling in Numpy, igraph, scikit, etc, will it still work?
The article mentions Numpy as a gang up when trying to use Python across multiple systems, but it doesn’t address whether Function deals with this.
Looks cool, regardless!
We want devs to find or develop inference code (always in Python); decorate it with Function's `@compile`; compile it; and run natively in their Android, iOS, macOS, Linux, WebAssembly, or Windows applications.
We won't be supporting other use cases like web servers (no Django or FastAPI).
Am I missing something or has that been possible for at least 20 years?
Writing programming languages before AI was bit of a daunting task ; now it's way easier to grasp a first good principles and dive trough ; would still take time to get something production ready ; but that's definitely something that could happen
The majority of the innovation here is in building enough rails (specifically around lowering Python's language features to native code) so that LLM codegen can help you transform any Python code into equivalent native code (C++ and Rust in our case).
"What if we could compile Python into raw native code *without having a Python interpreter*?"
The key distinguishing feature of this compiler is being able to make standalone, cross-platform native binaries from Python code. Numba will fallback to using the Python interpreter for code that it can't jit.
At opt-level 2 and 3 rust completely optimizes away the interior loop, leaving just two instructions (plus a check for if n=0). given that your graph isnt showing O(1) time complexity shows you either
1) didn't compile your rust code in release mode for benchmarking, as the release profile uses opt-level=3 or 2) intentionally downgraded the opt-level of the release profile in order to run these tests
I'd be interested to see a more proper example, one where the task itself has some non-zero time complexity. Show a quicksort implementation perhaps. But as it stands this is just marketing smoke and mirrors.
I've clarified that this is not designed to be a rigorous benchmark. We've got rigorous benchmarks coming for image processing and CNN inference. I'll reply with the image processing example benchmark this week.
We're not translating Python directly to LLVM IR (I think I've seen other projects do this). We translate Python to C++/Rust first, where we have rigorous unit tests for every operation we support translating. We then use LLVM for downstream compilation to object code.
Here's some more context: https://docs.fxn.ai/predictors/compiler
We're also not competing with LLMs at all--we use LLMs for said conversion (under strict verification requirements).
What happens when you use @compile on a function that calls into stdlib directly or indirectly? How do you deal with existing extensions, GIL etc?
If you accept that 100% of legal python is not accepted, you have to write down exactly what is accepted.
Example: https://github.com/py2many/py2many/blob/main/doc/langspec.md
With this in place, when we trace through a dev's function, a given function call is considered a leaf node unless the function's containing module is in `trace_modules`. This covers the Python stdlib.
We then take all leaf nodes, lookup their equivalent implementation in native code (written and validated by us), and use that native implementation for codegen.
We don't interact with the GIL. And we keep track of what is unsupported so far: https://docs.fxn.ai/predictors/requirements#language-coverag...
But beyond that, our goal is meeting devs where they are. This means that beyond just compiling Python code, we provide SDKs for different frameworks (JavaScript, Kotlin, Swift, React Native, Unity, etc) that devs can use to run these functions within their applications, in as little as two lines of code.
We're very (very) focused on developers shipping products that use Function. We're already embedded in web apps, apps on the App Store, Play Store, and other places.
I do that by using the Python C-API, no rust, no fuss...
Thinking about it, I haven't tried to see how well the robots do with wrapping C libraries -- though I usually use pybindgen to generate the initial py-module then fiddle with the code manually.
Other than that lisp implementation has anyone used the python api which lets you register a module as the 'compiler' for external file types? I forget what it's actually name is.
The way I like to think about this is how much more code got written when "high level" languages like C came onto the scene, at a time when Assembly was the default. Writing Python is way (way way) easier and faster than any of the lower-level languages--no pointers, no borrow checker!
See docs: https://docs.fxn.ai/predictors/requirements#floating-point-v...