Utilities#
Linear algebra backends#
Bramble.backend
source
backend(Wₕ::AbstractSpaceType) -> AbstractBackend
Returns the computational backend associated with the space Wₕ.
Bramble.Backend
source
Backend{VT, MT, EP}()
Compile-time descriptor specifying vector type VT, matrix type MT, and execution policy EP.
Type parameters
VT<:DenseVector: Concrete dense vector type (for CPU or GPU).MT<:AbstractMatrix: Concrete matrix type (e.g.SparseMatrixCSC{Float64, Int}orMatrix{Float64}).EP<:ExecutionPolicy: AnExecutionPolicy, for exampleSerial,Parallel,CpuPolyesterorGpuKernel.
All three parameters must agree on locality: a backend is meant to be wholly host – a Vector alongside a CPU sparse or dense matrix, run under a CpuPolicy – or wholly device – once a vendor is chosen, VT, MT and the policy are all that vendor's. Any other combination (a device VT with a host MT, a device VT with a CpuPolicy, or a host VT with a GpuPolicy) is rejected at construction rather than accepted and left to fail on first use.
Throws
ArgumentError:locality(VT) != locality(EP()).ArgumentError:locality(MT) != locality(VT).ArgumentError:EPis aGpuOffloadwhose wrapped device backend only supports an element type other thaneltype(VT).
See also: backend, vector_type, matrix_type, execution_policy, locality.
Bramble.Locality
source
Locality
Abstract supertype for where an array's storage lives: HostLocality for memory a CPU loop can index element by element, DeviceLocality for memory an accelerator schedules against instead.
Never declared directly – locality derives it from an array type, an ExecutionPolicy, or a Backend. So a Backend's storage and its execution policy answer the same question and can be checked against each other, rather than each independently claiming a locality that need not agree.
See also: locality, HostLocality, DeviceLocality.
Bramble.HostLocality
source
HostLocality() <: Locality
Storage a CPU loop can index element by element: Vector, Matrix, SparseMatrixCSC, and, by default, any array type locality has not been taught otherwise about.
See also: Locality, DeviceLocality, locality.
Bramble.DeviceLocality
source
DeviceLocality() <: Locality
Storage an accelerator schedules against rather than a CPU loop indexing it directly: MtlVector/MtlMatrix and any future GPU array type. Scalar indexing such storage from a host loop is the failure this trait exists to catch before it happens.
See also: Locality, HostLocality, locality.
Bramble.locality
source
locality(x) -> Locality
Return the Locality of x – an array type, an ExecutionPolicy, or a Backend.
Locality is derived from storage, never declared. The fallback method, locality(::Type{<:AbstractArray}) = HostLocality(), treats any array type this package has not been taught otherwise about as host memory. Any array type subtyping GPUArraysCore.AbstractGPUArray answers DeviceLocality() generically, so a GPU package extension need not add its own method for that to hold – Metal's MtlVector/MtlMatrix already subtype it, and any future CUDA/ROCm/oneAPI array type gets the right answer the same way, for free. BrambleMetalExt keeps its own MtlVector/MtlMatrix/sparse-matrix methods regardless: Julia dispatches to the more specific method, and those predate this generic one – the same idiom as ka_device: a method a package extension supplies for its own type, picked up automatically once loaded.
For an ExecutionPolicy, locality answers what the policy claims. A CpuPolicy claims HostLocality and a GpuPolicy claims DeviceLocality. For a Backend, it answers the locality of the backend's vector type VT alone, because VT is the storage a sweep actually indexes. The matrix type MT is not consulted here, since Backend's constructor checks MT against VT separately.
Examples
using Bramble
const B = Bramble
B.locality(Vector{Float64}) === B.HostLocality() &&
B.locality(B.CpuSerial()) === B.HostLocality() &&
B.locality(B.GpuKernel()) === B.DeviceLocality()
# output
true
See also: Locality, HostLocality, DeviceLocality, ka_device.
Bramble.ExecutionPolicy
source
ExecutionPolicy
Abstract supertype for backend execution policies.
Two regimes sit under it: CpuPolicy for work a CPU loop drives, GpuPolicy for work a device schedules. A policy says where the work runs: CpuSerial walks the array element by element on one CPU thread, the one thing a device array refuses.
Bramble.CpuPolicy
source
CpuPolicy <: ExecutionPolicy
Abstract supertype for the policies a CPU loop executes: CpuSerial, CpuThreaded and CpuPolyester.
The CPU sweeps dispatch on this rather than on each concrete policy, so a GpuPolicy reaching one is a method error with a message rather than a scalar-indexing failure several frames in.
Bramble.CpuSerial
source
CpuSerial() <: CpuPolicy
Sequential execution policy.
Directs grid operations and form assembly to execute via single-threaded loops. This is the default execution policy.
Also spelled Serial(): const Serial = CpuSerial.
The grid sizes at which CpuThreaded and CpuPolyester start beating this policy were measured per workload rather than assumed – see their own docstrings for the numbers.
See also: CpuThreaded, ExecutionPolicy.
Bramble.CpuThreaded
source
CpuThreaded() <: CpuPolicy
Multithreaded execution policy.
Directs grid operations and form assembly to execute across CPU threads via static partitioning. Execution is unconditional: no per-call size thresholds are imposed. For workloads dominated by small, frequently repeated calls, use CpuSerial.
Also spelled Parallel(): const Parallel = CpuThreaded.
A call made from inside another threaded region, such as the body of a user's own Threads.@threads loop, or from a spawned task while another task's threaded region runs, executes serially on the calling task instead of throwing, and returns exactly what the same call returns at top level.
Base.Threads.@threads is the primitive, and naming it that way leaves room for the others that are not this one – Polyester's @batch and MPI. CpuPolyester is that Polyester-backed sibling: the policy type ships here, but the sweeps it selects live in the BramblePolyesterExt package extension, and requesting one without using Polyester errors the way metal_backend does without using Metal.
Where "small" ends differs widely between workloads (restriction, cell averaging, the innerₕ/normₕ reductions, D₋ₓ!, broadcasts), so a size from one does not transfer to another, and it depends on the machine. benchmark/policy_crossover.jl measures it per workload. innerₕ/normₕ (_dot/_dot_masked, src/utils/linear_algebra.jl), D₋ₓ! and the warmed broadcast axpy vₕ .= a .* uₕ .+ wₕ into a VectorElement thread under this policy.
See also: CpuSerial, CpuPolyester, ExecutionPolicy.
Bramble.CpuPolyester
source
CpuPolyester() <: CpuPolicy
Polyester-batched execution policy.
Named for the provider, matching CpuSerial and CpuThreaded, so the name makes the using Polyester requirement self-evident.
Directs grid operations and form assembly through Polyester.jl's @batch, a primitive whose per-call overhead is low enough to pay off on grids where CpuThreaded's Threads.@threads does not. The sweeps this policy selects are implemented in the BramblePolyesterExt package extension, not here: using Polyester must be loaded before this policy reaches one of them, or the call errors naming the package, matching metal_backend's precedent. Parallel() means Threads.@threads.
Its crossover against CpuSerial falls at smaller sizes than CpuThreaded's for every workload measured, D₋ₓ! and the warmed broadcast axpy vₕ .= a .* uₕ .+ wₕ included. benchmark/policy_crossover.jl measures both on your machine.
Its reductions, innerₕ, normₕ and inner₊ with or without markers, split the sum into Threads.nthreads() fixed bands and add the band sums with sum, exactly as CpuThreaded does, and return CpuThreaded's value bitwise. The value depends only on the data and Threads.nthreads(), never on how many threads are free, so a reduction nested in another sweep, as in Rₕ!(uₕ, x -> innerₕ(vₕ, vₕ) * x[1]), returns exactly the top-level value. It agrees with CpuSerial to rounding, since the bands add the terms in another order, and on one thread it is CpuSerial's value bitwise. A task's first reduction allocates its band sums once.
A warm call under this policy allocates 0 bytes, on any grid, when the storage is a Vector or a contiguous view of one; storage that is not, such as a strided view, allocates on every call. Differences, shifts, averages, divergence, curl, εₕ!, Rₕ!, avgₕ! (masked and composite too), innerₕ, inner₊, the broadcasts (a 0-dimensional array leaf too), the weight build, linear and bilinear assembly and refill, the matrix-free product, a multigrid V-cycle, the explicit right-hand side (SemidiscretizeRHS) and the KroneckerLinearOperator product all allocate 0 B. Polyester copies what a loop captures into an argument box, and the box stays on the stack when it holds only plain arrays and isbits values. So the loops never capture a form, a space or a mesh: Bramble splits them into an isbits skeleton and their arrays, and each task rebuilds them. No call reaches Threads.@threads or Threads.@spawn.
A function you supply that captures an array, as in an avgₕ! whose source is a closure over a Vector, allocates nothing either, and sees its own objects: the loop holds a handle to a slot that stores the function, not the function. Only a function of a type beyond the 256 that have slots, a call that finds all 64 slots of its type taken (or retired by an interrupted call, for the session), and data the split cannot take apart, such as a BigFloat coefficient or a Dict, fall back to a heap-allocated box. That costs an amount per call that does not grow with the grid, and never changes the result. A task that throws rethrows on the caller with the exception type and text CpuSerial gives.
See also: CpuThreaded, CpuSerial, ExecutionPolicy.
Bramble.GpuOffload
source
GpuOffload{I, DB}() <: CpuPolicy
GpuOffload(device_backend::Backend, inner::CpuPolicy = CpuSerial()) -> GpuOffload
A CpuPolicy that wraps a host inner policy alongside a device Backend device_backend, for host-side Backends that offload individual calls (Rₕ!, avgₕ!) to the device rather than running them on the host.
Carries no runtime fields: I and DB are type parameters only, so a Backend{VT, MT, GpuOffload{I, DB}} still rebuilds EP() the way every other ExecutionPolicy does. locality answers HostLocality for it, as for any CpuPolicy: the Backend it configures is a host backend, and device_backend is only where the offloaded calls are dispatched, not this backend's own storage. execution_policy on a Backend configured with a GpuOffload policy returns I(), the wrapped inner policy instance, not the GpuOffload itself – every existing concrete-policy dispatch site sees the inner policy and behaves exactly as it does without a GpuOffload in play.
Throws
ArgumentError:GpuOffload(device_backend, ...)with adevice_backendwhoselocalityisHostLocality– nothing would be offloaded.ArgumentError: constructing aBackendwith aGpuOffload{I, DB}policy whose requested vector element type cannot be represented bydevice_backend's own vector element type (e.g.Float64requested against a Metaldevice_backend, which only supportsFloat32andFloat16).
See also: CpuPolicy, Backend, metal_backend, execution_policy.
Bramble.GpuPolicy
source
GpuPolicy <: ExecutionPolicy
Abstract supertype for the policies a device executes, currently GpuKernel.
See also: CpuPolicy, ExecutionPolicy.
Bramble.GpuKernel
source
GpuKernel() <: GpuPolicy
Device execution policy: work is dispatched to the accelerator rather than to a host loop.
What metal_backend carries by default. A GPU is massively parallel and cannot execute serially, so Serial() would be a false statement about the hardware and would send CPU assembly loops at device arrays.
The name states the launch mechanism only: work is dispatched to a device kernel. A kernel launch only enqueues onto the device's queue; a host read of the result, or ka_synchronize, is the barrier. There is no handle to wait on. Kernels on one queue run in its order, so a chain of operators needs no synchronise between its steps. The memory locality lives in the Locality trait, which this policy claims through locality.
See also: GpuPolicy, ExecutionPolicy.
Bramble.Serial
source
Serial
Alias for CpuSerial, the spelling the package's call sites, tests and benchmark baseline keys use.
Bramble.Parallel
source
Parallel
Alias for CpuThreaded.
Bramble.execution_policy
source
execution_policy(backend::Backend{<:Any, <:Any, EP}) -> EP
execution_policy(::Type{<:Backend{<:Any, <:Any, EP}}) -> EP
Return the ExecutionPolicy instance configured for backend.
For a GpuOffload backend this is the wrapped inner policy I(), not the GpuOffload wrapper, so every concrete-policy dispatch site sees the policy that runs the host work.
execution_policy(Ωₕ::AbstractMeshType) -> ExecutionPolicy
Return the ExecutionPolicy (Serial or Parallel) of the Backend associated with mesh Ωₕ.
Bramble.vector
source
vector(backend::Backend{VT}, n::Integer) -> VT
Allocate an uninitialized vector of length n using vector type VT configured in backend.
Arguments
backend: Target backend instance.n: Number of vector elements.
Which constructor is used is decided by supports_undef_construction(VT), from the type alone.
Throws
ErrorException: IfVTdoes not define the constructor its trait declares.
Bramble.matrix
source
matrix(backend::Backend{<:Any, MT}, n::Integer, m::Integer) -> MT
Allocate a matrix of dimensions n × m using matrix type MT configured in backend.
For dense matrix types, allocates uninitialized storage via MT(undef, n, m). For sparse matrix types (SparseMatrixCSC), allocates an empty sparse matrix via spzeros(T, Ti, n, m).
Arguments
backend: Target backend instance.n: Number of rows.m: Number of columns.
Which constructor is used is decided by supports_undef_construction(MT), from the type alone.
Throws
ErrorException: IfMTdoes not define the constructor its trait declares.
Bramble.vector_type
source
vector_type(backend::Backend{VT}) -> Type{VT}
vector_type(::Type{<:Backend{VT}}) -> Type{VT}
Return the vector type VT configured for backend.
Bramble.matrix_type
source
matrix_type(backend::Backend{<:Any, MT}) -> Type{MT}
matrix_type(::Type{<:Backend{<:Any, MT}}) -> Type{MT}
Return the matrix type MT configured for backend.
Bramble.backend_types
source
backend_types(backend::Backend{VT, MT, EP}) -> Tuple{Type, Type{VT}, Type{MT}, Type{Backend{VT, MT, EP}}}
backend_types(::Type{<:Backend{VT, MT, EP}}) -> Tuple{Type, Type{VT}, Type{MT}, Type{Backend{VT, MT, EP}}}
Return a 4-tuple containing (eltype(VT), VT, MT, Backend{VT, MT, EP}).
Bramble.supports_undef_construction
source
supports_undef_construction(::Type{AT}) -> Bool
Whether an array type builds from an undef initializer, AT(undef, dims...), rather than from its dimensions alone, AT(dims...).
The contract a custom array type opts into to be usable as a backend's vector_type or matrix_type. true by default, which is what every AbstractArray in Base and in every GPU package this repository has seen answers; a type constructed from its size alone declares otherwise:
Bramble.supports_undef_construction(::Type{<:MySizedArray}) = false
Read at compile time from the type, so the branch it guards folds away and the compiler sees through the allocation path. The three shipped backends never reach it – Vector, DenseMatrix and SparseMatrixCSC each have their own @inline method below – so it is the stated contract for the types that do.
Bramble.backend_eye
source
backend_eye(backend::Backend, n::Integer) -> AbstractMatrix
Construct an \(n \times n\) identity matrix matching the matrix type configured in backend.
Bramble.backend_zeros
source
backend_zeros(backend::Backend, n::Integer) -> AbstractMatrix
Construct an \(n \times n\) zero matrix matching the matrix type configured in backend.
Bramble.ka_device
source
ka_device(be) -> KernelAbstractions.Backend
Return the KernelAbstractions.jl device backend that runs kernels for the Bramble Backend be.
This is the extension contract every device backend implements: BrambleMetalExt adds a method for a Backend built from MtlVector/MtlMatrix, returning Metal.MetalBackend(), and a future GPU extension does the same for its own array types. Every @kernel declared in BrambleKernelAbstractionsExt is written against KernelAbstractions.Backend alone, so it becomes available on a new device the moment that device supplies this one method.
Throws
ErrorException: if no loaded package defineska_deviceforbe. Requiresusing KernelAbstractionsand the package that suppliesbe's device (for exampleusing Metalfor a Metal backend).
Bramble.ka_synchronize
source
ka_synchronize(x) -> Nothing
Block the calling thread until every KernelAbstractions.jl kernel and device transfer already queued against x's device backend has completed.
Under GpuKernel – the only GpuPolicy there is – a kernel launch only enqueues onto the device's own command queue and returns, so a chain of operators (D₋ₓ into D₋ᵧ, say) pipelines instead of paying a host round-trip after each step. Kernels enqueued on the same queue still run in that queue's order, so this is not needed between chained device calls, only at a genuine host boundary:
converting a device array to a host one (
Array(...),host_points, ...), or any other host-side read, reduction or assertion – though aGPUArraysreduction that returns a plain host scalar (sum, which_dot's device method insrc/utils/linear_algebra.jluses, and soinnerₕ/normₕtoo) already synchronises by fetching that scalar, with no call to this function needed;a write that reaches device memory some way other than a
@kernellaunch on that same queue – a plaincopyto!, which queues asynchronously exactly like a kernel launch does, but is not one of the launches above._flush_device_scatter!(src/assembly/bilinear_traversal.jl) is this function's first caller for exactly that reason: it ends a device-resident matrix's assembly withcopyto!(A.nzVal, mirror.nzval), and calling this right after is what keepsassemble/assemble!from returning before that write lands (a race found atn = 513over repeated assemblies, invisible at a small test size);an explicit call from user code that needs a hard barrier before doing something this package cannot see, such as timing a device computation in isolation.
Same idiom as ka_device: a helpful error naming the packages to load, not a bare MethodError, and the real method comes from BrambleKernelAbstractionsExt (synchronize(get_backend(x))) – written against KernelAbstractions alone, so a future GPU backend inherits it for free.
Throws
ErrorException: if no loaded package defineska_synchronizeforx. Requiresusing KernelAbstractions.
Bramble.metal_sparse_csr
source
metal_sparse_csr(A::SparseMatrixCSC) -> MetalSparseMatrixCSR
Convert a host SparseMatrixCSC to a sparse matrix in compressed sparse row (CSR) format, stored in Metal device memory.
Requires using Metal in the caller environment. The conversion is sparse-to-sparse throughout – A is never densified.
Arguments
A: The host sparse matrix to convert.
Throws
ErrorException: IfMetal.jlis not loaded.
Bramble.metal_sparse_csc
source
metal_sparse_csc(A::SparseMatrixCSC) -> MetalSparseMatrixCSC
Convert a host SparseMatrixCSC to a sparse matrix in compressed sparse column (CSC) format, stored in Metal device memory.
Requires using Metal in the caller environment. SparseMatrixCSC's own storage already is CSC, so no format conversion happens – its fields are placed in device memory as-is.
Arguments
A: The host sparse matrix to convert.
Throws
ErrorException: IfMetal.jlis not loaded.
Bramble.gpu_backend
source
gpu_backend(::Type{T} = Float32; policy::ExecutionPolicy = GpuKernel()) -> Backend
Construct a GPU Backend for whichever accelerator extension is loaded and has a functional device.
Detects a loaded GPU package extension with Base.get_extension, checks _gpu_functional for that device, and forwards to the backend's own constructor: currently metal_backend, under using Metal with Metal.functional() true. A CUDA or AMDGPU extension joins this dispatch once it exists.
Arguments
T: Floating-point element type (default:Float32).
Keywords
policy: Execution policy instance (default:GpuKernel).
Throws
ErrorException: two distinct diagnoses, deliberately not sharing a message. If no GPU extension is loaded at all, the message names the package to load, chosen from the host architecture: Metal on Apple Silicon, CUDA on Linux/Windows, and a generic "no GPU hardware" message otherwise. If a GPU extension is loaded but its device is not functional (e.g.using Metalon a host without working Metal drivers), the message says so instead – that is a driver, hardware or virtualisation problem, not a missingusing.
See also: metal_backend, backend.
Bramble.metal_backend
source
metal_backend(::Type{T} = Float32; policy::ExecutionPolicy = GpuKernel()) -> Backend
Construct a Metal GPU Backend backed by Metal.jl arrays.
Requires using Metal in the caller environment. Apple Silicon GPUs support Float32 and Float16, but do not support 64-bit floating point arithmetic.
A function projected on this backend has to be GPU-compilable. Rₕ(W, f) on a metal_backend space compiles f into a device kernel rather than calling it from a host loop, so f must be device-compilable: no Float64 literals (a bare literal like 0.5 forces double precision, which Apple Silicon GPUs do not support – write x[1] / 2, not x[1] * 0.5), no allocations, and no calls to a non-inlineable or host-only function. x -> sin(x[1]) compiles; anything capturing a boxed value or calling out to the host does not, and fails at compile time with a dynamic-invocation error rather than at the call site.
Arguments
T: Floating-point element type (Float32orFloat16, default:Float32).
Keywords
policy: Execution policy instance (default:GpuKernel). ACpuPolicyis rejected at construction, not accepted and left to fail on first use: Metal's device arrays answerDeviceLocalitytolocality, aCpuPolicyanswersHostLocality, andBackend's inner constructor requires the two to agree.
Throws
ErrorException: IfMetal.jlis not loaded.ArgumentError: Ifpolicyis aCpuPolicy(locality mismatch), or ifTis a numeric type other thanFloat16andFloat32(withMetal.jlloaded), such asFloat64.
Bramble.csr_backend
source
csr_backend(::Type{T} = Float64; policy::ExecutionPolicy = Serial()) -> Backend
Construct a SparseMatrixCSR Backend backed by SparseMatricesCSR.jl.
Requires using SparseMatricesCSR in the caller environment. A finite-difference stencil is assembled row by row, which compressed sparse row storage reaches without the column scatter a SparseMatrixCSC assembly needs.
Arguments
T: Coordinate and scalar element type (default:Float64).
Keywords
policy: Execution policy instance (default:Serial).
Throws
ErrorException: IfSparseMatricesCSR.jlis not loaded.
Bramble.profile_backends
source
profile_backends() -> BackendProfile
Time one sweep kernel under each execution policy this session can run on the host, for sizes 2^10, 2^12, ..., 2^22, and report where each policy overtakes Serial().
Each policy sweeps a vector through the same loop the package's own weight and scatter builds use, with an isbits kernel (i -> sqrt(i) * a + b): one warm run, then the best of three runs. The candidates are Serial(), Parallel(), and CpuPolyester() once using Polyester has loaded its extension. With using Metal on a functional device a GpuKernel() row is added, timed with a device synchronisation. Every row is timed on the same element type, so the rows compare: Float64, or Float32 for all of them once the Metal row is present (the only element type Metal offers), in which case the figures are not drop-in figures for a Float64 problem. The header of the table names the type. The host policies take well under a second including compilation. With Metal loaded, the first call is slow because it is often the session's first GPU operation, which compiles Metal's own code as well as the sweep kernel; loading Polyester before Metal makes that first GPU operation markedly slower (an upstream interaction), and loading it after Metal reduces it. Later calls take well under a second. It is meant to be called by hand, once per session, and is not run by backend or gridspace.
The result prints as a table of times, the crossover of each policy against Serial() (the first size from which it stays at least 1.2 times faster to the end of the sweep, or never) and the backend(policy = ...) spelling to use. The table times one sweep kernel, not assembly: a policy that wins here is a good first choice, not a guarantee for your form. The times are plain data in the eltype, sizes, labels and times fields.
Returns
BackendProfile:eltype,sizes, policylabels,spellingsand thetimesmatrix in seconds.
Examples
using Bramble
Bramble.profile_backends()
Matrix storage
profile_backends times execution policies only. It does not compare csr_backend with the default SparseMatrixCSC storage: the matrix type only matters during assembly and solves, and how much it matters depends on the form, the boundary conditions and the solver. To compare the two on your own problem, load SparseMatricesCSR, build the same mesh, space and form under each backend, and time assemble, assemble! and the solve you use:
using Bramble, SparseMatricesCSR, BenchmarkTools
Ω = domain(box((0.0, 0.0), (1.0, 1.0)))
for be in (backend(), csr_backend())
Wₕ = gridspace(mesh(Ω, (257, 257); backend = be))
a = form(Wₕ, Wₕ, (u, v) -> inner₊(∇ₕ(u), ∇ₕ(v)))
A = assemble(a; dirichlet = :boundary)
@btime assemble!($A, $a; dirichlet = :boundary)
end
Use the policy profile_backends recommended in both backends, e.g. csr_backend(; policy = Parallel()), so the comparison isolates the storage.
See also: backend, csr_backend, Parallel.
Bramble.BackendProfile
source
BackendProfile
Result of profile_backends: plain data, printed as a table.
Fields
eltype::DataType: Element type every policy was timed on.sizes::Vector{Int}: Number of points of each sweep.labels::Vector{String}: One label per policy; the first is theSerial()baseline.spellings::Vector{String}: The expression that selects each policy.times::Matrix{Float64}: Seconds,times[i, j]forsizes[i]under policyj.
Bramble.sparse_refactor!
source
sparse_refactor!(fact::Factorization, A::SparseMatrixCSC) -> Factorization
sparse_refactor!(fact::Factorization, a::BilinearForm; kwargs...) -> Factorization
Alias for refactor!. Recomputes numerical values of fact reusing the existing symbolic factorization.
Bramble.PRECOMPILE_WORKLOAD
source
Bramble.PRECOMPILE_WORKLOAD -> Bool
Whether the @compile_workload blocks – this file's own, and each package extension's (ext/*.jl, gpena/Bramble.jl#196) – run at precompile time. Read from the "precompile_workload" Preferences.jl key, defaulting to true.
using Preferences, Bramble
set_preferences!(Bramble, "precompile_workload" => false)
disables every workload, core and extensions alike, since they all gate on this same constant; the change takes effect on the next load, no manual cache clearing needed.
public, not exported: a real, cross-module contract every extension's own precompile gate reads, but not something an ordinary caller uses.