#Understanding when @function calls are added to the execution graph

1 messages · Page 1 of 1 (latest)

wispy schooner
#

I have the following basic example

@object_type
class BasicMath:

    @function
    async def add( self, a: float, b: float   ) -> float:
        return a+b

    @function
    async def mul( self, a: float, b: float   ) -> float:
        return a*b

    @function
    async def maths( self, val: float ) -> float :
        x = await self.add( val , 5 )
        y = await self.mul( 2 , x )
        return y 

I can execute dagger call maths 2 and I get the expected correct answer

dagger call maths --val 2
▶ connect 0.4s
▶ load module: . 4.9s
✔ parsing command line arguments 0.0s

✔ batches: Batches! 2.1s
▶ .maths(val: 2.000000): Float! 1.8s

14

But I'm expecting to see both add and mul in my trace output from dagger. I've also confimed in dagger cloud that only maths shows up in the trace.

I think I'm just using the sdk incorrectly - whats the right way to do a nested call and have all the @function tagged calls show up in my trace?

cursive linden
#

@wispy schooner something like this?

Full trace at https://dagger.cloud/mageep/traces/5e09d86642d372bc75016e672fa33e35
D:\Projects\pjmagee\daggerverse\python-say-msg [main ≡ +3 ~0 -1 !]> dagger call --val=2 add --value=5 mul --value=2 val
✔ connect 0.6s
✔ load module: . 0.6s
✔ parsing command line arguments 0.0s

✔ pythonTests(val: 2.000000): PythonTests! 0.0s
✔ .add(value: 5.000000): PythonTests! 0.0s
✔ .mul(value: 2.000000): PythonTests! 0.0s
✔ .val: Float! 0.0s

14
wispy schooner
#

yes thats exactly what im looking for !

cursive linden
#

let me give you a snippet

#

but i did change how you are 'implementing' but i think its more how you might want it

@object_type
class PythonTests:

    val: float = field()

    @function
    async def add( self, value: float) -> "PythonTests":
        self.val = self.val + value
        return self

    @function
    async def mul( self, value: float) -> "PythonTests":
        self.val = self.val * value
        return self
#

and this wil make sense with the command

#

dagger call --val=2 add --value=5 mul --value=2 val

#

so, since val can be set via default ctor, we can do --val=

#

its like a builder pattern

wispy schooner
#

so does maths look like

def maths( self , value: float ) -> float: 
    self.value = value 
    return self.add( 5 ).mul( 2).value 
cursive linden
#

yeah kind of, but that wouldnt show up in the logs

wispy schooner
#

would the individual add / mul cache though?

cursive linden
#

ahh

#

but if the value changes

#

hmm, theres also the new function cache disable feature, i havent used it yet though

wispy schooner
#

let me show you a snippet that is a better example of why i want the top level maths function

#
    @function
    async def risky_add( self, a: float, b: float   ) -> float:
        asyncio.sleep( 100 )
        if random.random() < 0.5:
            # Fails 50% of time
            raise ValueError("Failure")
        return a+b

    @function
    async def lots_of_math ( self, n: list[float] ) -> list[float]:
        x=[]
        for val in n: 
            v = await self.risky_add(2,val )
            x.append( v )
        return x 
#

risky_add takes a lot of time and may/may not fail ..... and i need to aggregate a bunch of them .... so what im trying to achieve is caching of the successes so that if i "re-run" lots_of_math i keep my successful trials

#

in real life risky_add is reaching out to some web service that may be busy or timeout or give bogus data (like a llm 🙂 )

#

but this is my dumb example to make sure i am getting the mechanics right

cursive linden
#

i think i understand

#

so, the @function would be public exposed api functions

#

and youre kind of treating risky_add really as a kind of private method?

wispy schooner
#

i can call risky_add from the cli as well if i just want to do it 1x

cursive linden
#

but your thinking is, for it to show in the logs, you would define it as a dagger function

#

ah ok

wispy schooner
#

the lots_of_math is more about doing a big "batch" of similar work

#

and then i want the benefit of dagger caching for each item in the for loop

#

since they "take a lot of time"

#

similarly if i do
dagger call lots-of-math 10

followed by
dagger call lots-of-math 11

I want to leverage the cache values for 0->10 rather than just repeating them

cursive linden
#

because from how i understand it, its a cache based on the called function inputs, not any function

#

hmm

#

Session caching is useful in situations where one function call may be repeated throughout the session (e.g. one function call repeatedly made by other functions)

#

function inputs as cache keys

wispy schooner
#

yes i use cache=never and ttl caching successfully for single function calls

#

which i learned from this page, but im still learning dagger so i wont pretend i didnt miss something important as ive read the docs

cursive linden
#

All good, theres a lot of docs and can be eays to miss or not see something, ive done it pleanty of times

#

ive got a suspicion what you want might not work

#

based on my understanding and the chaining of functions, thats how you get the nice output in the TUI

#

but your implementation is like a private function call, and i dont think is somehow executing in the same manner and cant benefit from the function cache, however... i dont have a working or non working example so im not 100% confident in my response

#

im trying to find other cache examples

wispy schooner
#

is there some way to do like

dag.my_module().risky_add() 

I can do something like this to use a container in python
dag.container() and it seems like this behavior is cached

cursive linden
wispy schooner
cursive linden
#

that seems like an interesting cookbook example 😄

cursive linden
#

and if you've declared both with @function theyre both going to be exposed as public functions to call when someone 'installs' your module as a dependency

wispy schooner
#

the difference is it farms everything out to a container where i want to farm it out to a function in my module

cursive linden
#

yeah, but also that matrix build doesnt really have the try/catch and only rebuild failed ones

wispy schooner
#

true

cursive linden
#

run_all_tests calls the other functions, but i wonder what that output looks like in the TUI and i assume it doesnt cache, but these also dont really demonstrate an unstable 50% fail rate

wispy schooner
#

let me try it and see .....ive tried using asyncio.gather to do something like this

#

but not anyio.create_task_group()

wispy schooner
#

ok @cursive linden

Isolating the risky call to another module and using dag to call it 100% works


@object_type
class Batches:



    @function
    async def doit( self, a: float, b: float   ) -> float:
        o = await dag.mymath().risky_add( float(a), float(b))
        return o

    @function
    async def lots_of_math ( self, n: int ) -> list[float]:
        x=[]
        for k in range(n ): 
            o = await self.doit( 2+.2, k+.1)
            x.append( o )
        return x 
#

heres the trace in dagger cloud

cursive linden
#

so, with one way, you are making a remote call to another module

#

but with the first way, youre in python, with python executing the function, in-memory

#

its graphql calls under the hood, but when you execute and call functions, that call other private functions - i don't know how you can get the cache behaviour you expect i think?

#

hmm

#

the self.doit is the same if you remove it and just call dag.mymath() from lots_of_math?

wispy schooner
#

yes

#

it even uses the previous caches for when it was nested it doit

#

so while id like to be able to do my original example, architecting my modules so that 'batching' is in its own module and the things that are batched are in a different module is totally acceptable to me.

#

but i am curious if there is a way do it in a single module

cursive linden
#

There could be a better way, im not sure if this way is considered a good practice or not

#

but the reason why it works this way, makes sense because installed dagger modules are making a call to the engine

#

if no graphQL query is called, i dont understand how the caching could come into play

wispy schooner
#

i (wrongly) assumed that calling something decorated with @function would also result in an engine call but thats clearly not the case

#

but I get whats going on now so thats good 🙂

cursive linden
#

in my original snippet to you to show you the output you sort of wanted, i achieved that because on the cli level, its chaining the calls

wispy schooner
#

i will probably clean my example up and check it into a github project

#

will share a link when i do

wispy schooner
#

thanks for the help understanding this @cursive linden

cursive linden
#

nice

#

im curious to know of an alternative like you had mentioned

wispy schooner
celest rivet