From 5160ad37524522008c49b808e09f2f7d0140e6c1 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 24 Apr 2026 15:22:35 +1200 Subject: [PATCH 1/6] [docs] convert parallelism tutorial into a literate document --- .github/workflows/documentation.yml | 1 + docs/.gitignore | 1 - docs/make_utilities.jl | 1 + docs/src/tutorials/algorithms/parallelism.jl | 556 ++++++++++++++++ docs/src/tutorials/algorithms/parallelism.md | 639 ------------------- 5 files changed, 558 insertions(+), 640 deletions(-) create mode 100644 docs/src/tutorials/algorithms/parallelism.jl delete mode 100644 docs/src/tutorials/algorithms/parallelism.md diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 39cf8a2eefd..def9921d3b0 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -36,6 +36,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key DOCUMENTER_LATEX_DEBUG: ${{ github.workspace }}/latex-debug-logs + JULIA_NUM_THREADS: 4 run: julia --color=yes --project=docs -p 2 docs/make.jl - uses: actions/upload-artifact@v7 if: ${{ always() }} diff --git a/docs/.gitignore b/docs/.gitignore index c338b3d3f72..84df4efb6a7 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -3,7 +3,6 @@ latex_build/ latex_src/ src/tutorials/*/*.md !src/tutorials/*/introduction.md -!src/tutorials/algorithms/parallelism.md src/moi/ src/tutorials/linear/transp.txt src/release_notes.md diff --git a/docs/make_utilities.jl b/docs/make_utilities.jl index 23627fad846..0d62fe511d7 100644 --- a/docs/make_utilities.jl +++ b/docs/make_utilities.jl @@ -111,6 +111,7 @@ function literate_tutorials() joinpath("getting_started", "performance_tips.md"), joinpath("linear", "tips_and_tricks.md"), joinpath("linear", "typed_indices.md"), + joinpath("algorithms", "parallelism.md"), ] filename = joinpath(@__DIR__, "src", "tutorials", file) content = read(filename, String) diff --git a/docs/src/tutorials/algorithms/parallelism.jl b/docs/src/tutorials/algorithms/parallelism.jl new file mode 100644 index 00000000000..11c6c4aeaed --- /dev/null +++ b/docs/src/tutorials/algorithms/parallelism.jl @@ -0,0 +1,556 @@ +# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors #src +# This Source Code Form is subject to the terms of the Mozilla Public License #src +# v.2.0. If a copy of the MPL was not distributed with this file, You can #src +# obtain one at https://mozilla.org/MPL/2.0/. #src + +# # Parallelism + +# The purpose of this tutorial is to give a brief overview of parallelism in +# Julia as it pertains to JuMP, and to explain some of the things to be aware of +# when writing parallel algorithms involving JuMP models. + +using JuMP +import HiGHS + +# ## Overview + +# There are two main types of parallelism in Julia: + +# 1. Multi-threading +# 2. Distributed computing + +# In multi-threading, multiple tasks are run in a single Julia process and share +# the same memory. In distributed computing, tasks are run in multiple Julia +# processes with independent memory spaces. This can include processes across +# multiple physical machines, such as in a high-performance computing cluster. + +# Choosing and understanding the type of parallelism you are using is important +# because the code you write for each type is different, and there are different +# limitations and benefits to each approach. However, the best choice is highly +# problem dependent, so you may want to experiment with both approaches to +# determine what works for your situation. + +# ## Multi-threading + +# To use multi-threading with Julia, you must either start Julia with the +# command line flag `--threads=N`, or you must set the `JULIA_NUM_THREADS` +# environment variable before launching Julia. For this documentation, we set +# the environment variable to: + +ENV["JULIA_NUM_THREADS"] + +# You can check how many threads are available using: + +Threads.nthreads() + +# The easiest way to use multi-threading in Julia is by placing the +# `Threads.@threads` macro in front of a `for`-loop: + +@time begin + ids = Int[] + my_lock = Threads.ReentrantLock() + Threads.@threads for i in 1:Threads.nthreads() + global ids, my_lock + Threads.lock(my_lock) do + push!(ids, Threads.threadid()) + end + sleep(1.0) + end +end + +# This for-loop sleeps for `1` second on each iteration. Thus, if it had +# executed sequentially, it should have taken the same number of seconds as +# there are threads available. Instead, it took only 1 second, showing that the +# iterations were executed simultaneously. We can verify this by checking the +# `Threads.threadid()` of the thread that executed each iteration: + +ids + +# !!! danger +# The `Threads.threadid()` that a task runs on may change during execution. +# Therefore, it is not safe to use `Threads.threadid()` to index into, say, +# a vector of buffer or stateful objects. As an example, do not do: +# ```julia +# x = rand(Threads.nthreads()) +# Threads.@threads for i in 1:Threads.nthreads() +# x[Threads.threadid()] *= 2 # Danger! This use of threadid is not safe +# end +# ``` +# For more information, read +# [PSA: thread-local state is no longer recommended](https://julialang.org/blog/2023/07/PSA-dont-use-threadid/). + +# ### Data races + +# When working with threads, you must avoid data races. A data race occurs when +# multiple threads access the same variable at the same time, at least one thread +# modifies the variable, and the order of the reads and writes are not properly +# coordinated. + +# Here's an example of a data race: +begin + x = Ref(0) + Threads.@threads for i in 1:Threads.nthreads() + for i in 1:1_000 + x[] += 1 + end + end + x[] +end + +# The expected answer is `4_000` (because there are four threads each incrementing +# 1,000 times), but the actual result is much smaller. Moreover, the result is +# non-deterministic; if we re-ran this code, we would get a different value each +# time. + +# We got the wrong answer because multiple threads are reading and writing `x` +# at the same time without coordination. For example, the following sequence +# could occur: + +# * Assume `x[]` currently has a value of `3` +# * Thread A reads `x[]` to get `3` +# * Thread B reads `x[]` to get `3` +# * Thread A writes `x[] = 3 + 1 = 4` +# * Thread B writes `x[] = 3 + 1 = 4` +# * The final value of `x[]` is `4` + +# The write from Thread A is overwritten, and so the value of `x[]` has increased +# by `1` instead of `2`. + +# Similar to the earlier `ids` example, we can fix this data race using a +# `ReentrantLock`. The lock ensures that only one thread can update (read and then +# write) `x` at a time. Now we get the correct answer: + +begin + x = Ref(0) + l = ReentrantLock() + Threads.@threads for i in 1:Threads.nthreads() + for i in 1:1_000 + lock(l) do + x[] += 1 + end + end + end + x[] +end + +# With the lock, the sequence of events goes something like: + +# * Assume `x[]` currently has a value of `3` +# * Thread A acquires the lock +# * Thread A reads `x[]` to get `3` +# * Thread B asks for the lock, but is denied because A is currently using it +# * Thread A writes `x[] = 3 + 1 = 4` +# * Thread A releases the lock +# * Thread B acquires the lock +# * Thread B reads `x[]` to get `4` +# * Thread B writes `x[] = 4 + 1 = 5` +# * Thread B releases the lock +# * The final value of `x[]` is `5` + +# See the [Multi-threading](https://docs.julialang.org/en/v1/manual/multi-threading/) +# section of the Julia documentation for more details. + +# ### JuMP models are not thread-safe + +# An object is thread-safe if it can be modified by separate threads without +# causing a data race. JuMP models are not thread-safe. Code that uses +# multi-threading to simultaneously modify or optimize a single JuMP model +# across threads may error, crash Julia, or silently produce incorrect results. + +# For example, the following incorrect use of multi-threading crashes Julia: + +function an_incorrect_way_to_use_threading() + model = Model(HiGHS.Optimizer) + set_silent(model) + @variable(model, x) + Threads.@threads for i in 1:10 + optimize!(model) + end + return +end + +# ```julia +# julia> an_incorrect_way_to_use_threading() +# julia(76918,0x16c92f000) malloc: *** error for object 0x600003e52220: pointer being freed was not allocated +# zsh: abort julia -t 4 +# ``` + +# To avoid issues with thread safety, create a new instance of a JuMP model in +# each iteration of the for-loop. In addition, you must avoid race conditions in +# the rest of your Julia code, for example, by using a lock when pushing elements +# to a shared vector. + +# ### Thread safety and the closure capture bug + +# !!! danger +# This section is very important to understand. It is not specific to JuMP and +# it applies to all multithreaded Julia programs. + +# There is an upstream design issue in Julia ([julia#14948](https://github.com/JuliaLang/julia/issues/14948)) +# that can silently introduce race conditions to your code and violate thread +# safety. + +# You can trigger this bug if you have a local variable inside the +# `Threads.@threads` loop with the same name as a variable outside the loop. +# Here's an example: + +function _create_model(j) + model = Model(HiGHS.Optimizer) + @variable(model, x[1:j]) + return model +end + +function dont_run_segfault_likely() + models = _create_model.(1:2) + Threads.@threads for j in 1:2 + model = models[j] # `model` is used inside the loop + set_lower_bound.(model[:x], j) + optimize!(model) + end + model = models[1] # `model` is used outside the loop +end + +# And indeed, running this code results in: +# ```julia +# julia> dont_run_segfault_likely() +# julia(67421,0x170d83000) malloc: *** error for object 0x600003192870: pointer being freed was not allocated +# julia(67421,0x170d83000) malloc: *** set a breakpoint in malloc_error_break to debug +# ``` + +# This code is problematic for the following reason. Because `model` appears +# both inside `Threads.@threads` and outside, Julia's scoping rules treat it as +# a single variable. Therefore, instead of creating a different `model` for each +# thread, Julia creates a single mutable container called a `Core.Box` that is +# used to store the value of `model` throughout the life of the function. Now +# there is a race condition for reads and writes of `model`, and so a thread may +# read the value of `model` only to find that its `model` was overwritten with the +# value of `model` from another thread. + +# To diagnose this issue, use `@code_warntype`. If your code is problematic, you +# will see a local variable with the type `::Core.Box`: +# ```julia +# julia> @code_warntype dont_run_segfault_likely() +# MethodInstance for dont_run_segfault_likely() +# from dont_run_segfault_likely() @ Main REPL[3]:1 +# Arguments +# ... +# Locals +# ... +# model::Core.Box +# ... +# ``` +# If you see `Core.Box`, you must refactor your code to avoid re-using the same +# variable name inside and outside the threading loop. + +# Alternatively, you can annotate the local variables inside the loop with `local` +# to disambiguate them from the variables outside the loop. + +function _create_model(j) + model = Model(HiGHS.Optimizer) + @variable(model, x[1:j]) + return model +end + +function safe_to_run() + models = _create_model.(1:2) + Threads.@threads for j in 1:2 + local model = models[j] # annotated as `local` + set_lower_bound.(model[:x], j) + optimize!(model) + end + model = models[1] # This `model` is not the inner `model` + return +end + +safe_to_run() + +# ### Example: parameter search with multi-threading + +# Here is an example of how to use multi-threading to solve a collection of JuMP +# models in parallel. + +function a_good_way_to_use_threading() + solutions = Pair{Int,Float64}[] + my_lock = Threads.ReentrantLock(); + Threads.@threads for i in 1:10 + model = Model(HiGHS.Optimizer) + set_silent(model) + set_attribute(model, MOI.NumberOfThreads(), 1) + @variable(model, x >= i) + @objective(model, Min, x) + optimize!(model) + assert_is_solved_and_feasible(model) + Threads.lock(my_lock) do + push!(solutions, i => objective_value(model)) + end + end + return solutions +end + +a_good_way_to_use_threading() + +# !!! warning +# For some solvers, it may be necessary to limit the number of threads used +# internally by the solver to 1 by setting the [`MOI.NumberOfThreads`](@ref) +# attribute. + +# ### Example: building data structures in parallel + +# For large problems, building the model in JuMP can be a bottleneck, and you +# may consider trying to write code that builds the model in parallel, for +# example, by wrapping a `for`-loop that adds constraints with +# `Threads.@threads`. Here's an example: + +function an_incorrect_way_to_build_with_multithreading() + model = Model() + @variable(model, x[1:10]) + Threads.@threads for i in 1:10 + @constraint(model, x[i] <= i) + end + return model +end + +# This code errors (although on same Julia versions it may just return a model +# that is missing some constraints): +# ```julia +# julia> an_incorrect_way_to_build_with_multithreading() +# ERROR: TaskFailedException +# +# nested task error: ConcurrencyViolationError("Vector can not be resized concurrently") +# ``` + +# The error happens because JuMP models are not thread-safe. Code that uses +# multi-threading to simultaneously modify or optimize a single JuMP model +# across threads may error, crash Julia, or silently produce incorrect results. + +# The correct way to build a JuMP model with multi-threading is to build the +# data structures in parallel, but add them to the JuMP model in a thread-safe +# way: + +function a_correct_way_to_build_with_multithreading() + model = Model() + @variable(model, x[1:10]) + my_lock = Threads.ReentrantLock() + Threads.@threads for i in 1:10 + con = @build_constraint(x[i] <= i) + Threads.lock(my_lock) do + add_constraint(model, con) + end + end + return model +end + +a_correct_way_to_build_with_multithreading() + +# !!! warning +# **Do not use multi-threading to build a JuMP model just because your original +# code is slow.** In most cases, we find that the reason for the bottleneck is +# not JuMP, but in how you are constructing the problem data, and that with +# changes, it is possible to build a model in a way that is not the bottleneck +# in the solution process. If you need help to make your code run faster, ask +# for help on the [community forum](https://jump.dev/forum). Make sure to +# include a reproducible example of your code. + +# ## Example: using Channels + +# Here's an example where we split the model building from the model solution. +# Instead of using an explicit `ReentrantLock`, we use a `Channel` to store the +# solutions. + +function build_model(s::Int; N::Int = 80) + model = Model(HiGHS.Optimizer) + set_silent(model) + @variable(model, x[1:N], Bin) + @variable(model, y[1:N], Bin) + @variable(model, z[1:N, 1:N] >= 0) + @constraint(model, [i in 1:N, j in 1:N], z[i, j] <= x[i]) + @constraint(model, [i in 1:N, j in 1:N], z[i, j] <= y[j]) + @constraint(model, [i in 1:N, j in 1:N], z[i, j] >= x[i] + y[j] - 1) + @objective(model, Min, sum(rand(-10:10) * i for i in z)) + set_time_limit_sec(model, s) + return model +end +struct Solution + scenario::Int + objective_value::Float64 +end +function solve_model(ch::Channel{Solution}, s::Int, model::Model) + optimize!(model) + @assert has_values(model) # There is always the trivial solution + put!(ch, Solution(s, objective_value(model))) + return +end +function run_channel_example(S::Int) + models = Vector{Model}(undef, S) + Threads.@threads for s in 1:S + models[s] = build_model(s) + end + ch = Channel{Solution}() + for (s, model) in enumerate(models) + Threads.@spawn solve_model(ch, s, model) + end + for i in 1:S + solution = take!(ch) + println("s=$(solution) [solved $i/$S]") + end + return +end +run_channel_example(15) + +# ## Distributed computing + +# To use distributed computing with Julia, use the `Distributed` package: + +import Distributed + +# Like multi-threading, we need to tell Julia how many processes to add. We can +# do this either by starting Julia with the `-p N` command line argument, or by +# using `Distributed.addprocs`: + +# ````julia +# julia> import Pkg + +# julia> project = Pkg.project(); + +# julia> workers = Distributed.addprocs(4; exeflags = "--project=$(project.path)") +# 4-element Vector{Int64}: +# 2 +# 3 +# 4 +# 5 +# ```` + +# !!! warning +# Not loading the parent environment with `--project` is a common mistake. + +# The added processes are "worker" processes that we can use to do computation +# with. They are orchestrated by the process with the id `1`. You can check +# what process the code is currently running on using `Distributed.myid()` + +Distributed.myid() + +# As a general rule, to get maximum performance you should add as many processes +# as you have logical cores available. + +# Unlike the `for`-loop approach of multi-threading, distributed computing +# extends the Julia `map` function to a "parallel-map" function +# `Distributed.pmap`. For each element in the list of arguments to map over, +# Julia will copy the element to an idle worker process and evaluate the +# function, passing the element as an input argument. + +# ````julia +# julia> function hard_work(i::Int) +# sleep(1.0) +# return Distributed.myid() +# end +# hard_work (generic function with 1 method) + +# julia> Distributed.pmap(hard_work, 1:4) +# ERROR: On worker 2: +# UndefVarError: #hard_work not defined +# Stacktrace: +# [...] +# ```` + +# Unfortunately, if you try this code directly, you will get an error message +# that says `On worker 2: UndefVarError: hard_work not defined`. The error is +# thrown because, although process `1` knows what the `hard_work` function is, +# the worker processes do not. + +# To fix the error, we need to use `Distributed.@everywhere`, which evaluates +# the code on every process: + +Distributed.@everywhere begin + function hard_work(i::Int) + sleep(1.0) + return Distributed.myid() + end +end + +# Now if we run `pmap`, we see that it took only 1 second instead of 4, and that +# it executed on each of the worker processes: + +# ````julia +# julia> @time ids = Distributed.pmap(hard_work, 1:4) +# 1.202006 seconds (216.39 k allocations: 13.301 MiB, 4.07% compilation time) +# 4-element Vector{Int64}: +# 2 +# 3 +# 5 +# 4 +# ```` + +# !!! tip +# For more information, read the Julia documentation +# [Distributed Computing](https://docs.julialang.org/en/v1/manual/distributed-computing/). + +# ### Example: parameter search with distributed computing + +# With distributed computing, remember to evaluate all of the code on all of the +# processes using `Distributed.@everywhere`, and then write a function which +# creates a new instance of the model on every evaluation: + +Distributed.@everywhere begin + using JuMP + import HiGHS +end + +Distributed.@everywhere begin + function solve_model_with_right_hand_side(i) + model = Model(HiGHS.Optimizer) + set_silent(model) + @variable(model, x) + @objective(model, Min, x) + set_lower_bound(x, i) + optimize!(model) + assert_is_solved_and_feasible(sudoku) + return objective_value(model) + end +end + +# ```julia +# julia> solutions = Distributed.pmap(solve_model_with_right_hand_side, 1:10) +# 10-element Vector{Float64}: +# 1.0 +# 2.0 +# 3.0 +# 4.0 +# 5.0 +# 6.0 +# 7.0 +# 8.0 +# 9.0 +# 10.0 +# ```` + +# ## Parallelism within the solver + +# Many solvers use parallelism internally. For example, commercial solvers like +# [Gurobi.jl](@ref) and [CPLEX.jl](@ref) both parallelize the search in +# branch-and-bound. + +# Solvers supporting internal parallelism will typically support the +# [`MOI.NumberOfThreads`](@ref) attribute, which you can set using +# [`set_attribute`](@ref): + +# ```julia +# using JuMP +# import Gurobi +# model = Model(Gurobi.Optimizer) +# set_attribute(model, MOI.NumberOfThreads(), 4) +# ``` + +# ## GPU parallelism + +# JuMP does not support GPU programming, but some solvers support execution on a +# GPU. + +# One example is [SCS.jl](@ref), which supports using a GPU to internally solve +# a system of linear equations. If you are on `x86_64` Linux machine, do: +# ```julia +# using JuMP +# import SCS +# import SCS_GPU_jll +# model = Model(SCS.Optimizer) +# set_attribute(model, "linear_solver", SCS.GpuIndirectSolver) +# ``` diff --git a/docs/src/tutorials/algorithms/parallelism.md b/docs/src/tutorials/algorithms/parallelism.md deleted file mode 100644 index 6df7467c2e0..00000000000 --- a/docs/src/tutorials/algorithms/parallelism.md +++ /dev/null @@ -1,639 +0,0 @@ -# Parallelism - -The purpose of this tutorial is to give a brief overview of parallelism in -Julia as it pertains to JuMP, and to explain some of the things to be aware of -when writing parallel algorithms involving JuMP models. - -## Overview - -There are two main types of parallelism in Julia: - - 1. Multi-threading - 2. Distributed computing - -In multi-threading, multiple tasks are run in a single Julia process and share -the same memory. In distributed computing, tasks are run in multiple Julia -processes with independent memory spaces. This can include processes across -multiple physical machines, such as in a high-performance computing cluster. - -Choosing and understanding the type of parallelism you are using is important -because the code you write for each type is different, and there are different -limitations and benefits to each approach. However, the best choice is highly -problem dependent, so you may want to experiment with both approaches to -determine what works for your situation. - -## Multi-threading - -To use multi-threading with Julia, you must either start Julia with the -command line flag `--threads=N`, or you must set the `JULIA_NUM_THREADS` -environment variable before launching Julia. For this documentation, we set -the environment variable to: - -````julia -julia> ENV["JULIA_NUM_THREADS"] -"4" -```` - -You can check how many threads are available using: - -````julia -julia> Threads.nthreads() -4 -```` - -The easiest way to use multi-threading in Julia is by placing the -`Threads.@threads` macro in front of a `for`-loop: - -````julia -julia> @time begin - ids = Int[] - my_lock = Threads.ReentrantLock() - Threads.@threads for i in 1:Threads.nthreads() - global ids, my_lock - Threads.lock(my_lock) do - push!(ids, Threads.threadid()) - end - sleep(1.0) - end - end - 1.037087 seconds (31.32 k allocations: 1.836 MiB, 2.02% compilation time) -```` - -This for-loop sleeps for `1` second on each iteration. Thus, if it had -executed sequentially, it should have taken the same number of seconds as -there are threads available. Instead, it took only 1 second, showing that the -iterations were executed simultaneously. We can verify this by checking the -`Threads.threadid()` of the thread that executed each iteration: - -````julia -julia> ids -4-element Vector{Int64}: - 2 - 4 - 1 - 3 -```` - -!!! danger - The `Threads.threadid()` that a task runs on may change during execution. - Therefore, it is not safe to use `Threads.threadid()` to index into, say, a - vector of buffer or stateful objects. As an example, do not do: - ```julia - x = rand(Threads.nthreads()) - Threads.@threads for i in 1:Threads.nthreads() - x[Threads.threadid()] *= 2 # Danger! This use of threadid is not safe - end - ``` - For more information, read - [PSA: thread-local state is no longer recommended](https://julialang.org/blog/2023/07/PSA-dont-use-threadid/). - -### Data races - -When working with threads, you must avoid data races. A data race occurs when -multiple threads access the same variable at the same time, at least one thread -modifies the variable, and the order of the reads and writes are not properly -coordinated. - -Here's an example of a data race: -````julia -julia> begin - x = Ref(0) - Threads.@threads for i in 1:Threads.nthreads() - for i in 1:1_000 - x[] += 1 - end - end - x[] - end -1106 -```` -The expected answer is `4_000` (because there are four threads each incrementing -1,000 times), but the actual result is much smaller. Moreover, the result is -non-deterministic; if we re-ran this code, we would get a different value each -time. - -We got the wrong answer because multiple threads are reading and writing `x` -at the same time without coordination. For example, the following sequence -could occur: - - * Assume `x[]` currently has a value of `3` - * Thread A reads `x[]` to get `3` - * Thread B reads `x[]` to get `3` - * Thread A writes `x[] = 3 + 1 = 4` - * Thread B writes `x[] = 3 + 1 = 4` - * The final value of `x[]` is `4` - -The write from Thread A is overwritten, and so the value of `x[]` has increased -by `1` instead of `2`. - -Similar to the earlier `ids` example, we can fix this data race using a -`ReentrantLock`. The lock ensures that only one thread can update (read and then -write) `x` at a time. Now we get the correct answer: -````julia -julia> begin - x = Ref(0) - l = ReentrantLock() - Threads.@threads for i in 1:Threads.nthreads() - for i in 1:1_000 - lock(l) do - x[] += 1 - end - end - end - x[] - end -4000 -```` - -With the lock, the sequence of events goes something like: - - * Assume `x[]` currently has a value of `3` - * Thread A acquires the lock - * Thread A reads `x[]` to get `3` - * Thread B asks for the lock, but is denied because A is currently using it - * Thread A writes `x[] = 3 + 1 = 4` - * Thread A releases the lock - * Thread B acquires the lock - * Thread B reads `x[]` to get `4` - * Thread B writes `x[] = 4 + 1 = 5` - * Thread B releases the lock - * The final value of `x[]` is `5` - -See the [Multi-threading](https://docs.julialang.org/en/v1/manual/multi-threading/) -section of the Julia documentation for more details. - -### JuMP models are not thread-safe - -An object is thread-safe if it can be modified by separate threads without -causing a data race. JuMP models are not thread-safe. Code that uses -multi-threading to simultaneously modify or optimize a single JuMP model -across threads may error, crash Julia, or silently produce incorrect results. - -For example, the following incorrect use of multi-threading crashes Julia: -```julia -julia> using JuMP, HiGHS - -julia> function an_incorrect_way_to_use_threading() - model = Model(HiGHS.Optimizer) - set_silent(model) - @variable(model, x) - Threads.@threads for i in 1:10 - optimize!(model) - end - return - end -an_incorrect_way_to_use_threading (generic function with 1 method) - -julia> an_incorrect_way_to_use_threading() -julia(76918,0x16c92f000) malloc: *** error for object 0x600003e52220: pointer being freed was not allocated -zsh: abort julia -t 4 -``` - -To avoid issues with thread safety, create a new instance of a JuMP model in -each iteration of the for-loop. In addition, you must avoid race conditions in -the rest of your Julia code, for example, by using a lock when pushing elements -to a shared vector. - -### Thread safety and the closure capture bug - -!!! danger - This section is very important to understand. It is not specific to JuMP and - it applies to all multithreaded Julia programs. - -There is an upstream design issue in Julia ([julia#14948](https://github.com/JuliaLang/julia/issues/14948)) -that can silently introduce race conditions to your code and violate thread -safety. - -You can trigger this bug if you have a local variable inside the -`Threads.@threads` loop with the same name as a variable outside the loop. -Here's an example: - -```julia -julia> using JuMP, HiGHS - -julia> function _create_model(j) - model = Model(HiGHS.Optimizer) - @variable(model, x[1:j]) - return model - end -_create_model (generic function with 1 method) - -julia> function dont_run_segfault_likely() - models = _create_model.(1:2) - Threads.@threads for j in 1:2 - model = models[j] # `model` is used inside the loop - set_lower_bound.(model[:x], j) - optimize!(model) - end - model = models[1] # `model` is used outside the loop - end -dont_run_segfault_likely (generic function with 1 method) -``` - -This code is problematic for the following reason. Because `model` appears -both inside `Threads.@threads` and outside, Julia's scoping rules treat it as -a single variable. Therefore, instead of creating a different `model` for each -thread, Julia creates a single mutable container called a `Core.Box` that is -used to store the value of `model` throughout the life of the function. Now -there is a race condition for reads and writes of `model`, and so a thread may -read the value of `model` only to find that its `model` was overwritten with the -value of `model` from another thread. - -To diagnose this issue, use `@code_warntype`. If your code is problematic, you -will see a local variable with the type `::Core.Box`: -```julia -julia> @code_warntype dont_run_segfault_likely() -MethodInstance for dont_run_segfault_likely() - from dont_run_segfault_likely() @ Main REPL[3]:1 -Arguments - ... -Locals - ... - model::Core.Box - ... -``` -If you see `Core.Box`, you must refactor your code to avoid re-using the same -variable name inside and outside the threading loop. - -Alternatively, you can annotate the local variables inside the loop with `local` -to disambiguate them from the variables outside the loop. - -```julia -julia> using JuMP, HiGHS - -julia> function _create_model(j) - model = Model(HiGHS.Optimizer) - @variable(model, x[1:j]) - return model - end -_create_model (generic function with 1 method) - -julia> function safe_to_run() - models = _create_model.(1:2) - Threads.@threads for j in 1:2 - local model = models[j] # annotated as `local` - set_lower_bound.(model[:x], j) - optimize!(model) - end - model = models[1] # This `model` is not the inner `model` - end -safe_to_run (generic function with 1 method) -``` - -### Example: parameter search with multi-threading - -Here is an example of how to use multi-threading to solve a collection of JuMP -models in parallel. -````julia -julia> using JuMP, HiGHS - -julia> function a_good_way_to_use_threading() - solutions = Pair{Int,Float64}[] - my_lock = Threads.ReentrantLock(); - Threads.@threads for i in 1:10 - model = Model(HiGHS.Optimizer) - set_silent(model) - set_attribute(model, MOI.NumberOfThreads(), 1) - @variable(model, x >= i) - @objective(model, Min, x) - optimize!(model) - assert_is_solved_and_feasible(model) - Threads.lock(my_lock) do - push!(solutions, i => objective_value(model)) - end - end - return solutions - end -a_good_way_to_use_threading (generic function with 1 method) - -julia> a_good_way_to_use_threading() -10-element Vector{Pair{Int64, Float64}}: - 7 => 7.0 - 9 => 9.0 - 4 => 4.0 - 1 => 1.0 - 5 => 5.0 - 2 => 2.0 - 8 => 8.0 - 10 => 10.0 - 3 => 3.0 - 6 => 6.0 -```` - -!!! warning - For some solvers, it may be necessary to limit the number of threads used - internally by the solver to 1 by setting the [`MOI.NumberOfThreads`](@ref) - attribute. - -### Example: building data structures in parallel - -For large problems, building the model in JuMP can be a bottleneck, and you -may consider trying to write code that builds the model in parallel, for -example, by wrapping a `for`-loop that adds constraints with -`Threads.@threads`. Here's an example: - -```julia -julia> using JuMP - -julia> function an_incorrect_way_to_build_with_multithreading() - model = Model() - @variable(model, x[1:10]) - Threads.@threads for i in 1:10 - @constraint(model, x[i] <= i) - end - return model - end - -julia> an_incorrect_way_to_build_with_multithreading() -A JuMP Model -├ solver: none -├ objective_sense: FEASIBILITY_SENSE -├ num_variables: 10 -├ num_constraints: 7 -│ └ AffExpr in MOI.LessThan{Float64}: 7 -└ Names registered in the model - └ :x -``` - -Unfortunately, this model is wrong. It has only seven constraints instead of the -expected ten. This happens because JuMP models are not thread-safe. Code that -uses multi-threading to simultaneously modify or optimize a single JuMP model -across threads may error, crash Julia, or silently produce incorrect results. - -The correct way to build a JuMP model with multi-threading is to build the -data structures in parallel, but add them to the JuMP model in a thread-safe -way: -```julia -julia> using JuMP - -julia> function a_correct_way_to_build_with_multithreading() - model = Model() - @variable(model, x[1:10]) - my_lock = Threads.ReentrantLock() - Threads.@threads for i in 1:10 - con = @build_constraint(x[i] <= i) - Threads.lock(my_lock) do - add_constraint(model, con) - end - end - return model - end - -julia> a_correct_way_to_build_with_multithreading() -A JuMP Model -├ solver: none -├ objective_sense: FEASIBILITY_SENSE -├ num_variables: 10 -├ num_constraints: 10 -│ └ AffExpr in MOI.LessThan{Float64}: 10 -└ Names registered in the model - └ :x -``` - -!!! warning - **Do not use multi-threading to build a JuMP model just because your original - code is slow.** In most cases, we find that the reason for the bottleneck is - not JuMP, but in how you are constructing the problem data, and that with - changes, it is possible to build a model in a way that is not the bottleneck - in the solution process. If you need help to make your code run faster, ask - for help on the [community forum](https://jump.dev/forum). Make sure to - include a reproducible example of your code. - -## Example: using Channels - -Here's an example where we split the model building from the model solution. -Instead of using an explicit `ReentrantLock`, we use a `Channel` to store the -solutions. - -```julia -julia> using JuMP - -julia> import HiGHS - -julia> function build_model(s::Int; N::Int = 80) - model = Model(HiGHS.Optimizer) - set_silent(model) - @variable(model, x[1:N], Bin) - @variable(model, y[1:N], Bin) - @variable(model, z[1:N, 1:N] >= 0) - @constraint(model, [i in 1:N, j in 1:N], z[i, j] <= x[i]) - @constraint(model, [i in 1:N, j in 1:N], z[i, j] <= y[j]) - @constraint(model, [i in 1:N, j in 1:N], z[i, j] >= x[i] + y[j] - 1) - @objective(model, Min, sum(rand(-10:10) * i for i in z)) - set_time_limit_sec(model, s) - return model - end -build_model (generic function with 1 method) - -julia> struct Solution - scenario::Int - objective_value::Float64 - end - -julia> function solve_model(ch::Channel{Solution}, s::Int, model::Model) - optimize!(model) - @assert has_values(model) # There is always the trivial solution - put!(ch, Solution(s, objective_value(model))) - return - end -solve_model (generic function with 1 method) - -julia> function run_channel_example(S::Int) - models = Vector{Model}(undef, S) - Threads.@threads for s in 1:S - models[s] = build_model(s) - end - ch = Channel{Solution}() - for (s, model) in enumerate(models) - Threads.@spawn solve_model(ch, s, model) - end - for i in 1:S - solution = take!(ch) - println("s=$(solution) [solved $i/$S]") - end - return - end -run_channel_example (generic function with 1 method) - -julia> run_channel_example(15) -s=Solution(1, -380.0) [solved 1/15] -s=Solution(3, -73.0) [solved 2/15] -s=Solution(4, -229.0) [solved 3/15] -s=Solution(10, -195.0) [solved 4/15] -s=Solution(8, 0.0) [solved 5/15] -s=Solution(14, -434.0) [solved 6/15] -s=Solution(12, -483.0) [solved 7/15] -s=Solution(7, -315.0) [solved 8/15] -s=Solution(2, -696.0) [solved 9/15] -s=Solution(9, -116.0) [solved 10/15] -s=Solution(15, -471.0) [solved 11/15] -s=Solution(11, -518.0) [solved 12/15] -s=Solution(6, -37.0) [solved 13/15] -s=Solution(5, 0.0) [solved 14/15] -s=Solution(13, -390.0) [solved 15/15] -``` - -## Distributed computing - -To use distributed computing with Julia, use the `Distributed` package: - -````julia -julia> import Distributed -```` - -Like multi-threading, we need to tell Julia how many processes to add. We can -do this either by starting Julia with the `-p N` command line argument, or by -using `Distributed.addprocs`: - -````julia -julia> import Pkg - -julia> project = Pkg.project(); - -julia> workers = Distributed.addprocs(4; exeflags = "--project=$(project.path)") -4-element Vector{Int64}: - 2 - 3 - 4 - 5 -```` - -!!! warning - Not loading the parent environment with `--project` is a common mistake. - -The added processes are "worker" processes that we can use to do computation -with. They are orchestrated by the process with the id `1`. You can check -what process the code is currently running on using `Distributed.myid()` - -````julia -julia> Distributed.myid() -1 -```` - -As a general rule, to get maximum performance you should add as many processes -as you have logical cores available. - -Unlike the `for`-loop approach of multi-threading, distributed computing -extends the Julia `map` function to a "parallel-map" function -`Distributed.pmap`. For each element in the list of arguments to map over, -Julia will copy the element to an idle worker process and evaluate the -function, passing the element as an input argument. - -````julia -julia> function hard_work(i::Int) - sleep(1.0) - return Distributed.myid() - end -hard_work (generic function with 1 method) - -julia> Distributed.pmap(hard_work, 1:4) -ERROR: On worker 2: -UndefVarError: #hard_work not defined -Stacktrace: -[...] -```` - -Unfortunately, if you try this code directly, you will get an error message -that says `On worker 2: UndefVarError: hard_work not defined`. The error is -thrown because, although process `1` knows what the `hard_work` function is, -the worker processes do not. - -To fix the error, we need to use `Distributed.@everywhere`, which evaluates -the code on every process: - -````julia -julia> Distributed.@everywhere begin - function hard_work(i::Int) - sleep(1.0) - return Distributed.myid() - end - end -```` - -Now if we run `pmap`, we see that it took only 1 second instead of 4, and that -it executed on each of the worker processes: - -````julia -julia> @time ids = Distributed.pmap(hard_work, 1:4) - 1.202006 seconds (216.39 k allocations: 13.301 MiB, 4.07% compilation time) -4-element Vector{Int64}: - 2 - 3 - 5 - 4 -```` - -!!! tip - For more information, read the Julia documentation - [Distributed Computing](https://docs.julialang.org/en/v1/manual/distributed-computing/). - -### Example: parameter search with distributed computing - -With distributed computing, remember to evaluate all of the code on all of the -processes using `Distributed.@everywhere`, and then write a function which -creates a new instance of the model on every evaluation: - -````julia -julia> Distributed.@everywhere begin - using JuMP - import HiGHS - end - -julia> Distributed.@everywhere begin - function solve_model_with_right_hand_side(i) - model = Model(HiGHS.Optimizer) - set_silent(model) - @variable(model, x) - @objective(model, Min, x) - set_lower_bound(x, i) - optimize!(model) - assert_is_solved_and_feasible(sudoku) - return objective_value(model) - end - end - -julia> solutions = Distributed.pmap(solve_model_with_right_hand_side, 1:10) -10-element Vector{Float64}: - 1.0 - 2.0 - 3.0 - 4.0 - 5.0 - 6.0 - 7.0 - 8.0 - 9.0 - 10.0 -```` - -## Parallelism within the solver - -Many solvers use parallelism internally. For example, commercial solvers like -[Gurobi.jl](@ref) and [CPLEX.jl](@ref) both parallelize the search in -branch-and-bound. - -Solvers supporting internal parallelism will typically support the -[`MOI.NumberOfThreads`](@ref) attribute, which you can set using -[`set_attribute`](@ref): - -```julia -using JuMP -import Gurobi -model = Model(Gurobi.Optimizer) -set_attribute(model, MOI.NumberOfThreads(), 4) -``` - -## GPU parallelism - -JuMP does not support GPU programming, but some solvers support execution on a -GPU. - -One example is [SCS.jl](@ref), which supports using a GPU to internally solve -a system of linear equations. If you are on `x86_64` Linux machine, do: -```julia -using JuMP -import SCS -import SCS_GPU_jll -model = Model(SCS.Optimizer) -set_attribute(model, "linear_solver", SCS.GpuIndirectSolver) -``` From a94d63b9cf88186a95dc765f186b4c0b634b526c Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 24 Apr 2026 15:39:12 +1200 Subject: [PATCH 2/6] Fix formatting --- docs/src/tutorials/algorithms/parallelism.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/tutorials/algorithms/parallelism.jl b/docs/src/tutorials/algorithms/parallelism.jl index 11c6c4aeaed..ed78e2bf6a7 100644 --- a/docs/src/tutorials/algorithms/parallelism.jl +++ b/docs/src/tutorials/algorithms/parallelism.jl @@ -53,6 +53,7 @@ Threads.nthreads() global ids, my_lock Threads.lock(my_lock) do push!(ids, Threads.threadid()) + return end sleep(1.0) end @@ -127,6 +128,7 @@ begin for i in 1:1_000 lock(l) do x[] += 1 + return end end end @@ -208,6 +210,7 @@ function dont_run_segfault_likely() optimize!(model) end model = models[1] # `model` is used outside the loop + return end # And indeed, running this code results in: @@ -282,6 +285,7 @@ function a_good_way_to_use_threading() assert_is_solved_and_feasible(model) Threads.lock(my_lock) do push!(solutions, i => objective_value(model)) + return end end return solutions @@ -335,6 +339,7 @@ function a_correct_way_to_build_with_multithreading() con = @build_constraint(x[i] <= i) Threads.lock(my_lock) do add_constraint(model, con) + return end end return model From 0bbd66784db9e67ca7f9d7d77847b7c6bf5463ea Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 24 Apr 2026 17:06:52 +1200 Subject: [PATCH 3/6] Update --- docs/src/tutorials/algorithms/parallelism.jl | 58 +++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/docs/src/tutorials/algorithms/parallelism.jl b/docs/src/tutorials/algorithms/parallelism.jl index ed78e2bf6a7..61897cdacce 100644 --- a/docs/src/tutorials/algorithms/parallelism.jl +++ b/docs/src/tutorials/algorithms/parallelism.jl @@ -406,7 +406,9 @@ run_channel_example(15) # To use distributed computing with Julia, use the `Distributed` package: -import Distributed +# ```julia +# julia> import Distributed +# ``` # Like multi-threading, we need to tell Julia how many processes to add. We can # do this either by starting Julia with the `-p N` command line argument, or by @@ -432,7 +434,10 @@ import Distributed # with. They are orchestrated by the process with the id `1`. You can check # what process the code is currently running on using `Distributed.myid()` -Distributed.myid() +# ```julia +# julia> Distributed.myid() +# 1 +# ``` # As a general rule, to get maximum performance you should add as many processes # as you have logical cores available. @@ -465,12 +470,14 @@ Distributed.myid() # To fix the error, we need to use `Distributed.@everywhere`, which evaluates # the code on every process: -Distributed.@everywhere begin - function hard_work(i::Int) - sleep(1.0) - return Distributed.myid() - end -end +# ```julia +# julia> Distributed.@everywhere begin +# function hard_work(i::Int) +# sleep(1.0) +# return Distributed.myid() +# end +# end +# ``` # Now if we run `pmap`, we see that it took only 1 second instead of 4, and that # it executed on each of the worker processes: @@ -495,25 +502,24 @@ end # processes using `Distributed.@everywhere`, and then write a function which # creates a new instance of the model on every evaluation: -Distributed.@everywhere begin - using JuMP - import HiGHS -end - -Distributed.@everywhere begin - function solve_model_with_right_hand_side(i) - model = Model(HiGHS.Optimizer) - set_silent(model) - @variable(model, x) - @objective(model, Min, x) - set_lower_bound(x, i) - optimize!(model) - assert_is_solved_and_feasible(sudoku) - return objective_value(model) - end -end - # ```julia +# julia> Distributed.@everywhere begin +# using JuMP +# import HiGHS +# end +# +# julia> Distributed.@everywhere begin +# function solve_model_with_right_hand_side(i) +# model = Model(HiGHS.Optimizer) +# set_silent(model) +# @variable(model, x) +# @objective(model, Min, x) +# set_lower_bound(x, i) +# optimize!(model) +# assert_is_solved_and_feasible(sudoku) +# return objective_value(model) +# end +# end # julia> solutions = Distributed.pmap(solve_model_with_right_hand_side, 1:10) # 10-element Vector{Float64}: # 1.0 From ef24449efe1d12e2c3693804fddb16fcd2ff5d4d Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 24 Apr 2026 20:37:02 +1200 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Oscar Dowson --- docs/src/tutorials/algorithms/parallelism.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/tutorials/algorithms/parallelism.jl b/docs/src/tutorials/algorithms/parallelism.jl index 61897cdacce..fd1573be2ca 100644 --- a/docs/src/tutorials/algorithms/parallelism.jl +++ b/docs/src/tutorials/algorithms/parallelism.jl @@ -520,6 +520,7 @@ run_channel_example(15) # return objective_value(model) # end # end +# # julia> solutions = Distributed.pmap(solve_model_with_right_hand_side, 1:10) # 10-element Vector{Float64}: # 1.0 @@ -532,7 +533,7 @@ run_channel_example(15) # 8.0 # 9.0 # 10.0 -# ```` +# # ## Parallelism within the solver From d0c4628e4d4f959f27bb74513587573fffc743f3 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Sat, 25 Apr 2026 08:14:25 +1200 Subject: [PATCH 5/6] Update code block formatting in parallelism.jl --- docs/src/tutorials/algorithms/parallelism.jl | 60 +++++++++++--------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/docs/src/tutorials/algorithms/parallelism.jl b/docs/src/tutorials/algorithms/parallelism.jl index fd1573be2ca..55f34621418 100644 --- a/docs/src/tutorials/algorithms/parallelism.jl +++ b/docs/src/tutorials/algorithms/parallelism.jl @@ -71,7 +71,7 @@ ids # The `Threads.threadid()` that a task runs on may change during execution. # Therefore, it is not safe to use `Threads.threadid()` to index into, say, # a vector of buffer or stateful objects. As an example, do not do: -# ```julia +# ```julia-repl # x = rand(Threads.nthreads()) # Threads.@threads for i in 1:Threads.nthreads() # x[Threads.threadid()] *= 2 # Danger! This use of threadid is not safe @@ -171,7 +171,7 @@ function an_incorrect_way_to_use_threading() return end -# ```julia +# ```julia-repl # julia> an_incorrect_way_to_use_threading() # julia(76918,0x16c92f000) malloc: *** error for object 0x600003e52220: pointer being freed was not allocated # zsh: abort julia -t 4 @@ -214,7 +214,7 @@ function dont_run_segfault_likely() end # And indeed, running this code results in: -# ```julia +# ```julia-repl # julia> dont_run_segfault_likely() # julia(67421,0x170d83000) malloc: *** error for object 0x600003192870: pointer being freed was not allocated # julia(67421,0x170d83000) malloc: *** set a breakpoint in malloc_error_break to debug @@ -231,7 +231,7 @@ end # To diagnose this issue, use `@code_warntype`. If your code is problematic, you # will see a local variable with the type `::Core.Box`: -# ```julia +# ```julia-repl # julia> @code_warntype dont_run_segfault_likely() # MethodInstance for dont_run_segfault_likely() # from dont_run_segfault_likely() @ Main REPL[3]:1 @@ -250,6 +250,7 @@ end function _create_model(j) model = Model(HiGHS.Optimizer) + set_silent(model) @variable(model, x[1:j]) return model end @@ -316,7 +317,7 @@ end # This code errors (although on same Julia versions it may just return a model # that is missing some constraints): -# ```julia +# ```julia-repl # julia> an_incorrect_way_to_build_with_multithreading() # ERROR: TaskFailedException # @@ -406,7 +407,7 @@ run_channel_example(15) # To use distributed computing with Julia, use the `Distributed` package: -# ```julia +# ```julia-repl # julia> import Distributed # ``` @@ -414,7 +415,7 @@ run_channel_example(15) # do this either by starting Julia with the `-p N` command line argument, or by # using `Distributed.addprocs`: -# ````julia +# ````julia-repl # julia> import Pkg # julia> project = Pkg.project(); @@ -434,10 +435,10 @@ run_channel_example(15) # with. They are orchestrated by the process with the id `1`. You can check # what process the code is currently running on using `Distributed.myid()` -# ```julia +# ````julia-repl # julia> Distributed.myid() # 1 -# ``` +# ```` # As a general rule, to get maximum performance you should add as many processes # as you have logical cores available. @@ -448,7 +449,7 @@ run_channel_example(15) # Julia will copy the element to an idle worker process and evaluate the # function, passing the element as an input argument. -# ````julia +# ````julia-repl # julia> function hard_work(i::Int) # sleep(1.0) # return Distributed.myid() @@ -470,19 +471,19 @@ run_channel_example(15) # To fix the error, we need to use `Distributed.@everywhere`, which evaluates # the code on every process: -# ```julia +# ````julia-repl # julia> Distributed.@everywhere begin # function hard_work(i::Int) # sleep(1.0) # return Distributed.myid() # end # end -# ``` +# ```` # Now if we run `pmap`, we see that it took only 1 second instead of 4, and that # it executed on each of the worker processes: -# ````julia +# ````julia-repl # julia> @time ids = Distributed.pmap(hard_work, 1:4) # 1.202006 seconds (216.39 k allocations: 13.301 MiB, 4.07% compilation time) # 4-element Vector{Int64}: @@ -502,7 +503,7 @@ run_channel_example(15) # processes using `Distributed.@everywhere`, and then write a function which # creates a new instance of the model on every evaluation: -# ```julia +# ````julia-repl # julia> Distributed.@everywhere begin # using JuMP # import HiGHS @@ -533,7 +534,7 @@ run_channel_example(15) # 8.0 # 9.0 # 10.0 -# +# ```` # ## Parallelism within the solver @@ -545,11 +546,14 @@ run_channel_example(15) # [`MOI.NumberOfThreads`](@ref) attribute, which you can set using # [`set_attribute`](@ref): -# ```julia -# using JuMP -# import Gurobi -# model = Model(Gurobi.Optimizer) -# set_attribute(model, MOI.NumberOfThreads(), 4) +# ```julia-repl +# julia> using JuMP +# +# julia> import Gurobi +# +# julia> model = Model(Gurobi.Optimizer); +# +# julia> set_attribute(model, MOI.NumberOfThreads(), 4) # ``` # ## GPU parallelism @@ -559,10 +563,14 @@ run_channel_example(15) # One example is [SCS.jl](@ref), which supports using a GPU to internally solve # a system of linear equations. If you are on `x86_64` Linux machine, do: -# ```julia -# using JuMP -# import SCS -# import SCS_GPU_jll -# model = Model(SCS.Optimizer) -# set_attribute(model, "linear_solver", SCS.GpuIndirectSolver) +# ```julia-repl +# julia> using JuMP +# +# julia> import SCS +# +# julia> import SCS_GPU_jll +# +# julia> model = Model(SCS.Optimizer); +# +# julia> set_attribute(model, "linear_solver", SCS.GpuIndirectSolver) # ``` From 5605c9c65444368460701a28235db7ce1dc53c4f Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Sat, 25 Apr 2026 15:56:15 +1200 Subject: [PATCH 6/6] Update parallelism.jl --- docs/src/tutorials/algorithms/parallelism.jl | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/src/tutorials/algorithms/parallelism.jl b/docs/src/tutorials/algorithms/parallelism.jl index 55f34621418..e6e9346a432 100644 --- a/docs/src/tutorials/algorithms/parallelism.jl +++ b/docs/src/tutorials/algorithms/parallelism.jl @@ -71,7 +71,7 @@ ids # The `Threads.threadid()` that a task runs on may change during execution. # Therefore, it is not safe to use `Threads.threadid()` to index into, say, # a vector of buffer or stateful objects. As an example, do not do: -# ```julia-repl +# ```julia # x = rand(Threads.nthreads()) # Threads.@threads for i in 1:Threads.nthreads() # x[Threads.threadid()] *= 2 # Danger! This use of threadid is not safe @@ -171,7 +171,7 @@ function an_incorrect_way_to_use_threading() return end -# ```julia-repl +# ```julia # julia> an_incorrect_way_to_use_threading() # julia(76918,0x16c92f000) malloc: *** error for object 0x600003e52220: pointer being freed was not allocated # zsh: abort julia -t 4 @@ -214,7 +214,7 @@ function dont_run_segfault_likely() end # And indeed, running this code results in: -# ```julia-repl +# ```julia # julia> dont_run_segfault_likely() # julia(67421,0x170d83000) malloc: *** error for object 0x600003192870: pointer being freed was not allocated # julia(67421,0x170d83000) malloc: *** set a breakpoint in malloc_error_break to debug @@ -231,7 +231,7 @@ end # To diagnose this issue, use `@code_warntype`. If your code is problematic, you # will see a local variable with the type `::Core.Box`: -# ```julia-repl +# ```julia # julia> @code_warntype dont_run_segfault_likely() # MethodInstance for dont_run_segfault_likely() # from dont_run_segfault_likely() @ Main REPL[3]:1 @@ -317,7 +317,7 @@ end # This code errors (although on same Julia versions it may just return a model # that is missing some constraints): -# ```julia-repl +# ```julia # julia> an_incorrect_way_to_build_with_multithreading() # ERROR: TaskFailedException # @@ -407,7 +407,7 @@ run_channel_example(15) # To use distributed computing with Julia, use the `Distributed` package: -# ```julia-repl +# ```julia # julia> import Distributed # ``` @@ -415,7 +415,7 @@ run_channel_example(15) # do this either by starting Julia with the `-p N` command line argument, or by # using `Distributed.addprocs`: -# ````julia-repl +# ````julia # julia> import Pkg # julia> project = Pkg.project(); @@ -435,7 +435,7 @@ run_channel_example(15) # with. They are orchestrated by the process with the id `1`. You can check # what process the code is currently running on using `Distributed.myid()` -# ````julia-repl +# ````julia # julia> Distributed.myid() # 1 # ```` @@ -449,7 +449,7 @@ run_channel_example(15) # Julia will copy the element to an idle worker process and evaluate the # function, passing the element as an input argument. -# ````julia-repl +# ````julia # julia> function hard_work(i::Int) # sleep(1.0) # return Distributed.myid() @@ -471,7 +471,7 @@ run_channel_example(15) # To fix the error, we need to use `Distributed.@everywhere`, which evaluates # the code on every process: -# ````julia-repl +# ````julia # julia> Distributed.@everywhere begin # function hard_work(i::Int) # sleep(1.0) @@ -483,7 +483,7 @@ run_channel_example(15) # Now if we run `pmap`, we see that it took only 1 second instead of 4, and that # it executed on each of the worker processes: -# ````julia-repl +# ````julia # julia> @time ids = Distributed.pmap(hard_work, 1:4) # 1.202006 seconds (216.39 k allocations: 13.301 MiB, 4.07% compilation time) # 4-element Vector{Int64}: @@ -503,7 +503,7 @@ run_channel_example(15) # processes using `Distributed.@everywhere`, and then write a function which # creates a new instance of the model on every evaluation: -# ````julia-repl +# ````julia # julia> Distributed.@everywhere begin # using JuMP # import HiGHS @@ -546,7 +546,7 @@ run_channel_example(15) # [`MOI.NumberOfThreads`](@ref) attribute, which you can set using # [`set_attribute`](@ref): -# ```julia-repl +# ```julia # julia> using JuMP # # julia> import Gurobi @@ -563,7 +563,7 @@ run_channel_example(15) # One example is [SCS.jl](@ref), which supports using a GPU to internally solve # a system of linear equations. If you are on `x86_64` Linux machine, do: -# ```julia-repl +# ```julia # julia> using JuMP # # julia> import SCS