Hacker News new | ask | show | jobs
Show HN: Lucen a Python compiler that parallelizes for-loops via comment pragmas (github.com)
10 points by soumik15630m 11 days ago
5 comments

Not very Pythonic

  for i in range(len(records)):
    scores[i] = score(records[i])
Better

  for i, record in enumerate(records):
    scores[i] = score(record)
Best

  scores = [score(record) for record in records]
Raymond Hettinger (Python core dev): https://gist.github.com/bespokoid/205efd91546ddb16b210678830...
Sure those are good designs... But Lucen is not about "here's a nicer way to write loops," it's "you shouldn't have to touch the loop you already have."..... showing Lucen parallelize the ugly version is the point.
I get it, but I'd never write it like that.

Does it support list comprehensions, etc?

not currently - currently lucen runs on marked for statement loop but yeah it's a cleaner one to add ...just not the surface i started with
I think I’m a little skeptical.

#pragma is a real keyword in C but # is just a comment in python - why not use a pythonic @decorator?

Vendoring a library like this into an app seems like a lot more toil than just writing the native multiprocessing python code, or using something built for number crunching like arrayfire or numpy

Because (even if you could decorate for-loops, or instead decorated functions containing a loop and nothing else) the resulting behavior would be completely unlike the behavior of all other @decorators in Python. It would also be nearly impossible to implement this functionality as a decorator without introducing tons of fragility.

The preprocessor/import-hook hack that allows macro-like behavior in Python does arbitrary transforms at the text/code-load level, while decorators are evaluated at runtime and run regular Python code. Decorators can be composed/wrapped by other functions and decorators, whereas implementing Lucen-like functionality with a decorator would necessarily match on the name/symbol of the decorator and couldn't indirect "through" it.

Heck, even if you disallowed wrapping and matched on the decorator name during preprocessing (basically stripping it out and rewriting decorated code strings at load time), even identifying the correct decorator as a transformation target would require reimplementing a ton of Python's import/scope/name resolution behavior by hand at code-transformation time. If your decorator was called lucen.parallelize(), consider the difference between "import lucen; @lucen.parallelize", "from lucen import parallelize; @parallelize", "from lucen import parallelize as pl; parallelize = 123; @pl", and so on. You'd have to handle all of those cases, and many more (e.g. decorated functions defined inside other classes/functions) at the code-as-string or AST level. You couldn't run the decorator at runtime, because a) syntactic information is gone by then, and b) because then you'd need to unimport/reimport module code which had already been run with arbitrary global side effects.

More information on the methods available to get preprocessor-like functionality in Python:

Custom source encoding text transformers can be loaded via .pth files: https://pydong.org/posts/PythonsPreprocessor/

Import hooks can be configured at runtime, and run during subsequent import statements. They were originally designed to allow customized module discovery, but nothing stops you from using them like Lucen does: to intercept imports of local Python files and transform/replace the code imported at load time: https://peps.python.org/pep-0302/

You can @decorate a for-loop?

I wish there were easier ways to play with syntax in Python, but there we are.

Though I guess using @decorator semantics would make it look a lot like python-numba syntax
just wanted to make sure ... even if someone someday wishes to revert the decision of using lucen they will be okay with just removing the import hooks - and run everything as native -- and when they have time, they can clear the comments but till then they can work without thinking
This code seems to be fairly pervasively AI-written (both structurally/syntacticly, with many additional AI verbosity tells in the documentation, and going by the commit history).

That's a problem for me, given what this library does: arbitrary transformations of Python code at load time. The fragility, security risk, and complexity added by basically writing a subdialect of Python with new semantics is something I'd like to see more human attention on and maturity of before I consider using it. There are times when AI smell doesn't concern me much when deciding whether or not to use someone's code. This isn't one of those times.

Sure man thanks for the critique, genuinely....

To be precise..the core concurrency shapes, disjoint-chunk commit ordering and DAG-level wavefront scheduling, are model-checked (SequentialEquivalence; DependencySafety + Termination), continuously re-verified in CI via a second independent executable encoding. The TLA+ file itself is bounded to a small instance and a 2-chunk model... the paired Python checker checks all contiguous chunk counts and all commit-order permutations for n up to 6.

Conflict detection isn't in the TLA+ file, it's verified separately .. unit and integration tests exercise the real dispatch code against an independent golden sequential run, covering duplicate-key writes, unresolved-index writes under an explicit "depend=none" assertion, and all three error modes (report/quiet/hard), plus a real module-import-level integration test and a concurrency stress test hammering the same conflict path across 10 threads x 6 repeats to catch anything nondeterministic.

Also a note: this is v1.1. The rigor was front-loaded by me, and not something that evolved over years ...so there can be a chance of more rigor or checks...currently working on that.

If you find a bug or a security issue, please open one, genuinely want the feedback.

I do appreciate the rigor that was included, and please don't take my comment above as saying "this is a bad project"; rather, it's not a project I personally am comfortable using at this time due to the combination of its purpose, maturity, and development methodology.

Formal verification is great, and I wish more authors included TLA+. However, changes to another platform's semantics are difficult to formally verify, since the scope of things that could affect those semantics is largely outside of the proof area for most specs. Put another way: you can verify the structures and behavior of input code you specify, but it's difficult (especially in a language with as much dynamism and action-at-a-distance as Python) to verify interactions with others' contexts.

Off the top of my head, some things that users might run into that the testing system doesn't yet take into account include: signal handling/interrupts, orphaned-thread capture/detection/reporting/killing when Lucen-rewritten pariters crash or are aborted (e.g. due to OOM), destructors that might end up being run in unexpected thread/process contexts (given the scope changes in the generated Python code), and potentially unexpected/extra-iteration behavior that occurs due to retries when dispatch to the executor fails partway. That last one might well be handled already via transactionality.

Some things that I think might already be handled, but wasn't quite sure of upon a cursory reading of the code include: operator overloading on user-provided things being accumulated/iterated over, user redefinition of builtin function or exception types (e.g. "def enumerate()"), and making sure the parallel executors are "eagerly" spawned (it sucks to find out mid-execution that you've hit a max-threads or max-processes limit).

Separately and entirely subjectively, a few improvements that might be nice (which I can file as feature requests if you'd like, but don't want to drive-by a bunch of stuff that's entirely personal preference):

I think I'd recommend using only threads for the backend unconditionally. ProcessPoolExecutor brings a lot of extra considerations, overhead, and risk that I think might both throw off your profitability analysis and bring more trouble than it's worth over the long term.

Additionally (though this is high-effort), I think it might be a usability improvement to use a codec for codegen rather than an import hook. Lots of big Python projects have a fairly difficult-to-control import order (because of e.g. frameworks/preloaders/multiple entry points). Unimporting/reimporting stuff is a struggle when it comes to locally activating Lucen on a per-file basis, and in those environments it may be a bit of a usability hassle to activate it centrally. Using a codec gives you per-Lucen-able file locality pretty well.

Lastly, it would also be a potential usability win to provide a real decorator or context manager which enables (in preprocessed files) or disables (nooping unless in preprocessed files) Lucen. That could be in addition to the magic comment, or instead of it (via some cheeky hack like failing to parse all syntax that doesn't refer to the fully qualified 'lucen.mydecorator' name, so you don't have to deal with full name resolution). I think such a decorator would be fairly simple to implement via a contextvar flag in the decorator which the generated code checks, and falls back to the original Python if it's unset, though that would expand the volume of generated code.

Interesting to see the rust code is doing the actual parallelization, while execution. Will it work for complex loops, and what happens if the loops uses global context and different variables, outside of the loop?. May be it should go to python standard library.
This should be part of the Standard Library if so efficient.