Category Archives: Julia

Installing Julia on Ubuntu

By: DSB

Re-posted from: https://medium.com/coffee-in-a-klein-bottle/install-julia-1-5-on-ubuntu-bb8be4b2571d?source=rss-8bd6ec95ab58------2

A quick tutorial on how to install Julia on Ubuntu and add the kernel to Jupyter

Installing Julia on Ubuntu is very straightforward. As anything on Linux, there are several ways to do the installation.

1. Using Apt-Get

This is the first way, and it is the easiest. Just open the terminal and run

sudo apt-get install julia

The problem with this method is that you don’t get the latest stable version for Julia. In the time I’m writing this article, the Ubuntu repository contains the version 1.0.4 for Julia, while the current stable version is 1.5.2.

So this is not the method I recommend!

2. From the Website (recommended)

First, go to the Julia website download page. There, several ways to run Julia are shown. Go to the “Current stable release” table, and click on the link shown in the image below.

Donwload Julia by clicking on the “64-bit” inside the red square

Then, the Julia Language will be donwloaded (most likely to your Download directory). Next, go on your terminal and unzip the downloaded file.

tar -xvzf julia-1.5.2-linux-x86_64.tar.gz

You now have a folder with all the Julia files. No installation is required. Now, we move this folder to “/opt” (this is not strictly necessary, you can use any other location in your machine).

sudo mv julia-1.5.2/ /opt/

Finally, create a symbolic link to the Julia binary file. This will allow you to run the command “julia” in your terminal and start the Julia REPL.

sudo ln -s /opt/julia-1.5.2/bin/julia /usr/local/bin/julia

Done! Now Julia is already installed and working in your machine. Let’s now add it to Jupyter.

3. Adding Julia kernel to Jupyter

We assume that Jupyter is already installed in your machine. To add the Julia Kernel to it is quite easy. Just open your terminal and run “julia”. This will open the REPL, as shown in the image below.

Inside the REPL, we then run the following commands:

using Pkg
Pkg.add(“IJulia”)

The “Pkg” package is used in Julia to install different packages on Julia. By installing “IJulia”, it automatically adds the Julia kernel to Jupyter.

And that’s it. Julia should be working with your Jupyter Notebook and/or JupyterLab.


Installing Julia on Ubuntu was originally published in Coffee in a Klein Bottle on Medium, where people are continuing the conversation by highlighting and responding to this story.

Julia Base is DataFrames.jl best friend

By: Blog by Bogumił Kamiński

Re-posted from: https://bkamins.github.io/julialang/2020/10/02/performance.html

Introduction

Recently I received a very interesting question about the performance
of operations on GroupedDataFrame in DataFrames.jl.

In this post I want to share some comments related to the performance of DataFrames.jl.

The TLDR summary is:
if you want top performance fall back to low-level Julia
code (and DataFrames.jl exposes API that allows you to follow that path).

In this post I am using Julia 1.5.2 and DataFrames.jl 0.21.7.

The question and a direct answer

Assume you have a large data frame df that has two columns: :id and :v
(in practice also many more columns that are irrelevant so I skip them).
For each unique value of :id you want to keep only one row that corresponds
to the smallest value of :v in the group defined by column :id.

We start with generating the example DataFrame and grouping it by :id column:

julia> using DataFrames, Random

julia> Random.seed!(1234);

julia> df = DataFrame(id=rand(1:10^6, 10^7), v=rand(10^7))
10000000×2 DataFrame
│ Row      │ id     │ v        │
│          │ Int64  │ Float64  │
├──────────┼────────┼──────────┤
│ 1        │ 99353  │ 0.954945 │
│ 2        │ 371388 │ 0.103541 │
⋮
│ 9999998  │ 71163  │ 0.37311  │
│ 9999999  │ 529940 │ 0.856262 │
│ 10000000 │ 845766 │ 0.401057 │

julia> gdf = groupby(df, :id);

julia> @time gdf = groupby(df, :id)
  1.326534 seconds (30 allocations: 280.590 MiB)
GroupedDataFrame with 999963 groups based on key: id
First Group (13 rows): id = 99353
│ Row │ id    │ v         │
│     │ Int64 │ Float64   │
├─────┼───────┼───────────┤
│ 1   │ 99353 │ 0.954945  │
│ 2   │ 99353 │ 0.584677  │
⋮
│ 11  │ 99353 │ 0.838052  │
│ 12  │ 99353 │ 0.0773429 │
│ 13  │ 99353 │ 0.774979  │
⋮
Last Group (1 row): id = 502811
│ Row │ id     │ v        │
│     │ Int64  │ Float64  │
├─────┼────────┼──────────┤
│ 1   │ 502811 │ 0.953105 │

(note that I run here and below all commands twice to exclude compilation time
and allocations from what is reported by @time macro)

A direct approach to solve this problem would be the following (here I store the
result in res1 variable to compare it to other solutions at the end of this post):

julia> res1 = combine(sdf -> first(sort(sdf)), gdf);

julia> @time combine(sdf -> first(sort(sdf)), gdf)
 13.587720 seconds (296.78 M allocations: 12.672 GiB, 9.73% gc time)
999963×2 DataFrame
│ Row    │ id     │ v         │
│        │ Int64  │ Float64   │
├────────┼────────┼───────────┤
│ 1      │ 99353  │ 0.0436469 │
│ 2      │ 371388 │ 0.0203823 │
⋮
│ 999961 │ 167495 │ 0.228266  │
│ 999962 │ 527369 │ 0.0525908 │
│ 999963 │ 502811 │ 0.953105  │

We see that the operation is quite slow and massively allocates. The problem is
that sort(sdf) allocates a fresh small DataFrame for each group. This is
inefficient.

Let us think then if we can improve the performance of this code.

Pre-sorting

The first option to consider is pre-sorting of df like this:

julia> res2 = combine(first, groupby(sort(df, :v), :id));

julia> @time combine(first, groupby(sort(df, :v), :id))
  7.166366 seconds (14.03 M allocations: 1.811 GiB, 2.85% gc time)
999963×2 DataFrame
│ Row    │ id     │ v          │
│        │ Int64  │ Float64    │
├────────┼────────┼────────────┤
│ 1      │ 533731 │ 1.27094e-7 │
│ 2      │ 709049 │ 1.29805e-7 │
⋮
│ 999961 │ 803454 │ 0.991274   │
│ 999962 │ 107403 │ 0.99218    │
│ 999963 │ 925877 │ 0.994074   │

We already see much improvement. However, this solution has a problem that
it still requires sorting, which in the chain sort, groupby, combine
is the slowest step.

Using argmin

Actually we see that we do not need to sort our data to get the result. It
is enough to find the minimum row with respect to column :v. Here is how
one can do it:

julia> res3 = combine(sdf -> sdf[argmin(sdf.v), :], gdf);

julia> @time combine(sdf -> sdf[argmin(sdf.v), :], gdf)
  1.581478 seconds (15.48 M allocations: 593.183 MiB, 4.29% gc time)
999963×2 DataFrame
│ Row    │ id     │ v         │
│        │ Int64  │ Float64   │
├────────┼────────┼───────────┤
│ 1      │ 99353  │ 0.0436469 │
│ 2      │ 371388 │ 0.0203823 │
⋮
│ 999961 │ 167495 │ 0.228266  │
│ 999962 │ 527369 │ 0.0525908 │
│ 999963 │ 502811 │ 0.953105  │

Now we are much faster. We can do better if we start using a low-level API
of DataFrames.jl and use Julia Base functionality.

Going for Julia Base

I will present two solutions here that do the job faster than the last
example of combine. The first one uses the fact that we can iterate
GroupedDataFrame and parentindices function returns us a tuple of indices
into the parent of the passed object. Here is the approach that can be used:

julia> res4 = df[[parentindices(sdf)[1][argmin(sdf.v)] for sdf in gdf], :];

julia> @time df[[parentindices(sdf)[1][argmin(sdf.v)] for sdf in gdf], :]
  0.990385 seconds (5.19 M allocations: 325.875 MiB, 4.73% gc time)
999963×2 DataFrame
│ Row    │ id     │ v         │
│        │ Int64  │ Float64   │
├────────┼────────┼───────────┤
│ 1      │ 99353  │ 0.0436469 │
│ 2      │ 371388 │ 0.0203823 │
⋮
│ 999961 │ 167495 │ 0.228266  │
│ 999962 │ 527369 │ 0.0525908 │
│ 999963 │ 502811 │ 0.953105  │

And we see that we are somewhat faster. We can go much faster by using the
groupindices function that gives us a group number for each row of the df
data frame. The cost is that the solution is a bit more verbose:

julia> function getfirst(gdf, v)
           loc = zeros(Int, length(gdf))
           best = Vector{eltype(v)}(undef, length(gdf))
           for (i, (ind, vi)) in enumerate(zip(groupindices(gdf), v))
               if !ismissing(ind) && (loc[ind] == 0 || vi < best[ind])
                   best[ind] = vi
                   loc[ind] = i
               end
           end
           return parent(gdf)[loc, :]
       end
getfirst (generic function with 1 method)

julia> res5 = getfirst(gdf, df.v);

julia> @time getfirst(gdf, df.v)
  0.359210 seconds (20 allocations: 116.349 MiB)
999963×2 DataFrame
│ Row    │ id     │ v         │
│        │ Int64  │ Float64   │
├────────┼────────┼───────────┤
│ 1      │ 99353  │ 0.0436469 │
│ 2      │ 371388 │ 0.0203823 │
⋮
│ 999961 │ 167495 │ 0.228266  │
│ 999962 │ 527369 │ 0.0525908 │
│ 999963 │ 502811 │ 0.953105  │

We could go even faster (at least twice in my tests), but let me stop here and
leave the potential improvements as an exercise.

Additionally, it is worth to mention that the vector groupindices(gdf) has a
value equal to missing if some row from df is not mapped to any group in
gdf (note that GroupedDataFrame can be indexed-into).

Conclusions

Before we conclude let us check if the five solutions give us the same
results:

julia> length(unique(sort.([res1, res2, res3, res4, res5])))
1

Indeed they do. I had to use sort here, as res2 has a different row order
than the rest of data frames holding the results.

What are the take aways from these examples:

  • if you think about avoiding allocations and doing unnecessary work (sorting
    in our case) you can get a reasonably fast result using high-level API of
    DataFrames.jl;
  • if you want to go really fast there is always an option to fall-back to
    custom code written in Julia Base and DataFrames.jl has a low-level API
    that allows you to efficiently do this.

The reason why high-level API is slower is that it handles dozens of possible
use cases under one function and going low-level allows you do do only the thing
that needs to be done in your use case.

In general, it is worth to highlight here that, as opposed to e.g. R, DataFrame
in Julia is just one of the many data structures you can use. The high-level API
DataFrames.jl provides will be fast enough in most of the cases (at the benefit
of being very flexible). However, if things are too slow just go low-level,
which is a natural thing to do in Julia.

CUDA.jl 2.0

By: Blog on JuliaGPU

Re-posted from: https://juliagpu.org/2020-10-02-cuda_2.0/

Today we’re releasing CUDA.jl 2.0, a breaking release with several new features. Highlights
include initial support for Float16, a switch to CUDA’s new stream model, a much-needed
rework of the sparse array support and support for CUDA 11.1.

The release now requires Julia 1.5, and assumes a GPU with compute capability 5.0 or
higher (although most of the package will still work with an older GPU).

Low- and mixed-precision operations

With NVIDIA’s latest GPUs emphasizing support for low-precision operations, CUDA.jl
now starts to support. For example,
the CUBLAS wrappers can be used with (B)Float16 inputs (running under JULIA_DEBUG=CUBLAS
to illustrate the called methods) thanks to the cublasGemmEx API call:

julia> mul!(CUDA.zeros(Float32,2,2), cu(rand(Float16,2,2)), cu(rand(Float16,2,2)))

I! cuBLAS (v11.0) function cublasStatus_t cublasGemmEx(...) called:
i!  Atype: type=cudaDataType_t; val=CUDA_R_16F(2)
i!  Btype: type=cudaDataType_t; val=CUDA_R_16F(2)
i!  Ctype: type=cudaDataType_t; val=CUDA_R_32F(0)
i!  computeType: type=cublasComputeType_t; val=CUBLAS_COMPUTE_32F(68)

2×2 CuArray{Float32,2}:
 0.481284  0.561241
 1.12923   1.04541
julia> using BFloat16s

julia> mul!(CUDA.zeros(BFloat16,2,2), cu(BFloat16.(rand(2,2))), cu(BFloat16.(rand(2,2))))

I! cuBLAS (v11.0) function cublasStatus_t cublasGemmEx(...) called:
i!  Atype: type=cudaDataType_t; val=CUDA_R_16BF(14)
i!  Btype: type=cudaDataType_t; val=CUDA_R_16BF(14)
i!  Ctype: type=cudaDataType_t; val=CUDA_R_16BF(14)
i!  computeType: type=cublasComputeType_t; val=CUBLAS_COMPUTE_32F(68)

2×2 CuArray{BFloat16,2}:
 0.300781   0.71875
 0.0163574  0.0241699

Alternatively, CUBLAS can be configured to automatically down-cast 32-bit inputs to Float16.
This is now exposed through a task-local
CUDA.jl math mode:

julia> CUDA.math_mode!(CUDA.FAST_MATH; precision=:Float16)

julia> mul!(CuArray(zeros(Float32,2,2)), CuArray(rand(Float32,2,2)), CuArray(rand(Float32,2,2)))

I! cuBLAS (v11.0) function cublasStatus_t cublasGemmEx(...) called:
i!  Atype: type=cudaDataType_t; val=CUDA_R_32F(0)
i!  Btype: type=cudaDataType_t; val=CUDA_R_32F(0)
i!  Ctype: type=cudaDataType_t; val=CUDA_R_32F(0)
i!  computeType: type=cublasComputeType_t; val=CUBLAS_COMPUTE_32F_FAST_16F(74)

2×2 CuArray{Float32,2}:
 0.175258  0.226159
 0.511893  0.331351

As part of these changes, CUDA.jl now defaults to using tensor cores. This may affect
accuracy; use math mode PEDANTIC if you want the old behavior.

Work is under way to extend these
capabilities to the rest of CUDA.jl, e.g., the CUDNN wrappers, or the native kernel
programming capabilities.

New default stream semantics

In CUDA.jl 2.0 we’re switching to CUDA’s
simplified stream programming
model
.
This simplifies working with multiple streams, and opens up more possibilities for
concurrent execution of GPU operations.

Multi-stream programming

In the old model, the default stream (used by all GPU operations unless specified otherwise)
was a special stream whose commands could not be executed concurrently with commands on
regular, explicitly-created streams. For example, if we interleave kernels executed on a
dedicated stream with ones on the default one, execution was serialized:

using CUDA

N = 1 << 20

function kernel(x, n)
    tid = threadIdx().x + (blockIdx().x-1) * blockDim().x
    for i = tid:blockDim().x*gridDim().x:n
        x[i] = CUDA.sqrt(CUDA.pow(3.14159f0, i))
    end
    return
end

num_streams = 8

for i in 1:num_streams
    stream = CuStream()

    data = CuArray{Float32}(undef, N)

    @cuda blocks=1 threads=64 stream=stream kernel(data, N)

    @cuda kernel(data, 0)
end
Multi-stream programming (old)

In the new model, default streams are regular streams and commands issued on them can
execute concurrently with those on other streams:

Multi-stream programming (new)

Multi-threading

Another consequence of the new stream model is that each thread gets its own default stream
(accessible as CuStreamPerThread()). Together with Julia’s treading capabilities, this
makes it trivial to group independent work in tasks, benefiting from concurrent execution on
the GPU where possible:

using CUDA

N = 1 << 20

function kernel(x, n)
    tid = threadIdx().x + (blockIdx().x-1) * blockDim().x
    for i = tid:blockDim().x*gridDim().x:n
        x[i] = CUDA.sqrt(CUDA.pow(3.14159f0, i))
    end
    return
end

Threads.@threads for i in 1:Threads.nthreads()
    data = CuArray{Float32}(undef, N)
    @cuda blocks=1 threads=64 kernel(data, N)
    synchronize(CuDefaultStream())
end
Multi-threading (new)

With the old model, execution would have been serialized because the default stream was the
same across threads:

Multi-threading (old)

Future improvements will make this behavior configurable, such that users can use a
different default stream per task.

Sparse array clean-up

As part of CUDA.jl 2.0, the sparse array support has been
refactored
, bringing them in line with other
array types and their expected behavior. For example, the custom switch2 methods have been
removed in favor of calls to convert and array constructors:

julia> using SparseArrays
julia> using CUDA, CUDA.CUSPARSE

julia> CuSparseMatrixCSC(CUDA.rand(2,2))
2×2 CuSparseMatrixCSC{Float32} with 4 stored entries:
  [1, 1]  =  0.124012
  [2, 1]  =  0.791714
  [1, 2]  =  0.487905
  [2, 2]  =  0.752466

julia> CuSparseMatrixCOO(sprand(2,2, 0.5))
2×2 CuSparseMatrixCOO{Float64} with 3 stored entries:
  [1, 1]  =  0.183183
  [2, 1]  =  0.966466
  [2, 2]  =  0.064101

julia> CuSparseMatrixCSR(ans)
2×2 CuSparseMatrixCSR{Float64} with 3 stored entries:
  [1, 1]  =  0.183183
  [2, 1]  =  0.966466
  [2, 2]  =  0.064101

Initial support for the COO sparse matrix type
has also been added, along with more better
support for sparse matrix-vector
multiplication
.

Support for CUDA 11.1

This release also features support for the brand-new CUDA 11.1. As there is no compatible
release of CUDNN or CUTENSOR yet, CUDA.jl won’t automatically select this version, but you
can force it to by setting the JULIA_CUDA_VERSION environment variable to 11.1:

julia> ENV["JULIA_CUDA_VERSION"] = "11.1"

julia> using CUDA

julia> CUDA.versioninfo()
CUDA toolkit 11.1.0, artifact installation

Libraries:
- CUDNN: missing
- CUTENSOR: missing

Minor changes

Many other changes are part of this release:

  • Views, reshapes and array reinterpretations are now
    represented
    by the Base array wrappers,
    simplifying the CuArray type definition.
  • Various optimizations to CUFFT and
    CUDNN library wrappers.
  • Support for LinearAlgebra.reflect! and
    rotate!
  • Initial support for calling CUDA libraries
    with strided inputs