Skip to content

Commit f85891a

Browse files
Fixup; get errors working (#61)
* tell llvm the correct types of objects; verify the IR after after modification * Update src/code_loading.jl Co-authored-by: Valentin Churavy <vchuravy@users.noreply.github.com> * use julia's optimizer, enable tests for error handling * make ManualMemory and StrideArraysCore available on other process * run optimization twice so that reloc can happen on optimized code * grab `jl_add_optimization_passes` from `libjulia-codegen` on newer versions * don't test on windows for now, it's pointless * remove unnecessary func * add some comments * reorganize * catch an error pathway Co-authored-by: Valentin Churavy <vchuravy@users.noreply.github.com>
1 parent 536ea44 commit f85891a

8 files changed

Lines changed: 158 additions & 128 deletions

File tree

.github/workflows/ci.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ jobs:
66
test:
77
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
88
runs-on: ${{ matrix.os }}
9-
continue-on-error: ${{ matrix.os == 'windows-latest' }}
109
strategy:
1110
fail-fast: false
1211
matrix:
@@ -16,7 +15,6 @@ jobs:
1615
os:
1716
- ubuntu-latest
1817
- macOS-latest
19-
- windows-latest
2018
arch:
2119
- x64
2220
steps:

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,5 @@ This package uses the [GPUCompiler package](https://github.com/JuliaGPU/GPUCompi
5050
* GC-tracked allocations and global variables do work with `compile`, but the way they are implemented is brittle and can be dangerous. Allocate with care.
5151
* GC-tracked allocations and global varaibles do *not* work with `compile_executable` (yet).
5252
* Type unstable code is not yet supported.
53-
* Throwing errors is not currently supported. In the meantime, consider wrapping possible errors with [ErrorTypes.jl](https://github.com/jakobnissen/ErrorTypes.jl)
5453
* Doesn't currently work on Windows.
5554
* If you find any other limitations, let us know. There's probably lots.

src/StaticCompiler.jl

Lines changed: 92 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
module StaticCompiler
22

33
using GPUCompiler: GPUCompiler
4-
using LLVM: LLVM
5-
using Libdl: Libdl
4+
using LLVM
5+
using LLVM.Interop
6+
using LLVM: API
7+
using Libdl: Libdl, dlsym, dlopen
68
using Base: RefValue
79
using Serialization: serialize, deserialize
810
using Clang_jll: clang
@@ -82,6 +84,7 @@ single method (the method determined by `types`).
8284
function compile(f, _tt, path::String = tempname(); name = GPUCompiler.safe_name(repr(f)), filename="obj",
8385
strip_llvm = false,
8486
strip_asm = true,
87+
opt_level=3,
8588
kwargs...)
8689
tt = Base.to_tuple_type(_tt)
8790
isconcretetype(tt) || error("input type signature $_tt is not concrete")
@@ -90,7 +93,7 @@ function compile(f, _tt, path::String = tempname(); name = GPUCompiler.safe_nam
9093
isconcretetype(rt) || error("$f on $_tt did not infer to a concrete type. Got $rt")
9194

9295
f_wrap!(out::Ref, args::Ref{<:Tuple}) = (out[] = f(args[]...); nothing)
93-
_, _, table = generate_shlib(f_wrap!, Tuple{RefValue{rt}, RefValue{tt}}, path, name; strip_llvm, strip_asm, filename, kwargs...)
96+
_, _, table = generate_shlib(f_wrap!, Tuple{RefValue{rt}, RefValue{tt}}, path, name; opt_level, strip_llvm, strip_asm, filename, kwargs...)
9497

9598
lf = LazyStaticCompiledFunction{rt, tt}(Symbol(f), path, name, filename, table)
9699
cjl_path = joinpath(path, "$filename.cjl")
@@ -99,6 +102,92 @@ function compile(f, _tt, path::String = tempname(); name = GPUCompiler.safe_nam
99102
(; f = instantiate(lf), path=abspath(path))
100103
end
101104

105+
"""
106+
```julia
107+
generate_shlib(f, tt, path::String, name::String, filenamebase::String="obj"; kwargs...)
108+
```
109+
Low level interface for compiling a shared object / dynamically loaded library
110+
(`.so` / `.dylib`) for function `f` given a tuple type `tt` characterizing
111+
the types of the arguments for which the function will be compiled.
112+
113+
See also `StaticCompiler.generate_shlib_fptr`.
114+
115+
### Examples
116+
```julia
117+
julia> function test(n)
118+
r = 0.0
119+
for i=1:n
120+
r += log(sqrt(i))
121+
end
122+
return r/n
123+
end
124+
test (generic function with 1 method)
125+
126+
julia> path, name = StaticCompiler.generate_shlib(test, Tuple{Int64}, "./test")
127+
("./test", "test")
128+
129+
shell> tree \$path
130+
./test
131+
|-- obj.o
132+
`-- obj.so
133+
134+
0 directories, 2 files
135+
136+
julia> test(100_000)
137+
5.256496109495593
138+
139+
julia> ccall(StaticCompiler.generate_shlib_fptr(path, name), Float64, (Int64,), 100_000)
140+
5.256496109495593
141+
```
142+
"""
143+
function generate_shlib(f, tt, path::String = tempname(), name = GPUCompiler.safe_name(repr(f)), filenamebase::String="obj";
144+
strip_llvm = false,
145+
strip_asm = true,
146+
opt_level=3,
147+
kwargs...)
148+
mkpath(path)
149+
obj_path = joinpath(path, "$filenamebase.o")
150+
lib_path = joinpath(path, "$filenamebase.$(Libdl.dlext)")
151+
152+
job, kwargs = native_job(f, tt; name, kwargs...)
153+
#Get LLVM to generated a module of code for us. We don't want GPUCompiler's optimization passes.
154+
mod, meta = GPUCompiler.codegen(:llvm, job; strip=strip_llvm, only_entry=false, validate=false, optimize=false)
155+
#use Julia's optimization pass on the LLVM code, but leave intrinsics alone
156+
julia_opt_passes(mod, job; opt_level, lower_intrinsics=0)
157+
# Scoop up all the pointers in the optimized module, and replace them with unitialized global variables.
158+
# table is a dictionary where the keys are julia objects that are needed by the function, and the values
159+
# of the dictionary are the names of their associated LLVM GlobalVariable names.
160+
table = relocation_table!(mod)
161+
# Now that we've removed all the pointers from the code, we can (hopefully) safely lower all the instrinsics
162+
julia_opt_passes(mod, job; opt_level, lower_intrinsics=1)
163+
# Make sure we didn't make any glaring errors
164+
LLVM.verify(mod)
165+
# Compile the LLVM module to native code and save it to disk
166+
obj, _ = GPUCompiler.emit_asm(job, mod; strip=strip_asm, validate=false, format=LLVM.API.LLVMObjectFile)
167+
open(obj_path, "w") do io
168+
write(io, obj)
169+
end
170+
path, name, table
171+
end
172+
173+
function julia_opt_passes(mod, job; opt_level, lower_intrinsics)
174+
triple = GPUCompiler.llvm_triple(job.target)
175+
tm = GPUCompiler.llvm_machine(job.target)
176+
177+
lib_path = VERSION > v"1.8.0-DEV" ? "libjulia-codegen" : "libjulia"
178+
179+
dlopen(lib_path) do lib
180+
opt_func = dlsym(lib, "jl_add_optimization_passes")
181+
ModulePassManager() do pm
182+
add_library_info!(pm, triple)
183+
add_transform_info!(pm, tm)
184+
ccall(opt_func, Cvoid,
185+
(LLVM.API.LLVMPassManagerRef, Cint, Cint),
186+
pm, opt_level, lower_intrinsics)
187+
run!(pm, mod)
188+
end
189+
end
190+
end
102191

103192
"""
104193
```julia
@@ -173,64 +262,6 @@ function compile_executable(f, _tt=(), path::String="./", name=GPUCompiler.safe_
173262
joinpath(abspath(path), filename)
174263
end
175264

176-
"""
177-
```julia
178-
generate_shlib(f, tt, path::String, name::String, filenamebase::String="obj"; kwargs...)
179-
```
180-
Low level interface for compiling a shared object / dynamically loaded library
181-
(`.so` / `.dylib`) for function `f` given a tuple type `tt` characterizing
182-
the types of the arguments for which the function will be compiled.
183-
184-
See also `StaticCompiler.generate_shlib_fptr`.
185-
186-
### Examples
187-
```julia
188-
julia> function test(n)
189-
r = 0.0
190-
for i=1:n
191-
r += log(sqrt(i))
192-
end
193-
return r/n
194-
end
195-
test (generic function with 1 method)
196-
197-
julia> path, name = StaticCompiler.generate_shlib(test, Tuple{Int64}, "./test")
198-
("./test", "test")
199-
200-
shell> tree \$path
201-
./test
202-
|-- obj.o
203-
`-- obj.so
204-
205-
0 directories, 2 files
206-
207-
julia> test(100_000)
208-
5.256496109495593
209-
210-
julia> ccall(StaticCompiler.generate_shlib_fptr(path, name), Float64, (Int64,), 100_000)
211-
5.256496109495593
212-
```
213-
"""
214-
function generate_shlib(f, tt, path::String = tempname(), name = GPUCompiler.safe_name(repr(f)), filenamebase::String="obj";
215-
strip_llvm = false,
216-
strip_asm = true,
217-
kwargs...)
218-
mkpath(path)
219-
obj_path = joinpath(path, "$filenamebase.o")
220-
lib_path = joinpath(path, "$filenamebase.$(Libdl.dlext)")
221-
222-
job, kwargs = native_job(f, tt; name, kwargs...)
223-
mod, meta = GPUCompiler.codegen(:llvm, job; strip=strip_llvm, only_entry=false, validate=false)
224-
225-
table = relocation_table!(mod)
226-
227-
obj, _ = GPUCompiler.emit_asm(job, mod; strip=strip_asm, validate=false, format=LLVM.API.LLVMObjectFile)
228-
229-
open(obj_path, "w") do io
230-
write(io, obj)
231-
end
232-
path, name, table
233-
end
234265

235266

236267
function generate_shlib_fptr(f, tt, path::String=tempname(), name = GPUCompiler.safe_name(repr(f)), filenamebase::String="obj"; temp::Bool=true, kwargs...)

src/code_loading.jl

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ struct LazyStaticCompiledFunction{rt, tt}
1515
reloc::IdDict{Any,String}
1616
end
1717

18+
"""
19+
unsafe_pointer_from_objref(x)
20+
21+
Sometimes Julia embeds immutables like `Base.string` into code, and julia
22+
will error if you call `pointer_from_objref(string)`, claiming that it
23+
doesn't have a pointer even though that's a lie.
24+
"""
25+
unsafe_pointer_from_objref(x) = ccall(:jl_value_ptr, Ptr{Cvoid}, (Any,), x)
26+
1827
function instantiate(p::LazyStaticCompiledFunction{rt, tt}) where {rt, tt}
1928
# LLVM.load_library_permantly(dirname(Libdl.dlpath(Libdl.dlopen("libjulia"))))
2029
lljit = LLVM.LLJIT(;tm=LLVM.JITTargetMachine())
@@ -25,14 +34,7 @@ function instantiate(p::LazyStaticCompiledFunction{rt, tt}) where {rt, tt}
2534

2635
# Set all the uninitialized global variables to point to julia values from the relocation table
2736
for (val, name) p.reloc
28-
if !ismutable(val)
29-
# Sometimes Julia embeds functions like `Base.string` into code, and this doesn't have a pointer
30-
# so we need to give it one manually, and put the ref in the dict to make sure it doesn't expire.
31-
delete!(p.reloc, val)
32-
val = Ref(val)
33-
p.reloc[val] = name
34-
end
35-
address = LLVM.API.LLVMOrcJITTargetAddress(reinterpret(UInt, pointer_from_objref(val)))
37+
address = LLVM.API.LLVMOrcJITTargetAddress(reinterpret(UInt, unsafe_pointer_from_objref(val)))
3638

3739
symbol = LLVM.API.LLVMJITEvaluatedSymbol(address, flags)
3840
gv = LLVM.API.LLVMJITCSymbolMapPair(LLVM.mangle(lljit, name), symbol)
@@ -54,6 +56,12 @@ function instantiate(p::LazyStaticCompiledFunction{rt, tt}) where {rt, tt}
5456
StaticCompiledFunction{rt, tt}(p.f, fptr, lljit, p.reloc)
5557
end
5658

59+
function absolute_symbols(symbols)
60+
ref = LLVM.API.LLVMOrcAbsoluteSymbols(symbols, length(symbols))
61+
LLVM.MaterializationUnit(ref)
62+
end
63+
64+
5765
struct StaticCompiledFunction{rt, tt}
5866
f::Symbol
5967
ptr::Ptr{Nothing}

src/pointer_patching.jl

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
function relocation_table!(mod)
22
i64 = LLVM.IntType(64; ctx=LLVM.context(mod))
3-
jl_t = LLVM.PointerType(LLVM.StructType(LLVM.LLVMType[]; ctx=LLVM.context(mod)))
43
d = IdDict{Any, Tuple{String, LLVM.GlobalVariable}}()
54

65
for func LLVM.functions(mod), bb LLVM.blocks(func), inst LLVM.instructions(bb)
@@ -121,23 +120,26 @@ function relocation_table!(mod)
121120
end
122121

123122
function get_pointers!(d, mod, inst)
124-
jl_t = LLVM.PointerType(LLVM.StructType(LLVM.LLVMType[]; ctx=LLVM.context(mod)))
123+
jl_t = (LLVM.StructType(LLVM.LLVMType[]; ctx=LLVM.context(mod)))
125124
for (i, arg) enumerate(LLVM.operands(inst))
126125
if occursin("inttoptr", string(arg)) && arg isa LLVM.ConstantExpr
127126
op1 = LLVM.Value(LLVM.API.LLVMGetOperand(arg, 0))
127+
if op1 isa LLVM.ConstantExpr
128+
op1 = LLVM.Value(LLVM.API.LLVMGetOperand(op1, 0))
129+
end
128130
ptr = Ptr{Cvoid}(convert(Int, op1))
129-
130131
frames = ccall(:jl_lookup_code_address, Any, (Ptr{Cvoid}, Cint,), ptr, 0)
131132
if length(frames) >= 1
132133
fn, file, line, linfo, fromC, inlined = last(frames)
133-
if isempty(String(fn)) || fn == :jl_system_image_data
134-
val = unsafe_pointer_to_objref(ptr)
134+
if (isempty(String(fn)) && isempty(String(file))) || fn == :jl_system_image_data
135+
val = unsafe_pointer_to_objref(ptr)
135136
if val keys(d)
136137
_, gv = d[val]
137138
LLVM.API.LLVMSetOperand(inst, i-1, gv)
138139
else
139140
gv_name = GPUCompiler.safe_name(String(gensym(repr(Core.Typeof(val)))))
140-
gv = LLVM.GlobalVariable(mod, jl_t, gv_name)
141+
gv = LLVM.GlobalVariable(mod, llvmeltype(arg), gv_name, LLVM.addrspace(llvmtype(arg)))
142+
141143
LLVM.extinit!(gv, true)
142144
LLVM.API.LLVMSetOperand(inst, i-1, gv)
143145

@@ -151,10 +153,7 @@ function get_pointers!(d, mod, inst)
151153
end
152154
end
153155

154-
function absolute_symbols(symbols)
155-
ref = LLVM.API.LLVMOrcAbsoluteSymbols(symbols, length(symbols))
156-
LLVM.MaterializationUnit(ref)
157-
end
156+
llvmeltype(x::LLVM.Value) = eltype(LLVM.llvmtype(x))
158157

159158
function pointer_patching_diff(mod::LLVM.Module, path1=tempname(), path2=tempname(); show_reloc_table=false)
160159
s1 = string(mod)
@@ -168,7 +167,12 @@ function pointer_patching_diff(mod::LLVM.Module, path1=tempname(), path2=tempnam
168167
s2 = string(mod)
169168
write(path2, s2)
170169

171-
run(`diff $p1 $p2`)
170+
try
171+
# this always ends in an error for me for some reason
172+
run(`diff $path1 $path2`)
173+
catch e;
174+
nothing
175+
end
172176
end
173177

174178

src/target.jl

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,9 @@ end
1919
GPUCompiler.runtime_slug(job::GPUCompiler.CompilerJob{NativeCompilerTarget}) = "native_$(job.target.cpu)-$(hash(job.target.features))"
2020

2121
module StaticRuntime
22-
# dummy methods
22+
# the runtime library
2323
signal_exception() = return
24-
# HACK: if malloc returns 0 or traps, all calling functions (like jl_box_*)
25-
# get reduced to a trap, which really messes with our test suite.
26-
malloc(sz) = Ptr{Cvoid}(Int(0xDEADBEEF))
24+
malloc(sz) = ccall("extern malloc", llvmcall, Csize_t, (Csize_t,), sz)
2725
report_oom(sz) = return
2826
report_exception(ex) = return
2927
report_exception_name(ex) = return
@@ -33,6 +31,11 @@ end
3331
struct StaticCompilerParams <: GPUCompiler.AbstractCompilerParams end
3432

3533
GPUCompiler.runtime_module(::GPUCompiler.CompilerJob{<:Any,StaticCompilerParams}) = StaticRuntime
34+
GPUCompiler.runtime_module(::GPUCompiler.CompilerJob{NativeCompilerTarget}) = StaticRuntime
35+
GPUCompiler.runtime_module(::GPUCompiler.CompilerJob{NativeCompilerTarget, StaticCompilerParams}) = StaticRuntime
36+
37+
GPUCompiler.can_throw(job::GPUCompiler.CompilerJob{<:Any,StaticCompilerParams}) = true
38+
GPUCompiler.can_throw(job::GPUCompiler.CompilerJob{NativeCompilerTarget, StaticCompilerParams}) = true
3639
GPUCompiler.can_throw(job::GPUCompiler.CompilerJob{NativeCompilerTarget}) = true
3740

3841
function native_job(@nospecialize(func), @nospecialize(types); kernel::Bool=false, name=GPUCompiler.safe_name(repr(func)), kwargs...)

test/Project.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
77
LoopVectorization = "bdcacae8-1622-11e9-2a5c-532679323890"
88
ManualMemory = "d125e4d3-2237-4719-b19c-fa641b8a4667"
99
StrideArraysCore = "7792a7ef-975c-4747-a70f-980b88e8d1da"
10-
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
11-
ErrorTypes = "7f495686-c6d2-4c77-9e8e-e4c865675f9d"
10+
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"

0 commit comments

Comments
 (0)