Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,34 @@ const README_PATH_MAPPINGS = [
(r"\(docs/safety\.md(#[^)]+)?\)", s"(basics/safety-rules.md\1)"),
]

"""
Inject Google Search Console verification meta tag into generated HTML files.
This is enabled only when `ENV["GOOGLE_SITE_VERIFICATION"]` is set.
"""
function inject_google_site_verification!(build_dir::String)
token = strip(get(ENV, "GOOGLE_SITE_VERIFICATION", ""))
isempty(token) && return

safe_token = replace(token, '"' => """)
meta_tag = "<meta name=\"google-site-verification\" content=\"$(safe_token)\" />"
injected = 0

for (root, _, files) in walkdir(build_dir)
for file in files
endswith(file, ".html") || continue
path = joinpath(root, file)
html = read(path, String)
occursin("google-site-verification", html) && continue
occursin("</head>", html) || continue

write_if_changed(path, replace(html, "</head>" => "$(meta_tag)\n</head>"; count = 1))
injected += 1
end
end

return @info "Injected google-site-verification meta tag" files = injected build_dir = build_dir
end

"""
Rewrite relative paths in README.md for Documenter structure.

Expand Down Expand Up @@ -74,9 +102,15 @@ makedocs(
sitename = "AdaptiveArrayPools.jl",
authors = "Min-Gu Yoo",
modules = [AdaptiveArrayPools],
# servedocs() sets root to docs/ which conflicts with project-root remotes.
# Enable GitHub source links only in CI where makedocs root matches git root.
remotes = get(ENV, "CI", nothing) == "true" ?
Dict(dirname(@__DIR__) => (Documenter.Remotes.GitHub("ProjectTorreyPines", "AdaptiveArrayPools.jl"), "master")) :
nothing,
format = Documenter.HTML(
prettyurls = get(ENV, "CI", nothing) == "true",
canonical = "https://projecttorreypines.github.io/AdaptiveArrayPools.jl",
edit_link = :commit,
assets = String[],
),
pages = [
Expand All @@ -92,6 +126,7 @@ makedocs(
"Multi-threading" => "features/multi-threading.md",
],
"Features" => [
"Pool Safety" => "features/safety.md",
"`@maybe_with_pool`" => "features/maybe-with-pool.md",
"Bit Arrays" => "features/bit-arrays.md",
"CUDA Support" => "features/cuda-support.md",
Expand All @@ -112,6 +147,8 @@ makedocs(
warnonly = [:cross_references, :missing_docs],
)

inject_google_site_verification!(joinpath(@__DIR__, "build"))

deploydocs(
repo = "github.com/ProjectTorreyPines/AdaptiveArrayPools.jl.git",
devbranch = "master",
Expand Down
131 changes: 131 additions & 0 deletions docs/src/features/safety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Pool Safety

AdaptiveArrayPools catches pool-escape bugs at **two levels**: compile-time (macro analysis) and runtime (configurable safety levels).

## Compile-Time Detection

The `@with_pool` macro statically analyzes your code and **rejects** any expression that would return a pool-backed array. This catches the most common mistakes at zero runtime cost.

```julia
# Direct array escape — caught at macro expansion time
@with_pool pool begin
v = acquire!(pool, Float64, 100)
v # ← ERROR: v escapes the pool scope
end
```

This would throw an error message as follows:
```
ERROR: LoadError: PoolEscapeError (compile-time)

The following variable escapes the @with_pool scope:

v ← pool-acquired view

Declarations:
[1] v = acquire!(pool, Float64, 100) [myfile.jl:2]

Escaping return:
[1] v [myfile.jl:3]

Fix: Use collect(v) to return owned copies.
Or use a regular Julia array (zeros()/Array{T}()) if it must outlive the pool scope.

in expression starting at myfile.jl:1
```

The analyzer tracks aliases, containers, and convenience wrappers:

```julia
# All of these are caught at compile time:
@with_pool pool begin
v = zeros!(pool, Float64, 10)
w = v # alias of pool variable
t = (1, v) # tuple wrapping pool array
w # ← ERROR
end

@with_pool pool function bad()
A = acquire!(pool, Float64, 3, 3)
return A # ← ERROR (explicit return)
end
```

Safe patterns pass without error:

```julia
@with_pool pool begin
v = acquire!(pool, Float64, 100)
sum(v) # ✅ scalar result
end

@with_pool pool begin
v = acquire!(pool, Float64, 100)
collect(v) # ✅ owned copy
end
```

## Runtime Safety Levels

For bugs the compiler can't catch (e.g., values hidden behind opaque function calls), runtime safety provides configurable protection via the type parameter `S` in `AdaptiveArrayPool{S}`.

### Level Overview

| Level | Name | CPU | CUDA | Overhead |
|-------|------|-----|------|----------|
| **0** | off | No-op (all branches dead-code-eliminated) | Same | Zero |
| **1** | guard | `resize!(v,0)` + `setfield!` invalidation | NaN/sentinel poisoning + cache clear | ~5ns/slot |
| **2** | full | Level 1 + data poisoning + escape detection at scope exit | Level 1 + device-pointer overlap check | Moderate |
| **3** | debug | Level 2 + acquire call-site tracking | Same | Moderate+ |

### Why CPU and CUDA Differ at Level 1

Both achieve the same goal — **make stale references fail loudly** — but use different mechanisms:

| | CPU | CUDA |
|---|-----|------|
| **Strategy** | Structural invalidation | Data poisoning |
| **Mechanism** | `resize!(v, 0)` shrinks backing vector to length 0; `setfield!(:size, (0,))` zeroes the array dimensions | `CUDA.fill!(v, NaN)` / `typemax` / `true` fills backing CuVector with sentinel values |
| **Stale access result** | `BoundsError` (array has length 0) | Reads `NaN` or `typemax` (obviously wrong data) |
| **Why not the other way?** | CPU `resize!` is cheap (~0 cost) | CUDA `resize!` calls `CUDA.Mem.free()` — destroys the pooled VRAM allocation |
| **Cache invalidation** | View length/dims zeroed | N-way view cache entries cleared to `nothing` |

### Setting the Level

```julia
using AdaptiveArrayPools

# Enable full safety on CPU + all GPU devices (preserves cached arrays, zero-copy)
set_safety_level!(2)

# Back to zero overhead everywhere
set_safety_level!(0)
```

The pool type parameter `S` is a compile-time constant. At `S=0`, the JIT eliminates all safety branches via dead-code elimination — true zero overhead with no `Ref` reads or conditional branches.

### Data Poisoning (Level 2+, CPU)

At Level 1, CPU relies on **structural invalidation** (`resize!` + `setfield!`) which makes stale views throw `BoundsError`. At Level 2+, CPU additionally **poisons** the backing vector data with sentinel values (`NaN`, `typemax`, all-`true` for `BitVector`) *before* structural invalidation. This catches stale access through `unsafe_acquire!` wrappers on Julia 1.10 where `setfield!` on Array is unavailable.

CUDA already poisons at Level 1 (its primary invalidation strategy), so no additional poisoning step is needed at Level 2.

### Escape Detection (Level 2+)

At every `@with_pool` scope exit, the return value is inspected for overlap with pool-backed memory. Recursively checks `Tuple`, `NamedTuple`, `Dict`, `Pair`, `Set`, and `AbstractArray` elements.

Level 3 additionally records each `acquire!` call-site, so the error message pinpoints the exact source line and expression that allocated the escaping array.

### Legacy: `POOL_DEBUG`

`POOL_DEBUG[] = true` triggers Level 2 escape detection regardless of `S`. For new code, prefer `set_safety_level!(2)`.

## Recommended Workflow

```julia
# Development / Testing: catch bugs early
set_safety_level!(2) # or 3 for call-site info in error messages

# Production: zero overhead
set_safety_level!(0) # all safety branches eliminated by the compiler
```
3 changes: 3 additions & 0 deletions ext/AdaptiveArrayPoolsCUDAExt/AdaptiveArrayPoolsCUDAExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ include("task_local_pool.jl")
# State management (checkpoint!, rewind!, reset!, empty!)
include("state.jl")

# Safety: poisoning, escape detection, borrow tracking
include("debug.jl")

# Display & statistics (pool_stats, show)
include("utils.jl")

Expand Down
Loading