Julia debugging is extremely slow

Hmm, but line-by-line execution is pretty easy and robust in Julia?..

actually, it is the same as if a chess player would always ask StockFish, actually more than StockFish, a pre-2000 chess engine most probably, about the best next move. It is almost obvious than the ability of an experienced player will slowly deteriorate, and that a beginner will most probably never be able to play well by himself. Continuing with the analogy, if one wants to become better at chess, consulting engines is also beneficial, if done wisely. Certainly though all this reasoning is based on the fact that one wants/needs to stay/become good at chess.

Less abstractly, usually one is trading-off comprehension for speed of execution. Which is a fine trade-off sometimes, though you are still losing something in the exchange. The jump from assembly to fortran is not at all as big as from math/coding to vibing instructions, even if it can be useful.

Well, I like to think it is in a constant state of improvement.

VSCode and Debugger.jl are constantly adding new features. And we just launched the first JetBrains IDE plugin less than 3 (!) months ago with multiple releases every week. So lots of progress for such little time. If we can keep the energy up, I am sure we can improve the tooling a lot in the weeks and months to come. I’ll keep you posted :slight_smile:

10 Likes

Julia is indeed, and unfortunately, pretty rough around the tooling edges, but I wonder if you wouldn’t end up better off by exploring some alternative workflows and debugging techniques, rather than abandoning the attempt because it doesn’t support the exact workflow that you’re used to. I would generally still consider Julia an excellent choice for porting Fortran code. Yes, the Debugger isn’t in great shape, but conversely there are a lot of interactive workflows that Julia enables, that just aren’t possible in Fortran. What did you have in mind as “another language”?

I have looked at all the alternative workflows that people have posted here and to me they are simply not good enough. I abandoned porting this code from Fortran to Julia and ended up just porting it to python. In the last 2 days I was able to port the entire code to python and by running python debugger parallel to my Fortran debugger I could step through both codes simultaneously and fix any bugs I made, including subtle bugs that only showed up and thousands of iterations of glacier modelling. The codes are now producing identical results down to double precision floating point error.
I know that python is not ideal and it is indeed running about 50x slower at the moment than it was in Fortran, but this is also with a naive port full of for loops and without optimizing or compiling the sections that really need the speed.
So in Julia I spent 8 days working on this problem and got maybe 30% through actually being able to even run the code (I would estimate it would have taken med several weeks or even months to get this working in Julia), while in Python I was able to get the code running in about 4 hours, and after 3 days I had fully debugged the code and even found a memory error in the original Fortran code.

I realize that this is not a fair comparison since I have been programming pretty exclusively in Python since I abandoned it 6 years ago so I am of course more familiar with it, but to me it is the same experience I had when I last abandoned it. Back then I was equally proficient in both language, and I ended up abandoning Julia because my development in Julia was about 3-10x slower than it was in Python.

I find the frequent recommendations of debugging alternatives in these conversations to be annoying distractions, though I acknowledge people here aren’t dismissing (source-level) debuggers outright. I think people already understand debuggers can’t catch all bugs, and some bugs are ideally prevented and better caught by other ways. Nobody even wants to step over every line, especially when code scales.

I do want to address the dismissal of debuggers I see in other forums; Daniel Lemire’s blogpost collects several detractions and can serve as a starting point. The nuance of these arguments tends to be lost through chains of forums and blogs, but you don’t have to dig far to find heated debates and contextual limitations. For example, one argument is that missing methods and variables are best caught by linters and compilers, not later at runtime. While true, that assumes the language even guarantees that static analysis, which Julia does not; the only MethodError that can be caught for a dynamic dispatch ahead of time is if there are 0 methods with the matching arity. Julia can’t even (and shouldn’t) guarantee a fallback method for every call like virtual methods in OOP classes.

Debuggers appear to be more beneficial to Julia than most other languages. When the summaries of JET.jl aren’t enough, Cthulhu.jl interactively steps into calls’ type inference, like a recursive @code_warntype, because we don’t have the luxury of one-method functions and declared return types. When it runs into the wall of a dynamic dispatch or errant runtime values we don’t have any leads on (it’s easy to say “don’t write code you don’t understand” but it’s not often our own code), then we have to step into calls at runtime, which is what debuggers do. Inserting print statements and errors as part of iterative method development isn’t a replacement because there is a possible combinatorial explosion of multiple methods per callee to inspect. The REPL isn’t a replacement because we still have to search for methods and collect expressions to evaluate in the REPL (as well as adjusting variable declarations for the global scope); a debugger actually saves a lot of work there. This is probably why Julia users aren’t as dismissive of debuggers as a concept.

5 Likes

Just curious, why port to Python for more debugging instead of wrapping the faster and debugged FORTRAN in a Python API? I’m not familiar with FORTRAN so I don’t know how different debugging there is different from Python.

I use jupyter for that kind of debugging: Much more convenient copy-paste / editing experience than the REPL.

In my dayjob, I develop in scala/java, using intellij idea. The debuggers for JVM languages are technically excellent, and very convenient for things where you have already localized the bug.

However, I find that REPL-style (actually jupyter-style) debugging is much more convenient if you need to set up and inspect complex state: Complex states cannot be understood from “inspect in debugger”, instead you need to write custom debug code; and that tends to be inconvenient in the intellij / JVM world, and you also need to debug your debug code.

(oh, the digraph implied by xyz relations is supposed to be acyclic. You’re not gonna eye-ball acyclicity of a graph with 100k nodes. Oh, the internal fields needed to look at that graph are all private and in a dependency… yeah, sure, I can ignore that in JVM, but it is damn inconvenient boilerplate)

In your case, I think I would try to do the porting “from the outer layers”: Write a small julia program / “port” that is correct and simply uses “ccall” to call the various fortran functions, but uses e.g. CSV.jl instead of whatever your fortran program uses. Then, piece by piece, port functions from fortran to julia, using the “oh, I have two implementations of this function; first call the fortran version, then the julia version, compare results and maybe error” technique.

That may require a little refactoring of the fortran code, but I would almost count that as a win (i.e. along the way, you turn a creaking fortran CLI tool into an actually usable library, plua julia wrappers, plus a pure julia implementation which you continuously test and benchmark against the fortran lib).

I would use the same approach for a python port/wrap, only that for python, the wrapper is the primary end-product and the pure python implementation is “for educational purposes only”, due to performance.

1 Like

Just curious, why port to Python for more debugging instead of wrapping the faster and debugged FORTRAN in a Python API? I’m not familiar with FORTRAN so I don’t know how different debugging there is different from Python.

The Fortran code is from an old professor, but has been used in lots of papers and scientific work. The code is pretty rough and basic. So the first step in bringing this code into a bigger glacier modelling framework is to port it from Fortran into something more modern that I and future researchers can more easily work with such that the code can be properly understood, refactored and then improved and implemented in more modern glacier models. So now the code has been ported to python and it has already paid off in the fact that I found a quite severe memory bug in the original code. Next is actually understanding what the code does and how it can be rewritten and improved :slight_smile:

1 Like

However, I find that REPL-style (actually jupyter-style) debugging is much more convenient if you need to set up and inspect complex state: Complex states cannot be understood from “inspect in debugger”, instead you need to write custom debug code; and that tends to be inconvenient in the intellij / JVM world, and you also need to debug your debug code.

I must admit I do not understand your reasoning.
I completely understand that having a debugger does not mitigate bad code practices or poorly written code, but I do not really think that is the argument you are trying to make either. (Though I have certainly heard that argument many times when it comes to why people prefer the REPL and I do not think that is correct.)
I cannot see how you are arguing that the REPL makes debugging anymore convenient than an actual debugger. Surely anything you could do with REPL debugging you could also do with an actual debugger, usually you just do not have to.

More like how manual laborers felt when machines took over their jobs.

Except then there was at least a path to being retrained in cognitive or creative labor.

1 Like

I gave the actual example: Something broke, and you suspect that the breakage is due to some internal temporary state/graph having cycles where no cycles should exist.

In a REPL workflow, you extract / exfiltrate the relevant internal state. Then you whip up a small function to inspect it (i.e. to check for cyclicity). Crashes and burns when applied to the exfiltrated internal state. You fix your small function. Try again. A handful of iterations later you figured out that yes, there is a cycle. So where was the cycle created?

Depending on complexity, you can retrace the steps leading there in the REPL, calling “checkForCycles” after each one, or you throw your REPL session away, extract the “cycle-checker” into an actual code file. You edit code by inserting “checkForCycles” calls at strategic locations, bisecting into the moment where the cycle actually is created, and ta-da, bug is found.

For this kind of bug / debugging, a debugger is not very helpful. What you need instead is a REPL, and the ability to quickly whip up dirty debug code that deals with various internal temporary states.

Yes, you can “evaluate expressions” in intellij or gdb. But the user experience of writing a 50 line graph traversal with helper classes and lots of imports in the little “evaluate expression” box in intellij is miserable! This is not an appropriate replacement for “world-is-my-oyster jupyter”.

Crucial is that the internal state persists, while you write more dirty debug functions to handle it. IOW, the essential part is “hot loading of code without restarting your program”, plus an environment that supports “scripting-style” quick & dirty code without insisting on extensive ceremony.

Without that, the entire thing degrades to “println”-debugging, where you recompile and re-run your program in each mental step. This also has its place and is the preferable approach for some kinds of bugs, but the turn-around becomes much slower, and this requires a lot of reproducibility that you may not have, especially if your program interacts with the outside world via a network. The debugging experience then ultimately becomes: Log a bunch of diagnostics, and have tooling / a REPL and small library of helper functions to inspect logs and especially differences between logs.

Now, REPLs exist for JVM languages as well. Scala has quite good REPLs (no good notebook, though :frowning: )! I use them all the time. And they are imo more important for debugging complex issues than the actual debugger.

+REPL +debugger is of course better than REPL and no debugger. The lack of a good debugger in julia sucks. But I prefer (+REPL, -debugger) over (-REPL, +debugger).

1 Like

Yes, you can “evaluate expressions” in intellij or gdb. But the user experience of writing a 50 line graph traversal with helper classes and lots of imports in the little “evaluate expression” box in intellij is miserable! This is not an appropriate replacement for “world-is-my-oyster jupyter”.

I think this is where the disconnect is. If the evaluate expression was all I had in a proper debugger then yes this would be miserable…
However when I debug things In python I have the full console available and I can evaluate and insert anything and it changes my state. So this is what I mean when I say that anything you can do in the REPL I should be able to do in my debugger as well.
Basically the debugger that I am used to is the kind that contain all the functionality of a REPL and more.

1 Like

The debugger in VSC has this functionality … when it works acceptably.

One alternative might be just to modernize the Fortran code. Modern Fortran is quite modern. You even use it interactively (https://lfortran.org/).

4 Likes

One alternative might be just to modernize the Fortran code. Modern Fortran is quite modern. You even use it interactively (https://lfortran.org/).

That is not a bad suggestion, and I am quite proficient in Fortran and coded my entire PhD in it (Though that is 10 years ago). However, the point in this project is to make this code more accessible such that future scientist can easily use this code and modify it to their needs.
Having a code written in Fortran is simply not very accessible in modern Earth science, so it needs to be something else.

Fortran is a rather easy language, for using it as a backend for python libraries I think it is very accessible to programmers in other compiled (C, C++, Rust…) languages. If, on the other side, you need it to be REPL-style accessible, and performant, than unfortunately (for you) the best is Julia…

2 Likes

I bet Claude would translate your Python version back to Julia with no issues.

I bet it would do an even better job at translating the original Fortran to Julia, given that Julia is semantically close to Fortran (array types, one-based, column-major), while Python is not

6 Likes

I think this is a “pick your poison” situation. It’s obvious why you considered Julia for this, but Julia is less popular than Fortran so that’s already a point against accessibility. Fortran’s age actually helps in another way; being more established often means more reliable funding, no matter how niche. But if you want more people to be able to pick up and develop this project in the near future, it’s pretty much Python wrapping now. There is no end to new compiled languages and libraries, and end-users still demand they be compiled and wrapped in Python. As much as everyone recognizes the burdens and limitations of maintaining optimized backends in different languages with different tooling, the two-language problem is still winning.

Although Julia “solves” the two-language problem as an interactively compiled language (and it shouldn’t be surprising that people here currently consider Julia the best option), the two-language problem in some ways solves some Julia problems. Julia’s JIT/pre-compilation latency is a much more frequent complaint than debugging, and the relative convenience of quickly downloading AOT-compiled backends that are compatible with various environments is helped a lot by the isolation from a separate language and its dependencies. There are some ideas on sharing precompile caches, but it would be more complicated and limited within one language. Back to the context of Julia’s debugging, the use of compiled mode was discussed earlier as a way to optimize stepping over calls we don’t need to debug e.g. reading files. One language’s debugger is fundamentally incapable of stepping into another backend language, so stepping over an optimized backend naturally exists in languages like Python that embrace wrapping compiled units. Julia’s debuggers also can’t step into the C core and jlls, but that’s not much of a convenience when we’re spending most of our time optimizing Julia.

I’m not familiar with your project so I obviously can’t know what the best option is. However, it won’t be easy or straightforward to cover that 50x performance gap in your Python port. Since accessibility is your goal, you’ll very likely rewrite the for-loop-heavy code to use established Python libraries that wrap other compiled languages, and it’s very uncertain if that’ll beat the Fortran code as is, let alone with improvements and a Python wrapper. Since the project is of a scale where a port took only a couple days, I think anything is worth a shot.

The biggest problem is when there are issues (I’m noticing that hallucinations that break code with immediate runtime errors are increasingly being released by open-source projects), someone still has to know the tooling and deal with the shortcomings. Even if we’re fortunate enough to immediately generate a working port validated by perfect tests, the next person working on the project may not be so lucky, and making people use tools we don’t like is the opposite of accessibility.

I also find AI ports to be underwhelming; a prompt with a relatively unconstrained idea can often generate better results than a prompt to port provided source code. Besides the hallucinations being much worse for relatively less popular languages, there’s a tendency to be unidiomatically straightforward. Fortran → Julia might have fewer issues than Fortran → Python or Python → Julia, but it’ll take some work on our part to use any language to its fullest.

4 Likes

As a curious user, how hard is it to develop an “industry strength” debugger for Julia which runs fast on compiled code? Is the challenge basically precisely mapping Julia source code lines to the emitted LLVM code without getting confused by the several processing steps like syntax lowering? Or it something that’s trivial in principle but time-consuming to implement?