The Accelerating Adoption of Julia
lwn.net
lwn.net
We had a BoF at this year's JuliaCon revolving around this topic [1] and are now planning the first Annual Industry Julia Users Contributhon as a result.
I especially think that well-configured Julia + K8s setups have the capacity to really tighten exploratory data science <-> operational data engineering feedback loops in industrial settings in a way that is much more ergonomic, generically useful, and portable/extensible than using pre-baked frameworks to achieve something similar. Julia-centric tooling for coarse-grained workflow orchestration, experiment tracking, data provenance, etc. would be nice, though I also think existing generic tools in this vein (e.g. Argo) could probably compose well too :)
A few different companies have nice in-house implementations of these kinds of setups, and Julia Computing is building a nice looking commercial product suite in this vein that I look forward to exploring more in the future (especially JuliaHub and JuliaRun).
[1] https://julialang.org/blog/2020/09/juliacon-2020-open-source...
The implicitly parallel fork-join model is easy to program and incredibly general. And I'm glad to see a high-level language embrace it.
-------
I probably should note some other languages of this model: CUDA, OpenCL, ISPC, OpenMP, OpenACC. Most of these other languages are low-level, where you manage memory directly (and GPUs have very little memory per thread. So manual memory management is still hugely important)
But for speed of development, prototyping, and higher level reasons, a language like Julia that implements this "mindset" is going to be hugely important moving forward.
------
The fact that parallel fork-join scales from 2 CPU threads all the way up to thousands of GPU-threads... or CPU + SIMD (such as AVX512), is proof that this methodology is useful. I feel like people are sleeping on this model: its hugely useful for scaling on what I believe to be the computer of the future.
The fork-join model generally starts off with one-thread, often called "The Single" or "The Master". Its where main() begins.
But somewhere along the line, you discover a portion of code that needs to be multithreaded. So you fork into many threads: maybe dozens of threads, or thousands in the case of SIMD or GPUs. This multithreaded portion is then "joined", that is, the "master" thread refuses to continue until all children are done executing.
Lets say there's a raytracer you are writing.
main() {
// In the "Single" thread
Foo();
Bar();
fork(doRayCastingInParallel(), 5-million); // Spawns 5,000,000-threads, one for each ray
// Implicitly wait for all 5-million threads to finish
doSomethingElse();
}
Now, raycasting has a bunch of things to do, but you can break it up into the same steps for all 5-million threads. doRayCastingInParallel(int myIdx){
doTrigonometry(ray[myIdx]);
findCollision(ray[myIdx]);
outputColor[myIdx] = calculateColors(ray[myIdx]);
}
So from both the "single" thread, as well as your "child" thread, it feels like you're writing single-threaded code. But in actuality, your "child" thread is a 5-million spawned copy, executing in gross parallelism.The "Fork-Join" model assigns a different "myIdx" number to each thread. So Ray#500 knows to do things differently than Ray#68121.
But otherwise, they run through the same code.
---------------
The important bit here is how general this mindset is. Its commonly used for GPU-programming (CPU issues a GPU command to spawn millions of threads often one-per-pixel, or even thousands per pixel). Then the CPU waits for the GPU threads to finish, and then just carries on.
But ISPC proved that you can use this fork-join model on SIMD units like AVX512 or SSE. Julia and Python are showing off how you can use it in higher level languages. Etc. etc.
OpenMP further proves that fork-join works on normal CPU threads. Maybe there's some other CPU-threading languages (but OpenMP is the one I'm personally familiar with).
Are you talking about the actual fork() function, instead of, say, spawning inside a loop explicitly?
Producer-consumer queues say otherwise. As does Async. There's also message-passing. There's also thread-pools.
Task-based programming doesn't wait for the tasks to finish. So without the "join", you don't have an easy synchronization point to rely upon, and have to use explicit locks for synchronization.
------
Producer - consumer can't work with fork/join, because there's no "join" at all! The producer keeps producing, and the consumers just keep consuming.
Async is... a tangled mess. I'm pretty sure its just the modern "unstructured goto" style of spaghetti that has been rediscovered. Yeah, its super-efficient, but no, its not really easy to program.
Task-based parallelism is probably my next favorite style. Fork-join is super easy, but the join causes a significant waiting period and underutilization of the processor. Task-based provides more flexibility (at the cost of a little bit more complexity).
------
There's a ton of other models of parallelism. But fork-join seems to be the methodology that more and more work is being focused on.
That's not inherent to async though, just a property of the implementations that have seen widespread adoption. There's nothing stopping async from implementing the same process tree semantics; in fact, there's at least one high-level async library that does exactly that (python-trio, https://trio.readthedocs.io/en/stable/)
I'm looking into it: and it looks like "Stupid Fibonacci" proves that its working in Julia (https://julialang.org/blog/2019/07/multithreading/).
Task-based parallelism is fraught with more overhead than other patterns. But it really is a powerful construct: converting recursive ideas into multithreaded concepts rather easily.
Also fork join sounds just like normal threads to me in the way you are describing it.
Sorry what an I missing?
If you need to synchronize, wait for the next fork/join cycle instead of doing explicit mutexes. The "join" provides your synchronization point.
If you have a complicated set of highly-synchronous calculations, then you don't fork at all. You simply use the "Single Thread" to perform all those calculations (and therefore negate the need of cross-thread synchronization).
Fork-join has low-utilization, but its very easy to program. In some cases, fork-join remains efficient (ex: spawn a thread per pixel on the screen), because all pixels do NOT need to synchronize with each other (or call mutexes).
-----
If a sync point within the children are needed, you usually make due with a barrier instead of a mutex.
> What does 'Single Instruction Multiple Data' have to do with threads?
SIMD processors, such as GPUs, emulate a thread in their SIMD units. Its called a "CUDA Thread". Its not a true thread in the sense of CPU-threads, but the performance by-and-large scales as if it were real threads. (With exception of the "thread-divergence problem")
Ultimately, the fork-join model translates trivially to SIMD. Any practitioner of CUDA, OpenCL, ROCm, or ISPC can prove that to you easily.
I mean it's not really any different from writing a C/C++ program that avoids use of mutexes by having each thread operate on separate parts of the process address space. I'm still intrigued but it's not mind-blowing to me to fork a bunch of threads and join them when the function execution completes.
Its more about discipline than anything else. A recognition that fork-join is much easier than other methodologies (such as Async).
Sounds like what you are saying that fork join model translates easely by the compiler to these SIMD instructions?
Some compilers can also vectorize plain loops, but you would advocate for fork join?
Why do you think CUDA has become so popular recently? That's exactly what CUDA, OpenCL, and ISPC does.
> Some compilers can also vectorize plain loops, but you would advocate for fork join?
CUDA style / OpenCL style fork-join is clearly easier than reading compiler output, trying to debug why your loop failed to vectorize. That's the thing about auto-vectorizers, you end up having to grok through tons of compiler output, or check out the assembly, to make sure it works.
ALL fork-join style CUDA / OpenCL code automagically compiles into SIMD instructions. Ditto with ISPC. Heck, GPU programmers have been doing this since DirectX 7 HLSL / OpenGL decades ago.
There's no "failed to vectorize". There's no looking up SIMD-instructions or registers or intrinsics. (Well... GPU-assembly is allowed but not necessary). It just works.
-------
If you've never tried it, really try one of those languages. CUDA is for NVidia GPUs. OpenCL for AMD. ISPC for Intel CPUs (instead of SIMD intrinsics, ISPC was developed for an OpenCL-like fork-join SIMD programming environment).
And of course, Julia and Python have some CUDA plugins.
https://www.openmp.org/spec-html/5.0/openmpsu42.html
Its not as reliable as a dedicated language like OpenCL or ISPC. But this might be easier for you to play with rather than learning another language.
OpenMP is just #pragmas on top of your standard C, C++, or Fortran code. So any C / C++ / Fortran compiler can give this sort of thing a whirl rather easily.
---------
OpenMP always was a fork-join model #pragma add on to C / C++. They eventually realized that their fork-join model works for SIMD, and finally added SIMD explicitly to their specification.
The term "fork/join cycle" is intriguing and meaningless to me as a non-Julia user. What exactly is this cycle?
> The term "fork/join cycle" is intriguing and meaningless to me as a non-Julia user. What exactly is this cycle?
https://www.researchgate.net/profile/Alina_Kiessling/publica...
There are many forks-and-joins across a program, when you're doing the fork-and-join paradigm. Each time the threads need to communicate, you join, and then use the "master" thread to pass data to all of the different units.
-------------
For example, most video-game engines issue a fork to calculate the verticies of all objects in the video game (the fork turns into a GPU call). This is called the vertex-shader.
Once all the verticies are calculated, the GPU joins these threads together, and the main-program / game engine continues.
The next step is the geometry shaders: so the CPU forks (aka: spawn thousands of GPU threads), and joins on the results of the geometry shaders. (Tesselation may spawn more vertexes. Ex: you model a rope as a square, but then the geometry shader turns the square into a rope-shape at this stage)
Then the pixel shaders. For every pixel of your 1080 x 1920 screen, a GPU SIMD-thread is forked off, and each pixel's final color is calculated based on the results of vertex-shaders and geometry shaders before it.
Each of these cycles is a fork-join cycle. Thousands of threads spawning, thousands of threads joining, the CPU calculating some synchronization data together, and then spawning thousands of threads again.
(In practice, modern game engines are now async for speed reasons. But the general fork/join model is still kinda there if you squint)
I don't know either.
However, GPUs basically do SMT behind the scenes. Because GPUs do not reorder instructions like a CPU they stall on memory accesses but instead of waiting they simply switch to another thread. This means you need a lot of threads for good performance.
That being said, packages such as SIMD.jl [0], and LoopVectorization.jl [1] are making fantastic progress, to the point that LoopVectorization forms the basis of a legitimate BLAS contender, in pure Julia [2]. It's not totally there yet, but it's close enough that real work is being done in LV at OpenBLAS-like speeds.
As an aside, I find it incredible that these kinds of extensions can be built in packages thanks to the fact that Julia's compiler is extensible enough to allow for direct manipulation of the LLVM intrinsics being emitted by user code.
[0] https://github.com/eschnett/SIMD.jl [1] https://github.com/chriselrod/LoopVectorization.jl [2] https://github.com/MasonProtter/Gaius.jl
> find it incredible that these kinds of extensions can be built in packages thanks to the fact that Julia's compiler is extensible
Come on, jeez.. Julia’s compiler is a Lisp-based LLVM driver: of course it can do these things.
I didn't take it as such; there are legitimate shortcomings to any tool, I just wanted to provide pointers to other readers that the devs are aware of it, and that there is ongoing development to address it. :)
> Come on, jeez.. Julia’s compiler is a Lisp-based LLVM driver: of course it can do these things.
As someone who, before Julia, was firmly entrenched in C/C++/Python land, I suppose I am discovering many of these "obvious" things for the first time. :)
sorry for the flippant remark then! it's great to be in discover mode, enjoy ;)
Indeed: ISPC provides constructs to provide structure-of-arrays, and other low-level memory layout details. This is a good thing: these details have significant implications on the speed of your program.
Nonetheless, any language which wrangles with manual details of memory layout, or new/delete based memory allocation, is inevitably going to be classified as low level in my books.
> ISPC among others provides a great interface to SIMD model for CPUs that simply isn’t available in Julia.
If Julia can compile into GPU-assembly (which is innately SIMD), I'm sure an AVX-based compile could work eventually.
They may have to target AVX512 (since most GPU-assembly requires per-thread exec-masks), but the general concept is being proven as Julia can now compile down into PTX or AMDGPU assembly.
Julia's compile down to GPU-assembly / SIMD code is not supported in the "general case", only in select circumstances. But that's still an incredible boon for a high-level language.
I guess we aren’t solving the same problems: memory allocation is trivial is my domain, mapping complex nested control flow in SIMD is the hard part.
> Julia can now compile down into PTX or AMDGPU assembly.
Sure but Julia-as-syntax is nothing special now, Numba does this for a Python as well.
Then suppose I rewrite the LU decomposition algorithm, using python. Now I want to accelerate the search by running the search on GPUs. I have to re-rewrite the code from scratch. Each GF256 encoding has to have rejiggered operators, and so I need to rewrite custom GPU kernels, then figure out how to resequence the operations (* looks different for each GF256 encoding), etc.
This is all SUPER easy in julia.
https://www.youtube.com/watch?v=Gh578e98qAk
> Suppose I want to write a library to find compression polynomials for reed-solomon encoding system (see mary wootters' talks on youtube) for my storage product. I need a LU decomposition algorithm that operates on GF256
Surely you use isa-l[1].
[1] https://github.com/intel/isa-l
> Now I want to accelerate the search by running the search on GPUs.
GPUs are float oriented so I don't think you'll get the performance you hope for out of 8 bit integer operations. If you have interesting results to share I'd like to read them.
You've never seen GPU Hashcat, or GPU Bitcoin / Ethereum mining?
Vega now has a huge focus on INT8 operations. NVidia can perform int operations in parallel with float operations (superscalar GPU cores)
I've heard of it, but afaik it hasn't been profitable to mine using a GPU for a long time due to the competition for hashes, power consumption, and rate they mine at. This is in contrast to ASICs which can mine even faster for less capex and opex.
Those statements are just confusing to me. In most GPU-code, you have crazy amounts of parallelism and therefore don't really care what order those statements execute in.
As such, if you can allocate memory, and map those if/else statements into a collection of queues and/or stacks (depending on if you want a breadth-first search, or depth-first search pattern).
In effect: you use memory allocation to solve the complex control flow issue. Maybe its more obvious with code:
Instead of doing:
if(baz()){
foo();
} else {
bar(); // Thread divergence!!
}
Do: if(baz()){
pushIntoFooQueue();
} else {
pushIntoBarQueue();
// Thread divergence, but not much of a penalty
}
while(fooIsNotEmpty()){ // No thread divergence at all
task = SIMDPopFoo();
task.execute();
}
while(barIsNotEmpty()){
task = SIMDPopBar();
task.execute();
}
This is heavier on the memory units, because you now have to manage the data-structures. But this style practically negates the thread-divergence penalty completely. If you're lucky, your fooQueue and barQueue fit in __shared__ memory.Bonus points: Not only is thread-divergence negated, but you also achieve effective load-balancing across your workgroup. If Thread#0 spawns 20 items for FooQueue, after pushing/popping from the queue, those 20 Foo-tasks will be assigned to 20 different threads.
-----
From there on out, you're just pushing / popping different parts of your code to various queues and/or stacks. But this is only really a valid solution if you have a decent memory allocator that knows where and how to clean up these queues / stacks (especially if you have nodes starting to point to each other for dependency management)
I haven't really solved this problem "in general", but is clear that the queues should be sorted into topological order, and that any tasks that depend onto each other need to be run in different iterations. It really depends on how much you're willing to spend on organizing this execution information.
In any case: the memory allocation issue is one-and-the-same with the thread-divergence / complex instruction flow issue to me. You need to create a data-structure that organizes the instruction flow, and any complex data-structure will need memory management as soon as you start linking things together. The above uses a queue or stack, but things can get more complicated.
--------
Not that Julia, Python, ISPC or anything really... solves this problem. But memory management is very useful for this "style" (I'm mostly doing ref-counted C++ with a custom allocator myself. But such trees or graphs of links can grow into the CPU-side and end up using the default memory allocator)
This is an interesting remark I have to sleep on. I usually don't see this as possible, since I have a bunch of arrays which are allocated once, then a bunch of nested but static control flow. The only way I've found to go beyond single thread performance is with "whole program" SIMD which only worked in the ISPC/OpenCL/CUDA programming models.
But its a different paradigm to do things: something to try if your standard if/else stuff isn't working out.
EDIT: If you can make due with just stack-allocation (or queue-allocation), if you have singular-sized tasks with predictable sizes, if all the information fits inside of __shared__ memory, and if thread-divergence is normally a problem... this methodology will probably work.
Oh, and for a hint:
myIdx = prefixSum(__activemask());
myTask = stack[stackTail + myIdx];
if(myIdx == 0){
stackTail -= __popc(__activemask()); // Lulz, I made a bug. Whatever, I think you understand...
}
__syncThreads(); // Barrier is important
Stack pops and pushes are pretty easy. activemask() provides your execution mask, and the logic needed to synchronously push / pop items to a stack (and probably to more complicated memory allocation functions that I haven't figured out yet)Eventually is nice but it’s also easy to imagine that it never gets done. ISPC works today.
It's a different language for a different computer. But the fundamentals are still there, laying the groundwork for the future.
Sure, ispc can win on latency. But the raw compute girth of GPU SIMD cannot be underestimated.
In any case, Julia has proven itself capable of compiling down to a SIMD instruction set and achieving nearly the full performance of those GPUs. Even if it's not a computer you prefer, the programming model and technology demonstrated here is clear.
My comment was more geared to the CPU side of things since I write code whose users don’t usually have GPUs available.
There are also problem sizes which don’t fit the GPU’s more stark memory system separation: tens of CPU cores with tens of MBs of cache can move past GPUs in terms of memory bandwidth for such cases so banking on CUDA just doesn’t work for everyone.
Apparently, Julia now supports some more complicated forms of parallelism: Async, @Spawn (aka: Tasks), and others. But @Thread based for loops are just easier to grok.
Overall, Julia is one of the few high-level languages that is explicitly adding in highly-threaded concepts like Fork-join, SIMD, or even GPUs to the language.
Yeah, some Python libraries extend these concepts to Python. But something like @Threads for ... end really demonstrates how the fork-join parallelism is a first-class member of the Julia language.
using PyCall
np = pyimport("numpy")
res = np.fft.fft(rand(ComplexF64, 10))
The interop with matlab and cpp is similarly painless.
It seems insane to me that it's this easy in Julia, yet most other languages are completely incompatible without transpiling one into the other.
This is not to say macros aren't awesome.
I've arguably got a lot more experience doing scientific programming in c++ and FORTAN, than I do in python. Yet for quick prototyping where performance is almost never an issue, I still elect to use python because it lets me wrap my head around the problem faster. I spend less time fixing stupid errors and thinking about coding and more problem thinking about the problem I really need to solve.
Julia might be even better, I've heard good things, I've just never used it though. In the scientific computing and data science world that I deal with, I'd say at least 95% of the time there's never a need to jump from a hacked together prototype to a more production level product.
I ended up making a custom system image with the libraries I always use (Plots, etc) which does help on that front.
That's a fairly minor gripe, mind you. It's still great.
You suggest alternative B?
Pyro can scale to pretty big datasets thanks to custom inference via guides. So you can do inference on pretty sophisticated models such as Deep Markov.
Whereas Turing is mostly for small or medium sized models. For learning purposes, for smaller models or for non-parametrics Turing might be a much better choice, though.
Within Julia, ForneyLab.jl is quite cool for non-deep state space models. I also like Turing.jl, but it has different tradeoffs.
In my experience, Pyro (generally variational inference) is more like a Bayesian optimization than full inference, since it will simply miss multimodality or non-Gaussian tails, etc.
Lastly, the article mentions that `plot` also works for uncertainty-carrying floats, because the `Measurements` package has implemented "a simple recipe" for this case. But doesn't this mean very tight coupling? The `Measurements` author very specifically (if I understand this right) implemented a `plot` functionality. For the end user, this allows things to just work, which is great. But under the hood, it seems this depends on package authors having to be aware of all possible use cases for their library code.
There's also the issue that one sometimes needs to specialize generic operations in generic algorithms on something other than the receiver—sometimes even on more than one of the arguments. Single dispatch forces you to use something slow and awkward like double dispatch in cases like this. And that's assuming the person who wrote the generic code anticipates the need for specialization! If they don't allow for it, then you're just stuck. With multiple dispatch, you can just define the method you need the specialization for and you're done.
So let's say you do that. You define your own new number-like type, say Complex. You pass that in to plot(n).
Somewhere in the body of plot(), though, it turns out there's some code like:
# Flip to put origin at bottom left.
window_height = 400
y = window_height - n
So now plot() tries to invoke "-" with an int on the left and your complex number on the right. It passes your complex number to the int class's minus operator, which has no idea about your type and barfs.Python has a hacky workaround for this specific case where the right operand can implement __rsub__(), but that hacky workaround exists specifically because Python doesn't have multiple dispatch.
If it supported multiple dispatch natively, you'd just define an operator - that took a number on the left and a complex on the right, plot() would call it, and you'd be good.
Plot recipes are different. The Plots.jl system calls the type recipe function recursively on the plotting data `X` until it sees a set of primitives that it knows. Libraries define dispatches to this function to denote how data types should transform to something more primitives. Quaternion numbers become four independent series of numbers (and add keyword arguments for doing things like modulus). The ODE solver library writes a recipe that say, if you try and plot an ODE, transform it into arrays of time series. Now plot on a quaternion ODE solution recurses into 4x time series of the solution, which is the plot you'd want to have. This is a feature I use all of the time: I don't generally do more than just `plot(sol)` since recursive recipes generally give me the plot I want (or else I open an issue because someone is missing a recipe).
> If it supported multiple dispatch natively, you'd just define an operator - that took a number on the left and a complex on the right, plot() would call it, and you'd be good.
This doesn't come across as a win to me.
This might come with sacrificed performance. If you want a different implementation for a different data type to get the best performance, writing one method makes that hard.
> The `Measurements` author very specifically (if I understand this right) implemented a `plot` functionality.
This is a good point. But in that example, note that `Measurements` also works with all of differential equations, and they didn't implement any differential equations specific code, unlike as you pointed out with `plot`. The fact that uncertainty propagates through a differential equation solver is pretty impressive, imo.
https://github.com/fonsp/Pluto.jl is a super-nice way of just starting, without too much setup cost. I think it will soon be self-contained as to what packages & versions are needed, too.
A bigger barrier here is that we had to know how to apply propagation of errors, so although we could use a tool like this to do the calculation we still had to generate a set of equations for the lower bound, best estimate, and upper bound. There's a seductive argument to be made for just plugging those equations into Excel instead of using them to validate that the Julia functions are working correctly.
Now contrast that with how much stuff you need to learn to be proficient with any programming language — and no matter how much you think your favorite one is a friendly unicorn, teaching a room full of beginners will be eye-opening for the things they get stuck on. Which editor to use? How to install it? Are you saying “$LANG” but actually using that as a shorthand for “$LANG, Git and shell tools, and the conventions for using which are common in my specialty”? How much time does it take to learn the basics of the language, how to interpret error messages, and debug things?
None of that is insurmountable, of course, but the difference in learning curves is why many people end up with the Excel file which started in a hurry and is now Frankenstein's workbook which everyone is afraid to touch or share. Over a sufficient time interval, it'd be much easier to pick any decent toolchain because maintaining that level of complexity in almost any major programming language is going to be less work but unless you're starting with experienced developers the short term answer will probably favor Excel. I've seen people who've been meaning to get around to rewriting it long enough that the target language has shifted as things fall in and out of favor.
The seamless way in which Julia libraries can be mixed and matched and used together is a testament to this. Python on the otherhand, each library is its own little world and cannot be used in novel or unexpected ways.
Moreover, the unperformant nature of Python means that you must use libraries in very cookie cutter ways or else performance falls off a cliff.
And the consequence in terms of programming is exactly that it that it shifts the responsibility of handling interoperability to whoever creates a new type. For example if I create MyType and want a method SomeoneType + MyType to work, I don't need to change the '+' implementation on SomeoneType for that to work (for example making a PR on his library), I only define methods with the type I just created. Which is why people say Julia ecosystem compose a lot, the creator of SomeoneType can just focus on what he wants and possibly never even know about MyType, MyType will extend it with whatever I want without having to re-implement stuff that SomeoneType already implemented (only MyType stuff and the intersection between libraries, defined by methods that use MyType and SomeoneType), and the final user can just import all stuff he wants and have a library that looks like a monolith but is actually many smaller libraries working together this way.
This story is more like a general intro to Julia, but it has an example with Knights, Pikeman and Archers fighting each other (sort of rock, paper, scissors game), which also shows the utility of multiple dispatch.
https://levelup.gitconnected.com/knights-pikemen-archers-and...
But to give a quick idea here. Imagine writing functions for intersecting two geometric shapes. The algorithm for intersecting a circle and square is entirely different from intersecting a polygon and a line segment. The specific algorithm needed will depend on BOTH shapes not just one. Python can only dispatch on one of the shapes, not both.
So in Julia I could write a different function implementation like this:
intersect(c::Circle, r::Rectangle)
intersect(c1::Circle, c2::Circle)
intersect(r1::Rectangle, r2::Rectangle)
and so on.You can then define somemethod for other data types, without touching the original definition, and that makes your new definition available to the other code that used the original somemethod. It is a unique feature of Julia (at least unique in a sense that it is absent from mainstream languages)
function somemethod(x::Int, y::Int)
function somemethod(x::Int, y::Float64)
function somemethod(x::Float64, y::Float64)
Multiple dispatch is available at least in Common LISP, though I guess that's not considered particularly mainstream.
It also exists in R, but again, is optional (and slow), and is therefore mostly unused, which limits its usefulness, making it less used, etc. etc.
In Julia, it is default, so everything participates in multiple dispatch. It completely pervades the language, which is what makes it so useful.
If the language is single dispatch though, animal1.f(animal2) will accurately call the Cat or Dog method for the first argument (before the dot), but the second argument will work like above, it will call the Animal version and not the Cat or Dog ones. If you want a method to call Cat or Dog method based on runtime information you'd have to redispatch on runtime within f(Animal), which is basically a pattern known as Visitor Pattern [1].
In Julia (and multiple dispatch languages) defining f(Cat, Dog) and f(Animal, Animal) means that even when I call f(animal1, animal2) the compiler will convert to the runtime values and if the first animal is Cat and the second is Dog it will call the first method (as a form of specialization) and otherwise it will go for f(Animal, Animal) (which is the more general method). And the Julia compiler is really good at inferring runtime types at compile time, so even though it's runtime values it's still static dispatch (that's why it's also fast).
You can find examples where operator overloading and multiple dispatch actually behaves differently and gives different answers, but the comparison works as a first order approximation.
(The fact that you can have static dispatch on dynamic types is a pretty subtle point that hurts my head, but the upshot is that you get semantically dynamic dispatch with static performance as an optimization.)
Personally, I hate this presentation. Multiple dispatch (to me) is cognitively heavier and not what I would default to. If it's a performance gain and I need performance, then sure. But too often people hold it up as a virtue because the need for speed is assumed.
It also heavily improves composability.
It's LESS performant if not done carefully. Certainly FastAI, Matt rocklin etc didn't reimplement a subset in python just to get slower code
It just seems profoundly un-natural and arbitrary to limit dispatch to only the first argument.
https://computationalthinking.mit.edu/Fall20/
There’s a live video on ray tracing starting in about 20 minutes
The issue for Julia is, if you stay in Numpy and Scipy in Python, and can use JIT when applicable, you are basically using C. You can make it fast using C libraries and there is not whole lot of room for Julia to shine. Even with C, I had to be lately a bit careful to beat Numpy.
Numpy is a great piece of engineering, but its limitations are very noticeable when compared to Julia.
edit: not trivial, but much easier
There's a ton of in-place operations there, including in-place solving of linear systems, and at least some stuff related to svd (though the names are pretty un-intuitive, being wrappers for BLAS and LAPACK functions).
Is this a matter of simply wrapping the 'right' LAPACK routines, or is there something missing in the interfaces that could be trivially added in principle?
For anyone else interested there is a large list starting here [1]. The in-place versions will all end with `!`.
[1] https://docs.julialang.org/en/v1/stdlib/LinearAlgebra/#Linea...
But i really think that in a few years time, when Julia’s ecosystem has had more time to grow and develop it can rival R.
For what it's worth, I found that taking the plunge into julia is what made me love programming.
I'm constantly amazed by how easy it is to whip something up myself that before I would have relied on a built-in routine in another language. Doing this made me a better programmer and more capable of doing the things that 'matter most' as well.
Plus, whenever I whip something up myself, I try to share it with the community either as it's own package or as a PR to an existing package, so I get to feel involved in the progress as we get to the sort of ecosystem you would find acceptable.
Also to note, Julia has seen a rise over the last few years, but so has Python, its primary competitor in the data science field.
For existing projects, fine, stick with Python. But Julia is strictly superior to Python in everyway, there is nothing that Python is better at than Julia. This is partially because Julia can call any Python library, but mostly because it's actually designed for data science and scientific computing.
Julia doesn't run everywhere that Python does. Especially if you include all of the Python implementations, including MicroPython.
Last I checked, Julia's startup cost was high, making it the wrong choice for short-lived programs. Yes, people do scientific computing in short-lived programs.
Now, I don't know where Julia stands in this, but Python is used to teach people to program, including high school students and younger. By the time Python was 8 years old (as Julia is now), Python was being used in high school courses. Python's built-in "turtle" module exists as part of that education component - at one Python conference, an educator pointed out that it can be very hard to get anything installed at primary and secondary schools, so having a maintained "turtle" in the base installation was important.
I therefore suspect Julia is a worse teaching language than Python for high school level and below.
Regarding Julia, yes the focus seems to be more on college, where the matlab-like math support makes it a lot closer to regular math. Though since it's not very different from Python someone can still pick it up easily from knowing Python in high school (or Racket).
Remember, I am presenting what I think are counter-examples to "is strictly superior to Python in everyway" (excluding "existing projects"). I still think that's true even if Python weren't popular at all.
As an example of the influence, the ":" at the end of lines which start a block in Python is not required, in the language sense. However, ABC testing showed it made the language more useful.
As an example of improving Python for learners, one of the reasons for Python's change from 1/2 == 0 -> 1/2 == 0.5 was feedback from the Alice developers, where they found that students did not understand van Rossum's decision to follow C's semantics.
Python started being used in schools when it wasn't popular. One of the presenters at the Python conference in 1998 or so was a high school AP comp sci teacher. At that time C++ and Java were the AP languages. He reported better success teaching students Python first and then teaching C++ or Java, than spending all of the time in one of the latter languages.
Which is why I attempted support my personal beliefs via examples of Python uptake. Something you interpreted as an argument by popularity.
I observe that https://julialang.org/learning/classes/ lists only a single high school, and I didn't see a single course meant as a general introductory programming course for non-programmers.
Here's the IPCC talk (from 2000) I mentioned about "Using Python in a High School Computer Science Program" https://legacy.python.org/workshops/2000-01/proceedings/pape...
It references the "Computer Programming for Everybody" essay at https://www.python.org/doc/essays/everybody/ , which I argue shows the stronger emphasis on the Python developers for beginning programs, inherited from ABC.
I also mentioned Alice. "Because Alice is targeted towards novice programmers, it is important that Python, more than languages such as Tcl or Scheme, can be mastered by new Alice programmers with little effort" (quoting the 1995 paper at http://www.cs.cmu.edu/~stage3/publications/95/journals/IEEEc... ). But that was at a university.
Python of course is much more established than Julia, which is why I linked to resources from when Python was only a year or two older than Julia is now.
That said, at https://www.python.org/community/sigs/current/edu-sig/ you can find other teaching resources for high school teaching and for "kids" and "young children". My local library here in Sweden has a book on programming for kids, which uses Python.
Is there a similar movement as CP4E within the Julia? Nothing from https://julialang.org/learning/ suggests that the needs of non-programmers, such as high school students, plays a strong role in its design.
Do you know of any such internal movement? Can you point me to any external resources which show Julia being used as a teaching language for non-programmers?
That is until you use the debugger.
Anyway, half the data-scientists I know don't use the debugger, they're solidly in the print-to-debug camp, or they're using notebooks and going step-by-step.
Or stumble into the first example in JuliaPlots, which doesn't work.
Unless you mean compling julia or something like it to WASM and running it in the browser. That could get interesting, but julia is still "a bit too chonky" for that.
And of course, you can also use a more low latency high availability oriented language handling the frontend (like Elixir, which will have trouble doing the heavy data oriented stuff Julia can), with Julia as a microservice to handle the actual backend logic and analytics, which is probably the setup I'd go for (using 2 high level lispy-languages with very different qualities, each doing what it does best).
I would do this exactly too, if I had to do number crunching.
Julia's overenthusiastic posts here in HN make me wary of the language instead of curious.
function cmatrix(Q::AbstractVecOrMat{Quaternion{T}}) where {T}
[complex.( real.(Q), imag.(Q)) complex.( jmag.(Q), kmag.(Q));
complex.(-jmag.(Q), kmag.(Q)) complex.( real.(Q), -imag.(Q))]
end
function qmatrix(C::AbstractMatrix{Complex{T}}) where {T}
n, m = size(C)
quat.(C[1:n÷2, 1:m÷2], C[1:n÷2, m÷2+1:m])
endEdit just to mention that Numba solves by and large the middle ground for the domain I work in, despite what Julia proponents have to say.
That's always the problem with a new programming language. The target for the language is the people that already have something that works. Then a decade passes and that language is no longer a shiny new object, so enthusiasm for it dies, it's just another old language with warts.
Numerical computing is something where there's always been something that works. Before Python it was C and Fortran.
The problem people ran into is that when everything is wrapped around low-level libraries for speed, you eventually run into the catch 22 of using the slower language that you prefer for clarity, or the faster one that makes it acceptable performance-wise.
In other words, with Python, to get the performance you would have got in C or Fortran, you have to code in C or Fortran. Then you're not using Python anymore. The idea (in theory, and a lot in practice) with Julia (or Nim, or other LLVM-targeting languages) is that you don't have this penalty.
So in that sense Julia is providing something that isn't working in Python or Matlab.
I think Julia's not quite what it's cracked up to be, but mostly it is, and I'd probably prefer working with it over Python or Matlab for numerical stuff.
But Python didn't take off because of scientific computing. Google was a heavy user of Python from the start. Python seriously sucked for scientific computing in 2005. I was looking for a new language (the old one wasn't cutting it) and I'd been looking for a use for Python for years. It had some projects but it just was not in a useful state so I went with R, which was quite mature by that time, and it focused on statistical analysis. Years later Python became popular for scientific computing, likely due to the userbase it had built up in other areas.
These days, Nim is a better Cython but with less dynamic temptations and more powerful metaprogramming (and, yes, a much smaller ecosystem..maybe not that much smaller than Python in the late 90s, though). { Not that this is all Nim is...It's actually a really good everything-language that's tricky to summarize in just a few words. }
I remember dynamic languages people picking up on three things in particular: Python apparently being designed to be inherently inefficient, not having proper garbage collection, and having weird scoping. At least Python was something to point people at who rejected Lisp as an alternative to Tcl and Perl for scientific computing.
And before that, analog circuits. Hodgkin and Huxley computed their first results for neuronal activity without a computer.
1. It's very easy to inspect generated code of your kernels where you really need SIMD. For instance:
julia> function my_kernel(xs)
total = zero(eltype(xs))
for x in xs
@fastmath total += x
end
total
end
julia> @code_native debuginfo=:none my_kernel(rand(10))
...
L96:
vaddpd 8(%rcx,%rax,8), %ymm0, %ymm0
vaddpd 40(%rcx,%rax,8), %ymm1, %ymm1
vaddpd 72(%rcx,%rax,8), %ymm2, %ymm2
vaddpd 104(%rcx,%rax,8), %ymm3, %ymm3
addq $16, %rax
cmpq %rax, %rdi
jne L96
...
2. There's a Julia package that does the code gen for vectorization exactly because LLVM does not always get it right: https://chriselrod.github.io/LoopVectorization.jl/stable/exa...3. You can make LLVM explain why it did not vectorize certain loops just like in clang.
Just to be clear: I want to use Julia but without guessing about SIMD use in kernels. Until this is available for complex control flow in Julia, it’s not a game changer in my opinion.
Edit: that kernel is trivial. I have nested control flow to vectorize.
julia> function simple!(ys, xs)
@avx for i = eachindex(xs)
ys[i] = if xs[i] < 3.
xs[i]^2
else
sqrt(xs[i])
end
end
end
This generates vbroadcastsd, vpcmpgtq, vmulpd, vsqrtpd, vblendvpd and vmaskmovpd AVX2 instructions (I don't have AVX512). But more complex control flow does indeed not work; the `@avx` macro errors it can't handle it at the moment. varying float aff[nc], xh[nn], wij[nn], x[nc];
varying int ih[nn];
uniform int t_ = t & (nl - 1);
uniform float k;
for (int j=0; j<nc; j++)
aff[j] = 0.0f;
for (int j=0; j<nn; j++)
for (int i=0; i<nc; i++)
aff[i] += wij[j] * shuffle(xh[j], ih[j]);
for (int i=0; i<nc; i++)
x[i] = 0.1f*(x[i] + x[i]*x[i]*x[i]/3.0f + k*aff[i]);
for (uniform int i=0; i<nn; i++)
xh[i] = insert(rotate(xh[i], 1), 0, extract(x[i/nl], i&(nl-1)));
Just being able to absorb SIMD lanes into the notion of varying vs uniform makes this easy to write, not to mention SIMD operations like shuffle or rotate. In Julia or Numba or CUDA I have to index into arrays, ensure compatible data layout etc. I imagine this could be done with more macrology in Julia, but again why not use something which already works.Sure, but there's always somebody crazy enough to try to implement it :p
From the fact that ISPC can generate C++ (with --emit-c++) I would think their compiler is conceptually very high-level. Since so few new concept are introduced, I wouldn't be surprised if someone would implement the same DSL in Julia at some point, just to see if one can get similar performance.
It's a Clang-based frontend for LLVM, so yeah, no reason Julia couldn't make use of their work. I think the main thing to tap into is the uniform vs varying support, since this drives the whole thing.
For performance you virtually always need to write 'vectorized' code, which is often difficult and memory heavy.
The daily churn of handling input parsing, along with contorting code into vectorized form takes up an extraordinary amount of time when writing Matlab code.
Matlab is certainly not 'good enough' for me! It's just what I'm stuck with at work.
But that's just extremely restrictive. The jit does not work on arbitrary user code.
I have recently been working with a REST API, something I don't do with either Python or Julia normally. I spent probably 10x as long time getting this to work in Python. It was not just a matter of not doing what I wanted, but also in not supporting well established conventions well as well as simply having far more complex APIs than the Julia equivalent.
This is something I frequently find. Julia APIs for doing the same thing as in Python will generally be much easier to work with.
Python APIs also tend to be very OOP oriented and stateful. Meaning there is a lot of mutating of hidden state. Julia APIs tend to show much more clearly what the data flow, is because they follow a more functional approach, where mutation of state is generally avoided. Although Julia is pragmatic. It isn't like working in Haskell.
NOTE: I am not saying Python is a bad language. It would usually be my second choice. I am just trying to say that there are REAL advantages to using Julia, which go far beyond mere performance. I never started using Julia due to performance but due to its expressiveness and clean APIs.
Right now, it appears like a niche interpreted language for numeric geeks only.
> Right now, it appears like a niche interpreted language for numeric geeks only.
Yes, it was designed for numerical and scientific computing. In Julia, you can at least prototype the numerical algorithm before implementing in C++ or Fortran.
Note that it doesn't do slim binaries yet for embedded. Don't know if that's in the roadmap.
Start a julia repl, hit ] for the package manager, paste `add MKLSparse`, write `using SparseArrays, MKLSparse`, run `sprand(100,100, 0.1) * rand(100)`
Personally, working on sparse eigensolvers, my only complaint is that IterativeSolvers.jl is still somewhat lacking (though the ARPACK.jl bindings are more comparable to what scipy does), but otherwise cannot imagine going back to python for this kind of low-level numerical research. Having code run comparably to fortran speed (barring some memory annoyances) is huge for working on numerical algorithms.
Having to switch to a language you don't know is not a good situation, it doesn't help that a lot of other people know that other language.
FTFY cf Numba
That said, in terms of composability you can jit over the closure to achieve a lot of what you might want, e.g.
def make_loop(f):
@jit
def fn(x):
for i in range(x.shape[0]):
x[i] = f(x[i])
return fn
for any jit'd function f will be just as fast as if you had inlined the body of f.For introspection and simplicity, I think, in high performance, you simply have to choose two of fast, simple and generic. Julia clearly chooses fast and generic.
In terms of deployment, you essentially have to have Julia installed wherever you want to run, which frequently ok, and if not there’s always PackageCompiler except it’s not that easy to get a working shared lib.
There are other things I find complex but probably because I’ve used Python too long so Julia is different etc. I think Julia is a great choice for HPC generally except for challenging SIMD codes where it’s hard to vectorize without an explicit ILP model.
https://jax.readthedocs.io/en/latest/notebooks/autodiff_cook...
It’s honestly exhausting arguing with all you Julia boosters. You can down vote me to hell, I don’t care. I’m done engaging with this community.
You all are not winning over any market share from Python with your dismissive, arrogant, closed minded culture.
> Please don't comment on whether someone read an article. "Did you even read the article? It mentions that" can be shortened to "The article mentions that."
> Please don't comment about the voting on comments. It never does any good, and it makes boring reading.
> Please respond to the strongest plausible interpretation of what someone says, not a weaker one that's easier to criticize. Assume good faith.
Also, look from where this conversation started. My claim was that jax does not work with "(scipy ode solvers, special functions, image processing libraries, special number types (mpmath), domain-specific libraries)". A julia library does not need to know of Zygote.jl to be autodifferentiable. A python library needs to be pure-python numpy-based library to work with jax.
In order to try to contribute to the discussion: I think this paper describes relatively well what is so special about the Julia autodiff tools: https://arxiv.org/abs/1810.07951
For a separate approach, which is also very original, check out https://github.com/jrevels/Cassette.jl
In what language are you defining these custom arrays or types? certainly not in python, or they'll be too slow to be worthwhile.
[1] https://github.com/JuliaLang/Microbenchmarks/blob/af3d18f7b3...
[2] https://github.com/JuliaLang/Microbenchmarks/blob/af3d18f7b3...
Most the speed advantage of the C here is due to reusing the same memory over and over, which you can do pretty easily in Julia as well. Here's the Julia code using in-place operations and it's still quite a bit more readable than the C version: https://gist.github.com/StefanKarpinski/e57f5a36b7890b261a0d.... I timed this and it's the same speed as the C version. When developing this, it follows the same general outline as the C version, but you have several benefits: (1) You can use asserts to compare to the easy version; (2) There are niceties like bounds checks and array indexing. In C you can't do (1) because there is on easy version to compare with. And doing the array index computations in C is kind of a nightmare—it's so easy to accidentally screw them up.
Jax is composable. In fact it’s a core design goal. Jax arrays implement the Numpy API. I routinely drop Jax arrays into other python libraries designed for Numpy. It works quite well. It’s not effortless 100% of the time but no library interop is (including Julia multiple dispatch).
I can introspect Jax. Until you wrap your function with jit(foo) it’s as introspectable as any other Python code, at least if I’m understanding what you mean by introspection.
Jax has implemented most of the Numpy functions, certainly most of the ones anyone needs to use on a regular basis. I rarely find anything missing. And if it is, I can write it myself, in python, and have it work seamlessly with the rest of Jax (autodiff, jit, etc)
You want to work with Units or track Measurment error (or both?). Basically same story. Except better in some ways worse in others. Better because you don't have to fork numpy, it is extensible enough to allow that. Packages exist that use that etendability for exactly that. Worse because those are scalar types, why are you even having to write code to deal with array support at all. Agian 2 julia packages and they don't even mention arrays internally.
The problem's not Jax. The problem is numpy. Or rather the problem is this level of composability is really hard most of the time in most languages (including the python + C combo. Especially so even).
Its true that this is not always trivial 100$% of the time with julia's multiple dispatch. but it is truer there than anywhere else i have seen.
Julia hasn't been around long enough to build ecosystem with multiple commerical giants building competing products.
> Julia does not make that sacrifice.
Sure, Julia sacrifices your sanity on the alter of stack traces, method resolution, time to first plot, among others.
> fairly limited subset of python
Fantastic comment, always brought up, except that this same subset of Python (really how to use CPU efficiently) is about the same that anyone writing about Julia performance is preaching.
Julia provides the interoperability between such libraries, including custom datatypes, frequently with fast zero-cost abstractions.
> fast zero-cost abstraction
Until you get a compiler error and the stack trace takes up a few screens of text. Who maintains that abstraction by the way? Nothing is free.
This is completely and demonstrably false. Julia allows fast programming with macros, multiple dispatch, abstract types and more.
In fact, multiple dispatch is essential to get good performance, abstract types have a cost if explicitly used inside struct definitions, but that's why you use parametric types, and abstract types do not have a cost in function signatures.
Macros are expanded at compile-time, and at least have no runtime cost, in fact they are often used for improved performance.
I think there's some fundamental misunderstanding going on here, but I'm not sure what it is. Or do you mean to say that if it is possible to write slow code in a language, then only a subset of that language has good performance? If that is the case, I don't know how to respond.
In fact, those aren't special features and there is no other subset. Those are the core abstractions on which everything in Julia, down to primitive types and wrappers for llvm intrinsics are built. Without them you wouldn't have Julia.
Julia's GPU ecosystem wouldn't be where it is with just one or two people maintaining it without being able to reuse those features to plug into existing machinery.
Makes me think of this little pun: https://github.com/FluxML/Flux.jl/blob/master/src/utils.jl#L...
Does this work with numba? Are your jit-compiled functions compiled into external modules?
Ideas from Lisp have been creeping into "mainstream" programming languages for decades. As the article points out, Common Lisp introduced multiple dispatch over 40 years ago and now it's finding its way into Julia. There's lots more in Lisp that can still find its way into a modern programming language.
I'm particularly interested in the inclusion of a LispSyntax for Julia that might allow Lisp macros to be written at the LispSyntax level that can then be used at the surface Julia level.
(T has some historical interest just as the original implementation language of the Yale Haskell compiler.)
I've heard from some colleagues that Julia as a language is pretty solid these days, but all the machinery that you might need to take a system to production _aside_ from the business logic is a little lacking. I'd be interested to hear if others agree!
I don't think it would actually be that hard to get the stdlib logging to work directly with that kind of system either. (Probably if we were starding today we might have built ontop of the stdlib logging, but Memento is a bit older than that, and a bit different in philosophy)
THere are a bunch of database drivers. We use LibPQ.jl in production. I hear good things also about OCDB.jl, and SQLite.jl also. and anything to do with data is really nice because of how interoperable all the data-frames like packges are. Everything just works together as long as the minimal API described by Tables.jl is implemented. (and everything implements that)
What was the thinking behind adopting the `end` notation for marking the end of blocks. I know that certain family of PLs (Erlang, Ruby) use them. But the dominant flavor is the `{}` syntax (c/c++/scala/rust etc)
Was it an explicit choice or more based on your past familiarity with other languages?
Julia currently uses {} mostly for type systemy things like parametric types. When I write Foo{T}, this says that T is a parameter in the type Foo.
E.g. a vector of Float64s in julia is an Array{Float64, 1} (1 means one dimensional) whereas a matrix of vectors of complex Ints is an Array{Array{Complex{Int}, 1}, 2}.
When I am in other languages like C/C++ I tell myself that I working with memory addresses. For memory addresses 0-index makes more sense. For tables, matrices etc I honestly think 1-based indexing makes more sense.
Only time I dislike 1-based indexing in Julia is when I write code which is strongly related to how memory works. Like when I was implementing a CPU simulator and assembler.
After studying ordinal numbers, I feel like the normal way to start counting is from 0 (or the "empty set" ordinal).
This is also how you prove something by transfinite induction, you need to start by 0.
I wish I could easily recompile Julia so that it is 0-indexed. But then what about all the libraries...
I try to write code to accept 0-, 1-, 2-, and StarWars-based indexing.
So it's this "extra work" that is the problem.
It would be nice to have it as an environment variable or a runtime option. Then what about portability issues...
So let's just fork Julia and call it JuliB with 0-indexing =)
> the technical reason we started counting arrays at zero is that in the mid-1960’s, you could shave a few cycles off of a program’s compilation time on an IBM 7094. The social reason is that we had to save every cycle we could, because if the job didn’t finish fast it might not finish at all and you never know when you’re getting bumped off the hardware because the President of IBM just called and fuck your thesis, it’s yacht-racing time.