Skip to content

Commit 5fce479

Browse files
authored
Merge branch 'main' into chore/sign-CLA
2 parents 0e50dc2 + 43b5c47 commit 5fce479

File tree

8 files changed

+385
-15
lines changed

8 files changed

+385
-15
lines changed

LOCAL_DEVELOPMENT.md

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ A minimal local Lilypad network consists of the following pieces of infrastructu
1717
- One bacalhau node
1818
- One postgres database
1919
- One solver service
20-
- One job creator service
2120
- One resource provider service
2221

23-
Order matters because the `solver`, `job creator` and `resource provider` will right away try to connect to the `blockchain node`. First, the `solver` will update the state of a known smart contract to publish a URL where other services can connect to it. Then, the `job creator`, and `resource provider` will fetch from the `blockchain` the URL for the `solver` and try to connect to it.
22+
Order matters because the `solver` and `resource provider` will right away try to connect to the `blockchain node`. First, the `solver` will update the state of a known smart contract to publish a URL where other services can connect to it. Then, the `resource provider` will fetch from the `blockchain` the URL for the `solver` and try to connect to it.
2423

2524
## Minimal local setup
2625

@@ -39,11 +38,7 @@ A helper script is in place to verify balances on the accounts: `cd hardhat && n
3938

4039
This process can be executed directly if Golang has been installed or in a docker container. The commands are `./stack solver`,`./stack solver-docker-build` and `./stack solver-docker-run` respectively. The `solver` service will output a log line that reads that "the solver has been registered successfully" or "the solver already exists". It is best to wait for this output before starting the services that will try to connect to the `solver`.
4140

42-
### 3. Job creator
43-
44-
This process can be executed directly if Golang has been installed or in a docker container. The commands are `./stack job-creator`,`./stack job-creator-docker-build` and `./stack job-creator-docker-run` respectively. The `job-creator` service's main function is to listen to events from the blockchain to execute jobs and when it receives such an event it will relay the payload to the `solver`. So think about the `job-creator` as the "on-chain solver".
45-
46-
### 4. Resource provider
41+
### 3. Resource provider
4742

4843
For the time being this process has to be executed directly and needs Golang to be installed. This is the command to execute the service: `./stack resource-provider`. If you have a GPU you can use the following flag to use it: `./stack resource-provider --offer-gpu 1`
4944

@@ -76,7 +71,25 @@ Run `./stack compose-down`.
7671

7772
## Running a job
7873

79-
Once all the services are up and running this command can be used to trigger an on-chain job: `./stack run-cowsay-onchain`
74+
Once all the services are up, run a cowsay job with:
75+
76+
```sh
77+
./stack run cowsay:v0.0.4 -i Message="Hello!"
78+
```
79+
80+
The `cowsay:v0.0.4` specifies the module to run with a short code and tag, but a module URL and tag can also be used:
81+
82+
```sh
83+
./stack run github.com/Lilypad-Tech/lilypad-module-cowsay:v0.0.4 -i Message="Hello!"
84+
```
85+
86+
Lastly, a module URL and git hash can be used:
87+
88+
```sh
89+
./stack run github.com/Lilypad-Tech/lilypad-module-cowsay:cb8b670805b06206bd63603a8ba582638a619fe5 -i Message="Hello!"
90+
```
91+
92+
The `-i Message="Hello!"` states the input to the module. `Message="Hello!"` is an input expected by the cowsay module. Other modules may expect a different set of input key-value pairs.
8093

8194
### Tests
8295

@@ -111,6 +124,12 @@ This can be addressed by doing the following:
111124

112125
- Open your Docker Desktop app, go to `Volumes` and delete `lilypad_chain-data` as there might be stale data in the volume not allowing you to properly execute all the transactions `chain-boot` executes
113126

127+
## Onchain job creator
128+
129+
This onchain job creator can be run directly if Golang has been installed. Run the onchain job creator with `./stack job-creator`. The onchain job creator service's main function is to listen to events from the blockchain to execute jobs and when it receives such an event it will relay the payload to the solver.
130+
131+
We have an example script that submits a job the blockchain for the onchain job creator to pick up and run. Use `./stack run-cowsay-onchain` to run an onchain job. Note that all the services listed above must also be running.
132+
114133
### Issues running onchain cowsay
115134

116135
If you find that you have issues with the Job Creator not picking up your `run-cowsay-onchain` command while running the Lilypad stack through Docker, do the following:

cmd/lilypad/run.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,11 @@ func runJob(cmd *cobra.Command, options jobcreator.JobCreatorOptions, network st
142142
return err
143143
}
144144
spinner.Stop()
145-
fmt.Printf("\n🍂 Lilypad job completed, try 👇\n open %s\n cat %s/stdout\n cat %s/stderr\n https://ipfs.io/ipfs/%s\n",
145+
fmt.Printf("🆔 Data ID: %s\n", result.Result.DataID)
146+
fmt.Printf("\n🍂 Lilypad job completed, try 👇\n open %s\n cat %s/stdout\n cat %s/stderr\n",
146147
solver.GetDownloadsFilePath(result.JobOffer.DealID),
147148
solver.GetDownloadsFilePath(result.JobOffer.DealID),
148149
solver.GetDownloadsFilePath(result.JobOffer.DealID),
149-
result.Result.DataID,
150150
)
151151
return err
152152
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ require (
211211
k8s.io/klog/v2 v2.110.1 // indirect
212212
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
213213
lukechampine.com/blake3 v1.3.0 // indirect
214+
pgregory.net/rapid v1.1.0 // indirect
214215
rsc.io/tmplfunc v0.0.3 // indirect
215216
sigs.k8s.io/yaml v1.4.0 // indirect
216217
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,6 +1355,8 @@ modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6
13551355
modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
13561356
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
13571357
modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
1358+
pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw=
1359+
pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
13581360
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
13591361
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
13601362
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=

pkg/data/types.go renamed to pkg/data/data.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package data
22

33
import (
4+
"encoding/json"
5+
46
"github.com/lilypad-tech/lilypad/pkg/data/bacalhau"
57
)
68

@@ -76,11 +78,62 @@ type Result struct {
7678
ID string `json:"id"`
7779
DealID string `json:"deal_id"`
7880
// the CID of the actual results
79-
DataID string `json:"results_id"`
81+
DataID string `json:"data_id"`
8082
Error string `json:"error"`
8183
InstructionCount uint64 `json:"instruction_count"`
8284
}
8385

86+
// Provides compatibility for older clients that expect the results_id field
87+
func (r Result) MarshalJSON() ([]byte, error) {
88+
// TODO(bgins) Remove when older clients have been deprecated
89+
90+
// Create an auxiliary type to avoid recursively calling json.Marshal
91+
// https://stackoverflow.com/a/23046869
92+
type ResultAux Result
93+
94+
// Add results_id field to the existing Result fields and marshal
95+
return json.Marshal(struct {
96+
ResultAux
97+
ResultsID string `json:"results_id"`
98+
}{
99+
ResultAux: ResultAux(r),
100+
ResultsID: r.DataID,
101+
})
102+
}
103+
104+
// Provides compatibility for newer clients that expect the data_id field
105+
func (r *Result) UnmarshalJSON(data []byte) error {
106+
// TODO(bgins) Remove when older clients have been deprecated
107+
108+
// Create an auxiliary type to avoid recursively calling json.Unmarshal
109+
// https://stackoverflow.com/a/52433660
110+
type ResultAux Result
111+
112+
// Unmarshal into auxiliary type
113+
var aux ResultAux
114+
if err := json.Unmarshal(data, &aux); err != nil {
115+
return err
116+
}
117+
118+
// Cast the auxilliary type to Result
119+
*r = Result(aux)
120+
121+
// Create a raw map to capture the results_id field
122+
var rawMap map[string]interface{}
123+
if err := json.Unmarshal(data, &rawMap); err != nil {
124+
return err
125+
}
126+
127+
// Check if results_id exists and assign it to DataID if so
128+
if resultsID, ok := rawMap["results_id"]; ok {
129+
if strID, ok := resultsID.(string); ok {
130+
r.DataID = strID
131+
}
132+
}
133+
134+
return nil
135+
}
136+
84137
// MarketPrice means - get me the best deal
85138
// job creators will do this by default i.e. "just buy me the cheapest"
86139
// FixedPrice means - take it or leave it

pkg/data/data_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//go:build unit
2+
3+
package data
4+
5+
import (
6+
"encoding/json"
7+
"testing"
8+
9+
"github.com/mr-tron/base58"
10+
"pgregory.net/rapid"
11+
)
12+
13+
func TestResultJSONRoundtrip(t *testing.T) {
14+
rapid.Check(t, func(t *rapid.T) {
15+
// Generate a random Result
16+
original := Result{
17+
ID: generateCID(t),
18+
DealID: generateCID(t),
19+
DataID: generateCID(t),
20+
}
21+
22+
// Test marshaling to JSON
23+
data, err := json.Marshal(original)
24+
if err != nil {
25+
t.Fatalf("Marshal failed: %v", err)
26+
}
27+
28+
// Test unmarshaling back
29+
var decoded Result
30+
err = json.Unmarshal(data, &decoded)
31+
if err != nil {
32+
t.Fatalf("Unmarshal failed: %v", err)
33+
}
34+
35+
// Verify the roundtrip preserved values
36+
if original != decoded {
37+
t.Errorf("Roundtrip failed: got %v, want %v", decoded, original)
38+
}
39+
40+
// Verify both fields exist in JSON
41+
var rawData map[string]interface{}
42+
err = json.Unmarshal(data, &rawData)
43+
if err != nil {
44+
t.Fatalf("Raw unmarshal failed: %v", err)
45+
}
46+
47+
// Check both data_id and results_id exist and match
48+
dataID, hasDataID := rawData["data_id"]
49+
resultsID, hasResultsID := rawData["results_id"]
50+
51+
if !hasDataID {
52+
t.Error("data_id field missing")
53+
}
54+
if !hasResultsID {
55+
t.Error("results_id field missing")
56+
}
57+
if dataID != resultsID {
58+
t.Errorf("data_id and results_id mismatch: %v != %v", dataID, resultsID)
59+
}
60+
})
61+
}
62+
63+
func TestResultJSONBackwardsCompatibility(t *testing.T) {
64+
rapid.Check(t, func(t *rapid.T) {
65+
expectedDataID := generateCID(t)
66+
67+
// Test old client format (results_id)
68+
oldClientJSON := map[string]interface{}{
69+
"id": generateCID(t),
70+
"results_id": expectedDataID,
71+
}
72+
oldClientData, _ := json.Marshal(oldClientJSON)
73+
74+
var resultFromOld Result
75+
err := json.Unmarshal(oldClientData, &resultFromOld)
76+
if err != nil {
77+
t.Fatalf("Unmarshal of old format failed: %v", err)
78+
}
79+
if resultFromOld.DataID != expectedDataID {
80+
t.Errorf("Old format: got DataID %v, want %v", resultFromOld.DataID, expectedDataID)
81+
}
82+
83+
// Test new client format (data_id)
84+
newClientJSON := map[string]interface{}{
85+
"id": generateCID(t),
86+
"data_id": expectedDataID,
87+
}
88+
newClientData, _ := json.Marshal(newClientJSON)
89+
90+
var resultFromNew Result
91+
err = json.Unmarshal(newClientData, &resultFromNew)
92+
if err != nil {
93+
t.Fatalf("Unmarshal of new format failed: %v", err)
94+
}
95+
if resultFromNew.DataID != expectedDataID {
96+
t.Errorf("New format: got DataID %v, want %v", resultFromNew.DataID, expectedDataID)
97+
}
98+
})
99+
}
100+
101+
// Generators
102+
103+
func generateCID(t *rapid.T) string {
104+
bytes := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw(t, "bytes")
105+
return "Qm" + base58.Encode(bytes)
106+
}

pkg/solver/matcher/match.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,50 @@ func (result ramMismatch) attributes() []attribute.KeyValue {
8181
}
8282
}
8383

84+
type vramMismatch struct {
85+
resourceOffer data.ResourceOffer
86+
jobOffer data.JobOffer
87+
}
88+
89+
func (_ vramMismatch) matched() bool { return false }
90+
func (_ vramMismatch) message() string { return "did not match VRAM" }
91+
func (result vramMismatch) attributes() []attribute.KeyValue {
92+
var resourceOfferVRAMs []int
93+
for _, gpu := range result.resourceOffer.Spec.GPUs {
94+
resourceOfferVRAMs = append(resourceOfferVRAMs, gpu.VRAM)
95+
}
96+
97+
var jobOfferVRAMS []int
98+
for _, gpu := range result.jobOffer.Spec.GPUs {
99+
jobOfferVRAMS = append(jobOfferVRAMS, gpu.VRAM)
100+
}
101+
102+
return []attribute.KeyValue{
103+
attribute.String("match_result", fmt.Sprintf("%T", result)),
104+
attribute.Bool("match_result.matched", result.matched()),
105+
attribute.String("match_result.message", result.message()),
106+
attribute.IntSlice("match_result.resource_offer.spec.gpus.vram", resourceOfferVRAMs),
107+
attribute.IntSlice("match_result.job_offer.spec.gpus.vram", jobOfferVRAMS),
108+
}
109+
}
110+
111+
type diskSpaceMismatch struct {
112+
resourceOffer data.ResourceOffer
113+
jobOffer data.JobOffer
114+
}
115+
116+
func (_ diskSpaceMismatch) matched() bool { return false }
117+
func (_ diskSpaceMismatch) message() string { return "did not match disk space" }
118+
func (result diskSpaceMismatch) attributes() []attribute.KeyValue {
119+
return []attribute.KeyValue{
120+
attribute.String("match_result", fmt.Sprintf("%T", result)),
121+
attribute.Bool("match_result.matched", result.matched()),
122+
attribute.String("match_result.message", result.message()),
123+
attribute.Int("match_result.job_offer.spec.disk", result.jobOffer.Spec.Disk),
124+
attribute.Int("match_result.resource_offer.spec.disk", result.resourceOffer.Spec.Disk),
125+
}
126+
}
127+
84128
type moduleIDError struct {
85129
resourceOffer data.ResourceOffer
86130
jobOffer data.JobOffer
@@ -227,6 +271,34 @@ func matchOffers(
227271
}
228272
}
229273

274+
// Skip VRAM check when job offer does not request VRAM
275+
if len(jobOffer.Spec.GPUs) > 0 {
276+
// Mismatch if job offer requests VRAM but resource provider has none
277+
if len(resourceOffer.Spec.GPUs) == 0 {
278+
return &vramMismatch{
279+
jobOffer: jobOffer,
280+
resourceOffer: resourceOffer,
281+
}
282+
}
283+
284+
// Mismatch if job offer largest VRAM is greater than resource offer largest VRAM
285+
largestResourceOfferVRAM := getLargestVRAM(resourceOffer.Spec.GPUs)
286+
largestJobOfferVRAM := getLargestVRAM(jobOffer.Spec.GPUs)
287+
if largestResourceOfferVRAM < largestJobOfferVRAM {
288+
return &vramMismatch{
289+
jobOffer: jobOffer,
290+
resourceOffer: resourceOffer,
291+
}
292+
}
293+
}
294+
295+
if resourceOffer.Spec.Disk < jobOffer.Spec.Disk {
296+
return &diskSpaceMismatch{
297+
jobOffer: jobOffer,
298+
resourceOffer: resourceOffer,
299+
}
300+
}
301+
230302
moduleID, err := data.GetModuleID(jobOffer.Module)
231303
if err != nil {
232304
return &moduleIDError{
@@ -295,6 +367,16 @@ func matchOffers(
295367
}
296368
}
297369

370+
func getLargestVRAM(gpus []data.GPUSpec) int {
371+
largestVRAM := 0
372+
for _, gpu := range gpus {
373+
if gpu.VRAM > largestVRAM {
374+
largestVRAM = gpu.VRAM
375+
}
376+
}
377+
return largestVRAM
378+
}
379+
298380
func logMatch(result matchResult) {
299381
switch r := result.(type) {
300382
case offersMatched:

0 commit comments

Comments
 (0)