brenhinkeller / StaticTools.jl

Enabling StaticCompiler.jl-based compilation of (some) Julia code to standalone native binaries by avoiding GC allocations and llvmcall-ing all the things!
MIT License
167 stars 11 forks source link

Passing primitives? #43

Closed Quafadas closed 1 year ago

Quafadas commented 1 year ago

This is a "discussion" or request for help rather than an issue. Apologies if it's the wrong place, I couldn't see "discussions".

I've successfully run the matrix multiplication example in the readme.

I was trying to simplify it even further, to understand more closely the nature of the constraints in place with "static compiler".


ttAdd = (RefValue{Ptr{Float64}}, RefValue{Ptr{Float64}}, RefValue{Ptr{Float64}})
@inline function add!(c :: Ptr{Float64},  a:: Ptr{Float64} , b :: Ptr{Float64} )
    c = zero(eltype(c))
    c = a + b
    return 0
end 

add!(C :: Ref, A:: Ref, B ::Ref) = add!(C, A, B)

Errors out with

compile_shlib(add!, ttAdd, "./", "add", filename="add")
ERROR: `add!(RefValue{Ptr{Float64}}, RefValue{Ptr{Float64}}, RefValue{Ptr{Float64}})` did not infer to a concrete type. Got `Union{}`

I had also believed, that something like this, ought to be possible, as floats are primitives?

ttAdd2 = (Float64, Float64)
@inline function add(a:: Float64 , b :: Float64 )
    return a+b
end 

add(A:: Float64, B ::Float64) = add(a, b)

But I get the same message. I suspect, that my understanding of the role of the type tuple, is incomplete somehow.

Would anyone have a hint?

Full diclosure; my aim is to call a statically compiled Julia lib from scala, and gradually increase the complexity https://quafadas-literate-space-goldfish-g5qw954xj6f9qgp.github.dev/

brenhinkeller commented 1 year ago

Ah so for the latter, yes you should be able to just return a float since it is primitive type and an LLVM type -- try the following:

using StaticCompiler, StaticTools

@inline function add(a::Float64 , b::Float64)
    return a+b
end

compile_shlib(add, (Float64, Float64), filename="libadd")

# Have to add julia_ in front because we didn't demangle and it's added by default
@ccall "libadd".julia_add(1.0::Float64, 2.0::Float64)::Float64

I get

julia> compile_shlib(add, (Float64, Float64), filename="libadd")
"/Users/cbkeller/libadd.dylib"

julia> @ccall "libadd".julia_add(1.0::Float64, 2.0::Float64)::Float64
3.0

(actually, no need for even using StaticTools there unless you want to print or something)

brenhinkeller commented 1 year ago

For the former, it looks like you maybe have some redundancies between pointers and refs.. try instead for example

using StaticCompiler

@inline function add!(c::Ptr{Float64},  a::Ptr{Float64} , b::Ptr{Float64} )
    Base.unsafe_store!(c, Base.unsafe_load(a) + Base.unsafe_load(b))
    return 0
end

compile_shlib(add!, (Ptr{Float64}, Ptr{Float64},Ptr{Float64}), filename="libadd")

a,b,c = Ref.((1., 2., 0.))
pa,pb,pc = Ptr{Float64}.(pointer_from_objref.((a,b,c)))

# Have to add julia_ in front because we didn't demangle
# Note that the ! becomes a _
@ccall "libadd".julia_add_(pc::Ptr{Float64}, pa::Ptr{Float64}, pb::Ptr{Float64})::Int64

I get

julia> @ccall "libadd".julia_add_(pc::Ptr{Float64}, pa::Ptr{Float64}, pb::Ptr{Float64})::Int64
0

julia> c
Base.RefValue{Float64}(3.0)
brenhinkeller commented 1 year ago

In the latter example the Refs are basically just a convenient way to get pointers to somewhere where the Float64s are allocated on the heap -- which in this case isn't especially necessary, but would be if they were something other than a primitive type that directly corresponds to an LLVM type

In all cases, the tuple of types is just to tell StaticCompiler what to compile.

Sometimes if you have overwritten the method you want to compile several times (by re-defining it) it seems like staticcompiler/gpucompiler may get confused, so it may sometimes help to start a new session if it seems like you're trying to compile a method that really should return a concrete type (though 99 times out of 100 it's something more like a typo)

Quafadas commented 1 year ago

Yes! Thankyou so much. I think that first example, is a lot of what I needed to know.

  println("Julia add 1 + 2")
  val summed = addLib.julia_add(1, 2)  
  println(s"sums too =  $summed")

This runs (apparently successfully) ... but produces a puzzling result when called from scala.

julia add 1 + 2
sums too =  Infinity

I would guess, that something about CFloat, is not quite consistent with Julia's Int64. But I think this shows it quite concretely - it may be a tractable problem. Never have I been so excited about bad mathematics.

Thankyou!

markehammons commented 1 year ago

Int64 would be CLongLong, not CFloat. Can you try that?

Edit: the C types are only valid for bindings to C. Julia's In64 is directly equivalent to Long in Scala.

Quafadas commented 1 year ago

So ... trying to use a float for a Julia int - not my smartest moment! Using both Long and CLongLong, I get

julia add 1 + 2
0

Which means I'm infinitely closer :-). I'll look again tomorrow, in case I have done something stupid. It's feels like this really might be tractable though!

brenhinkeller commented 1 year ago

Sounds like progress. You might also make sure that's the version that returns the result c rather than returning 0.

I'll close the issue, but feel free to keep discussing here!

Quafadas commented 1 year ago

Well, I have some progress in that primitives now seem to work. I do not yet however, appear to have reached the stage, where I can innovate on my knowledge :-( ...

using StaticCompiler
using StaticTools

@inline function hi(c::Ptr{MallocString},  a::Ptr{MallocString}) :: Int64
    load_a = Base.unsafe_load(a)    
    StaticTools.printf("entered julia")
    StaticTools.printf(load_a)
    tmp = c"hi right back from julia"    
    Base.unsafe_store!(c, tmp)
    return 0
end

compile_shlib(hi, (Ptr{MallocString}, Ptr{MallocString}), filename="string_pointers")

s1 :: MallocString = c"hi1"
s2 :: MallocString = c"hi2"

s1_p :: Ptr{MallocString}, s2_p :: Ptr{MallocString} = pointer_from_objref(Ref(s1)), pointer_from_objref(Ref(s2))

@ccall "/workspaces/slincTest/myJlib/string_pointers.so".julia_hi(s1_p:: Ptr{MallocString} ,s2_p:: Ptr{MallocString}) :: Int64

s1_p

Gets me a segmenetation fault. Am I doing something obviously wrong here?

brenhinkeller commented 1 year ago

Ah, a few things: 1) Your function is expecting pointers to MallocStrings but you're giving it pointers to StaticStrings 2) the line StaticTools.printf("entered julia") has a regular string, which will cause problems 3) the line s1_p :: Ptr{MallocString}, s2_p :: Ptr{MallocString} = pointer_from_objref(Ref(s1)), pointer_from_objref(Ref(s2)) makes Julia's garbage collector think that you never need the Refs because it looks like you throw them away right away (it doesn't know you're keeping a pointer around), so either GC.@preserve or just put on a separate line like we did before. This will segfault if the Refs are GC'd 4) the line Base.unsafe_store!(c, tmp) is going to try to write a StaticString (tmp) to a pointer that's supposed to hold a MallocString. A MallocString is just a pointer and a length, while a StaticString includes a tuple of data, so that won't work as is. It could work if you made tmp a MallocString instead of a StaticString, or you could just write to the string c rather than replacing it.

Try instead:

using StaticCompiler, StaticTools

@inline function hi(c::Ptr{MallocString},  a::Ptr{MallocString})::Int64
    load_a = Base.unsafe_load(a)
    StaticTools.printf(c"entered julia\n")
    StaticTools.printf(load_a)
    load_c = Base.unsafe_load(c)
    tmp = c"hi right back from julia"
    load_c[:] = tmp # This basically does copyto, could also loop through the individual characters / UInt8s.
    return 0
end

compile_shlib(hi, (Ptr{MallocString}, Ptr{MallocString}), filename="string_pointers")

sa = m"hi1"
sc = m"This string has to be big enough to hold your return message"
ra, rc = Ref.((sa, sc))
pa, pc = Ptr{MallocString}.(pointer_from_objref.((ra, rc))) # The pointer typecast is because pointer_from_objref returns Ptr{nothing}
@ccall "string_pointers".julia_hi(pc::Ptr{MallocString},pa::Ptr{MallocString})::Int64

I get

julia> @ccall "string_pointers".julia_hi(pc::Ptr{MallocString},pa::Ptr{MallocString})::Int64
0

julia> sc
m"hi right back from julia"
Quafadas commented 1 year ago

@brenhinkeller That is a next level answer. Thankyou so much again. I guess you can just about tell this is more or less a first foray into unmanaged memory for me! Thankyou again so much for your patience and answers. I can follow your example which I can successfully call this from Julia itself, which is great.

I haven't managed to make the final call from scala though... I feel like I'm now either very close ... or very far!

warning: Using incubator modules: jdk.incubator.foreignException in thread "main" java.lang.UnsatisfiedLinkError: /workspaces/slincTest/myJlib/string_pointers.so: /workspaces/slincTest/myJlib/string_pointers.so: undefined symbol: 
__stack_chk_guard        at java.base/jdk.internal.loader.NativeLibraries.load(Native Method)        at 
java.base/jdk.internal.loader.NativeLibraries$NativeLibraryImpl.open(NativeLibraries.java:388)        at 
java.base/jdk.internal.loader.NativeLibraries.loadLibrary(NativeLibraries.java:232)        at 
java.base/jdk.internal.loader.NativeLibraries.loadLibrary(NativeLibraries.java:174)        at 
java.base/java.lang.ClassLoader.loadLibrary(ClassLoader.java:2389)        at 
java.base/java.lang.Runtime.load0(Runtime.java:755)        at java.base/java.lang.System.load(System.java:1953)        at 
fr.hammons.slinc.modules.LinkageTools$.load(LinkageTools.scala:65)        at 
fr.hammons.slinc.modules.LinkageTools$.loadDependency(LinkageTools.scala:100)        at 
fr.hammons.slinc.modules.FSetModule17$package$fsetModule17$.getBacking$$anonfun$1(FSetModule17.scala:23)        at scala.runtime.function.JProcedure1.apply(JProcedure1.java:15)        at 
scala.runtime.function.JProcedure1.apply(JProcedure1.java:10)        at scala.collection.immutable.List.foreach(List.scala:333)        at fr.hammons.slinc.modules.FSetModule17$package$fsetModule17$.getBacking(FSetModule17.scala:23)        at fr.hammons.slinc.FSet.get(FSet.scala:26)        at fr.hammons.slinc.FSet.get$(FSet.scala:13)        at slinj.stringTest$$anon$6.get(slinc.scala:58)        at slinj.slinc$package$.<clinit>(slinc.scala:64)        at slinj.calc.main(slinc.scala:84)

The nature of the message and success from Julia makes me think that the Julia library I've just compiled, relies on some utility, that I have not managed to correctly include during compilation, but that happens to be present in the scope of StaticTools / Compiler, when called from Julia itself.

Could that be the case?

@markehammons cc/ in case this could be ( I don't think it is) to do with slinc?

brenhinkeller commented 1 year ago

Ah interesting -- I don't know what this message means because I don't know scala or java, but if someone else does hopefully they can chime in.

Some of this stuff is new to everyone, since Julia is certainly not designed with the intention of people manually managing their memory!

markehammons commented 1 year ago

There's an unsatisfied link error causing a missing symbol. This basically means there's missing definitions in the library you loaded. Specifically __stack_chk_guard.

Quafadas commented 1 year ago

It looks like this might be a rehash of an old problem?

https://github.com/brenhinkeller/StaticTools.jl/issues/28