diff --git a/Dockerfile.api b/Dockerfile.api index 305d2cf0c2..3615b94620 100644 --- a/Dockerfile.api +++ b/Dockerfile.api @@ -5,6 +5,6 @@ COPY go.mod go.sum ./ RUN go mod download COPY api ./api WORKDIR /app/api -RUN go build -o /lilysaas +RUN go build -o /helix EXPOSE 80 -ENTRYPOINT [ "/lilysaas" ] \ No newline at end of file +ENTRYPOINT [ "/helix" ] \ No newline at end of file diff --git a/README.md b/README.md index 8d1d38408c..fd8e81f9e0 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,3 @@ -# lilysaas +# helix - * what are the core database schema "things" - * what is the plan for taking payments? - * what are the key actions? - -## entities - - * auth - * use keycloak - * model - * json file to start with - * a list of things that lilypad can run - * job - * inference - * training (fine tunings) - * starts with an existing model - * produces a new model - -## plan - -This is to have a hello world that you can login to. - - * docker compose stack - * keycloak - * frontend (react & vite) - * api (go) - * postgres - - -## dev - -You need the following installed: - - * docker - * docker-compose - * [abigen](https://geth.ethereum.org/docs/getting-started/installing-geth) - * [bacalhau](https://github.com/bacalhau-project/lilypad/blob/fe9999b96d0920083ab3b1c4dbe4c647c5db36d3/CONTRIBUTING.md#bacalhau) - -You also need the lilypad repo cloned alongside this one. - -### smart contract interface - -When the smart contract in lilypad changes - checkout latest main of lilypad and then: - -```bash -./stack generate-golang-bindings -``` - -This will re-create the `api/pkg/contract/Modicum.go` - -## running alongside lilypad in local dev - -Make sure that you have cloned [lilypad](https://github.com/bacalhau-project/lilypad) at the same level as this repo. - -```bash -export RUN_LILYPAD=1 -./stack start -``` - -This will boot a fresh lilypad and lilysaas stack and link everything together using tmux. - -To run each part manually here are the guides: - -#### lilypad - -First start lilypad and then bacalhau: - -```bash -cd lilypad -./stack boot -``` - -Then we start bacalhau: - -**NOTE** you will require the correct version of bacalhau - run through [this guide](https://github.com/bacalhau-project/lilypad/blob/fe9999b96d0920083ab3b1c4dbe4c647c5db36d3/CONTRIBUTING.md#bacalhau): - -```bash -./stack bacalhau-serve -``` - -Now we need 3 other terminals each with `LOG_LEVEL=debug` - -```bash -./stack solver --server-url http://172.17.0.1:8080 -``` - -so it reports an address accessible from inside docker (the default docker bridge ip - this will probably only work on linux? on mac maybe you can use `host.docker.internal`) - - -```bash -./stack mediator -``` - -```bash -./stack resource-provider -``` - -Now we switch to lilysaas and get it booted alongside lilypad. - -#### lilysaas - -We need to create an top level `.env` file like so - -``` -export WEB3_PRIVATE_KEY=XXX -``` - -You can get the WEB3_PRIVATE_KEY value with this command: - -```bash -cat ../lilypad/.env | grep JOB_CREATOR_PRIVATE_KEY -``` - -Copy the value of this to be the `WEB3_PRIVATE_KEY` value in the `.env` file - the other values should be as shown. - -```bash -docker-compose up -d -``` - -Then we exec into the api container and run the api server: - -```bash -docker-compose exec api bash -go run . serve -``` \ No newline at end of file +Your own ChatGPT as a service. \ No newline at end of file diff --git a/api/cmd/lilysaas/root.go b/api/cmd/helix/root.go similarity index 89% rename from api/cmd/lilysaas/root.go rename to api/cmd/helix/root.go index 25221d0c46..6c0870c6de 100644 --- a/api/cmd/lilysaas/root.go +++ b/api/cmd/helix/root.go @@ -1,4 +1,4 @@ -package lilysaas +package helix import ( "context" @@ -16,8 +16,8 @@ func init() { //nolint:gochecknoinits func NewRootCmd() *cobra.Command { RootCmd := &cobra.Command{ Use: getCommandLineExecutable(), - Short: "LilySaaS", - Long: `LilySaaS`, + Short: "Helix", + Long: `Helix`, } RootCmd.AddCommand(newServeCmd()) return RootCmd diff --git a/api/cmd/lilysaas/serve.go b/api/cmd/helix/serve.go similarity index 93% rename from api/cmd/lilysaas/serve.go rename to api/cmd/helix/serve.go index a3d5e79ed4..4bcc04bbaf 100644 --- a/api/cmd/lilysaas/serve.go +++ b/api/cmd/helix/serve.go @@ -1,4 +1,4 @@ -package lilysaas +package helix import ( "context" @@ -8,12 +8,11 @@ import ( "os/signal" "path/filepath" - "github.com/bacalhau-project/lilysaas/api/pkg/controller" - "github.com/bacalhau-project/lilysaas/api/pkg/filestore" - "github.com/bacalhau-project/lilysaas/api/pkg/job" - "github.com/bacalhau-project/lilysaas/api/pkg/server" - "github.com/bacalhau-project/lilysaas/api/pkg/store" - "github.com/bacalhau-project/lilysaas/api/pkg/system" + "github.com/lukemarsden/helix/api/pkg/controller" + "github.com/lukemarsden/helix/api/pkg/filestore" + "github.com/lukemarsden/helix/api/pkg/server" + "github.com/lukemarsden/helix/api/pkg/store" + "github.com/lukemarsden/helix/api/pkg/system" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -35,7 +34,7 @@ func NewAllOptions() *AllOptions { }, FilestoreOptions: filestore.FileStoreOptions{ Type: filestore.FileStoreType(getDefaultServeOptionString("FILESTORE_TYPE", "fs")), - LocalFSPath: getDefaultServeOptionString("FILESTORE_LOCALFS_PATH", "/tmp/lilysaas/filestore"), + LocalFSPath: getDefaultServeOptionString("FILESTORE_LOCALFS_PATH", "/tmp/helix/filestore"), GCSKeyBase64: getDefaultServeOptionString("FILESTORE_GCS_KEY_BASE64", ""), GCSKeyFile: getDefaultServeOptionString("FILESTORE_GCS_KEY_FILE", ""), GCSBucket: getDefaultServeOptionString("FILESTORE_GCS_BUCKET", ""), @@ -43,7 +42,7 @@ func NewAllOptions() *AllOptions { StoreOptions: store.StoreOptions{ Host: getDefaultServeOptionString("POSTGRES_HOST", ""), Port: getDefaultServeOptionInt("POSTGRES_PORT", 5432), - Database: getDefaultServeOptionString("POSTGRES_DATABASE", "lilysaas"), + Database: getDefaultServeOptionString("POSTGRES_DATABASE", "helix"), Username: getDefaultServeOptionString("POSTGRES_USER", ""), Password: getDefaultServeOptionString("POSTGRES_PASSWORD", ""), AutoMigrate: true, @@ -63,8 +62,8 @@ func newServeCmd() *cobra.Command { serveCmd := &cobra.Command{ Use: "serve", - Short: "Start the lilysaas api server.", - Long: "Start the lilysaas api server.", + Short: "Start the helix api server.", + Long: "Start the helix api server.", Example: "TBD", RunE: func(cmd *cobra.Command, _ []string) error { return serve(cmd, allOptions) @@ -245,11 +244,6 @@ func serve(cmd *cobra.Command, options *AllOptions) error { return err } - jobRunner, err := job.NewJobRunner(ctx) - if err != nil { - return err - } - store, err := store.NewPostgresStore(options.StoreOptions) if err != nil { return err @@ -257,7 +251,6 @@ func serve(cmd *cobra.Command, options *AllOptions) error { options.ControllerOptions.Store = store options.ControllerOptions.Filestore = fs - options.ControllerOptions.JobRunner = jobRunner if options.FilestoreOptions.Type == filestore.FileStoreTypeLocalFS { options.ServerOptions.LocalFilestorePath = options.FilestoreOptions.LocalFSPath @@ -278,7 +271,7 @@ func serve(cmd *cobra.Command, options *AllOptions) error { return err } - log.Info().Msgf("LilySaaS server listening on %s:%d", options.ServerOptions.Host, options.ServerOptions.Port) + log.Info().Msgf("Helix server listening on %s:%d", options.ServerOptions.Host, options.ServerOptions.Port) go func() { err := server.ListenAndServe(ctx, cm) diff --git a/api/cmd/lilysaas/utils.go b/api/cmd/helix/utils.go similarity index 97% rename from api/cmd/lilysaas/utils.go rename to api/cmd/helix/utils.go index 5fa53424c3..e84dbf048a 100644 --- a/api/cmd/lilysaas/utils.go +++ b/api/cmd/helix/utils.go @@ -1,4 +1,4 @@ -package lilysaas +package helix import ( "os" diff --git a/api/main.go b/api/main.go index 997597f748..86021d5706 100644 --- a/api/main.go +++ b/api/main.go @@ -1,11 +1,11 @@ package main import ( - "github.com/bacalhau-project/lilysaas/api/cmd/lilysaas" "github.com/joho/godotenv" + "github.com/lukemarsden/helix/api/cmd/helix" ) func main() { _ = godotenv.Load() - lilysaas.Execute() + helix.Execute() } diff --git a/api/pkg/controller/controller.go b/api/pkg/controller/controller.go index 61eac095ba..413c025997 100644 --- a/api/pkg/controller/controller.go +++ b/api/pkg/controller/controller.go @@ -7,18 +7,16 @@ import ( "sync" "time" - "github.com/bacalhau-project/lilypad/pkg/data" - "github.com/bacalhau-project/lilysaas/api/pkg/filestore" - "github.com/bacalhau-project/lilysaas/api/pkg/job" - "github.com/bacalhau-project/lilysaas/api/pkg/store" - "github.com/bacalhau-project/lilysaas/api/pkg/types" + "github.com/lukemarsden/helix/api/pkg/filestore" + "github.com/lukemarsden/helix/api/pkg/model" + "github.com/lukemarsden/helix/api/pkg/store" + "github.com/lukemarsden/helix/api/pkg/types" "github.com/rs/zerolog/log" ) type ControllerOptions struct { Store store.Store Filestore filestore.FileStore - JobRunner *job.JobRunner // this is an "env" prefix like "dev" // the user prefix is handled inside the controller // (see getFilestorePath) @@ -35,8 +33,19 @@ type ControllerOptions struct { type Controller struct { Ctx context.Context Options ControllerOptions - JobUpdatesChan chan *types.Job SessionUpdatesChan chan *types.Session + // the backlog of sessions that need a GPU + sessionQueue []*types.Session + sessionQueueMtx sync.Mutex + // the map of active sessions that are currently running on a GPU + activeSessions map[string]*types.Session + activeSessionMtx sync.Mutex + + // the map of text streams attached to a session + // not all sessions will have an active text stream + // it depends what type the session is + activeTextStreams map[string]*model.TextStream + activeTextStreamsMtx sync.Mutex } func NewController( @@ -49,152 +58,67 @@ func NewController( if options.Filestore == nil { return nil, fmt.Errorf("filestore is required") } - if options.JobRunner == nil { - return nil, fmt.Errorf("job runner is required") - } controller := &Controller{ Ctx: ctx, Options: options, - JobUpdatesChan: make(chan *types.Job), SessionUpdatesChan: make(chan *types.Session), + activeSessions: map[string]*types.Session{}, + activeTextStreams: map[string]*model.TextStream{}, + sessionQueue: []*types.Session{}, } return controller, nil } func (c *Controller) Start() error { + err := c.loadSessionQueues(c.Ctx) + if err != nil { + return err + } go func() { for { select { case <-c.Ctx.Done(): return - case err := <-c.Options.JobRunner.ErrorChan: - log.Error().Msgf("Lilypad error in job runner: %s", err.Error()) - return default: - log.Debug().Msg("Starting loopSessions") - time.Sleep(1 * time.Second) - err := c.loopSessions(c.Ctx) - if err != nil { - log.Error().Msgf("Lilypad error in controller loop: %s", err.Error()) - debug.PrintStack() - } - err = c.loop(c.Ctx) + time.Sleep(10 * time.Second) + err := c.loop(c.Ctx) if err != nil { - log.Error().Msgf("Lilypad error in controller loop: %s", err.Error()) + log.Error().Msgf("Helix error in controller loop: %s", err.Error()) debug.PrintStack() } } } }() - c.Options.JobRunner.Subscribe(c.Ctx, c.handleJobUpdate) return nil } func (c *Controller) loop(ctx context.Context) error { - var wg sync.WaitGroup - errChan := make(chan error, 1) - - // Wrap the function in a closure and handle the WaitGroup and error channel - runFunc := func(f func(context.Context) error) { - defer wg.Done() - if err := f(ctx); err != nil { - select { - case errChan <- err: - default: - } - } - } - - wg.Add(1) - - // an example of a function that is called in the loop - go runFunc(c.checkForRunningJobs) - - go func() { - wg.Wait() - close(errChan) - }() - - if err := <-errChan; err != nil { - return err - } - return nil -} - -func (c *Controller) loopSessions(ctx context.Context) error { - var wg sync.WaitGroup - errChan := make(chan error, 1) - - // Wrap the function in a closure and handle the WaitGroup and error channel - runFunc := func(f func(context.Context) error) { - defer wg.Done() - if err := f(ctx); err != nil { - select { - case errChan <- err: - default: - } - } - } - - wg.Add(1) - - // an example of a function that is called in the loop - go runFunc(c.triggerSessionTasks) - - go func() { - wg.Wait() - close(errChan) - }() - - if err := <-errChan; err != nil { - return err - } - return nil -} - -func (c *Controller) handleJobUpdate(evOffer data.JobOfferContainer) { - job, err := c.Options.Store.GetJob(context.Background(), evOffer.ID) - if err != nil { - fmt.Printf("error loading job: %s\n", err.Error()) - return - } - // we have a race condition where we need to write the job to the solver to get - // it's ID and then we might not have written the job to the database yet - // TODO: make lilypad have a way to have deterministic ID's so we can know the - // job ID before submitting it - if job == nil { - // this means the job has not been written to the database yet (probably) - time.Sleep(time.Millisecond * 100) - job, err = c.Options.Store.GetJob(context.Background(), evOffer.ID) - if err != nil { - return - } - if job == nil { - fmt.Printf("job not found: %s\n", evOffer.ID) - return - } - } - jobData := job.Data - jobData.Container = evOffer - - c.Options.Store.UpdateJob( - c.Ctx, - evOffer.ID, - data.GetAgreementStateString(evOffer.State), - "", - jobData, - ) - - job, err = c.Options.Store.GetJob(context.Background(), evOffer.ID) - if err != nil { - fmt.Printf("error loading job: %s\n", err.Error()) - return - } - - c.JobUpdatesChan <- job -} - -// load all jobs that are currently running and check if they are still running -func (c *Controller) checkForRunningJobs(ctx context.Context) error { + // var wg sync.WaitGroup + // errChan := make(chan error, 1) + + // // Wrap the function in a closure and handle the WaitGroup and error channel + // runFunc := func(f func(context.Context) error) { + // defer wg.Done() + // if err := f(ctx); err != nil { + // select { + // case errChan <- err: + // default: + // } + // } + // } + + // wg.Add(1) + + // // an example of a function that is called in the loop + // go runFunc(c.reloadSessionQueues) + + // go func() { + // wg.Wait() + // close(errChan) + // }() + + // if err := <-errChan; err != nil { + // return err + // } return nil } diff --git a/api/pkg/controller/filestore.go b/api/pkg/controller/filestore.go index dc8a8b540d..5b6db218c3 100644 --- a/api/pkg/controller/filestore.go +++ b/api/pkg/controller/filestore.go @@ -8,8 +8,8 @@ import ( "path/filepath" "text/template" - "github.com/bacalhau-project/lilysaas/api/pkg/filestore" - "github.com/bacalhau-project/lilysaas/api/pkg/types" + "github.com/lukemarsden/helix/api/pkg/filestore" + "github.com/lukemarsden/helix/api/pkg/types" ) type userPathTemplateData struct { diff --git a/api/pkg/controller/handlers.go b/api/pkg/controller/handlers.go index 6fffd481aa..c90e94c40b 100644 --- a/api/pkg/controller/handlers.go +++ b/api/pkg/controller/handlers.go @@ -1,11 +1,8 @@ package controller import ( - "github.com/bacalhau-project/lilypad/pkg/data" - jobutils "github.com/bacalhau-project/lilysaas/api/pkg/job" - "github.com/bacalhau-project/lilysaas/api/pkg/store" - "github.com/bacalhau-project/lilysaas/api/pkg/system" - "github.com/bacalhau-project/lilysaas/api/pkg/types" + "github.com/lukemarsden/helix/api/pkg/store" + "github.com/lukemarsden/helix/api/pkg/types" ) func (c *Controller) GetStatus(ctx types.RequestContext) (types.UserStatus, error) { @@ -28,58 +25,9 @@ func (c *Controller) GetStatus(ctx types.RequestContext) (types.UserStatus, erro }, nil } -func (c *Controller) GetJobs(ctx types.RequestContext) ([]*types.Job, error) { - return c.Options.Store.GetJobs(ctx.Ctx, store.GetJobsQuery{ - Owner: ctx.Owner, - OwnerType: ctx.OwnerType, - }) -} - func (c *Controller) GetTransactions(ctx types.RequestContext) ([]*types.BalanceTransfer, error) { return c.Options.Store.GetBalanceTransfers(ctx.Ctx, store.GetBalanceTransfersQuery{ Owner: ctx.Owner, OwnerType: ctx.OwnerType, }) } - -func (c *Controller) CreateJob(ctx types.RequestContext, request types.JobSpec) (data.JobOfferContainer, error) { - container, err := c.Options.JobRunner.RunJob(ctx.Ctx, request) - if err != nil { - return container, err - } - module, err := jobutils.GetModule(request.Module) - if err != nil { - return container, err - } - err = c.Options.Store.CreateBalanceTransfer(ctx.Ctx, types.BalanceTransfer{ - ID: system.GenerateUUID(), - Owner: ctx.Owner, - OwnerType: ctx.OwnerType, - PaymentType: types.PaymentTypeJob, - Amount: -module.Cost, - Data: types.BalanceTransferData{ - JobID: container.ID, - }, - }) - if err != nil { - return container, err - } - err = c.Options.Store.CreateJob(ctx.Ctx, types.Job{ - ID: container.ID, - Owner: ctx.Owner, - OwnerType: ctx.OwnerType, - State: data.GetAgreementStateString(container.State), - Status: "", - Data: types.JobData{ - Spec: types.JobSpec{ - Module: request.Module, - Inputs: request.Inputs, - }, - Container: container, - }, - }) - if err != nil { - return container, err - } - return container, err -} diff --git a/api/pkg/controller/models.go b/api/pkg/controller/models.go index 5ff8796daf..faa6624fe9 100644 --- a/api/pkg/controller/models.go +++ b/api/pkg/controller/models.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/bacalhau-project/lilysaas/api/pkg/types" + "github.com/lukemarsden/helix/api/pkg/types" ) //////////////////////////////////////////////////////////////////////////////// @@ -49,12 +49,12 @@ func (l *LanguageModel) Mistral_7B_Instruct_v0_1(ctx context.Context) { // . venv/bin/activate; // python -u -m axolotl.cli.inference examples/mistral/qlora-instruct.yml"`, // ) - // luke@mind:~/pd/lilysaas$ echo "[INST]i really like you[/INST]" |docker run --gpus all -i quay.io/lukemarsden/axolotl:v0.0.1 python -u -m axolotl.cli.inference examples/mistral/qlora-instruct.yml + // luke@mind:~/pd/helix$ echo "[INST]i really like you[/INST]" |docker run --gpus all -i quay.io/lukemarsden/axolotl:v0.0.1 python -u -m axolotl.cli.inference examples/mistral/qlora-instruct.yml l.streamOutput( "[INST]"+lastUserMessage+"[/INST]", "[/INST]", "", - "ssh", "-o", "StrictHostKeyChecking=no", "luke@172.17.0.1", `bash -c " + "ssh", "-o", "StrictHostKeyChecking=no", "kai@192.168.86.40", `bash -c " docker run --gpus all -i quay.io/lukemarsden/axolotl:v0.0.1 python -u -m axolotl.cli.inference examples/mistral/qlora-instruct.yml"`, ) // echo "prove pythagoras theorem" | -m axolotl.cli.inference examples/mistral/qlora.yml diff --git a/api/pkg/controller/sessions.go b/api/pkg/controller/sessions.go index 20bd52bc2b..d5651d0231 100644 --- a/api/pkg/controller/sessions.go +++ b/api/pkg/controller/sessions.go @@ -7,168 +7,310 @@ import ( "fmt" "log" - "github.com/bacalhau-project/lilysaas/api/pkg/store" - "github.com/bacalhau-project/lilysaas/api/pkg/types" + "github.com/davecgh/go-spew/spew" + "github.com/lukemarsden/helix/api/pkg/model" + "github.com/lukemarsden/helix/api/pkg/store" + "github.com/lukemarsden/helix/api/pkg/types" ) // set to false in production (will log messages to web UI) const DEBUG = true -// load all jobs that are currently running and check if they are still running -func (c *Controller) triggerSessionTasks(ctx context.Context) error { - log.Println("Starting triggerSessionTasks") - // NB: for the demo, being serialized here is good: it means we'll only - // spawn one GPU task at a time, and run less risk of GPU OOM. Later, we'll - // need to figure out how to scale/parallelize this, which is what the - // Lilypad/Kubernetes schedulers are for +// the core function - decide which task to give to a worker +// TODO: keep track of the previous tasks run by this worker (and therefore we know which weights are loaded into RAM) +// try to send similar tasks to the same worker +func (c *Controller) ShiftSessionQueue(ctx context.Context, filter types.SessionFilter) (*types.Session, error) { + c.sessionQueueMtx.Lock() + defer c.sessionQueueMtx.Unlock() - st := c.Options.Store - // fetch all sessions - sessions, err := st.GetSessions(ctx, store.GetSessionsQuery{}) - if err != nil { - return err + // right now this is very dumb - it literally just returns the next thing and doesn't even care what type it is + // TODO: get the worker auth system plugged in so we know who is asking for the task + // and then we can keep track of the last thing they ran and pick better + for i, session := range c.sessionQueue { + if filter.Mode != "" && session.Mode != filter.Mode { + continue + } + if filter.Type != "" && session.Type != filter.Type { + continue + } + if filter.ModelName != "" && session.ModelName != filter.ModelName { + continue + } + c.sessionQueue = append(c.sessionQueue[:i], c.sessionQueue[i+1:]...) + return session, nil } - for _, session := range sessions { + return nil, nil +} - st := c.Options.Store +func (c *Controller) ConvertSessionToTask(ctx context.Context, session *types.Session) (*types.WorkerTask, error) { + if session == nil { + return nil, nil + } - msgs := session.Interactions.Messages - if len(msgs) == 0 { - // should never happen, sessions are always initiated by the user - // creating an initial message - continue - } + task := &types.WorkerTask{ + SessionID: session.ID, + Mode: session.Mode, + Type: session.Type, + ModelName: session.ModelName, + } - latest := msgs[len(msgs)-1] - if latest.User == "system" { - // we've already given a response, don't need to do anything - continue + switch { + case session.Mode == "Create" && session.Type == "Text": + model, err := model.GetLanguageModel(session.ModelName) + if err != nil { + return nil, err + } + prompt, err := model.GetPrompt(ctx, session) + if err != nil { + return nil, err + } + task.Prompt = prompt + return task, nil + case session.Mode == "Create" && session.Type == "Image": + model, err := model.GetImageModel(session.ModelName) + if err != nil { + return nil, err } + prompt, err := model.GetPrompt(ctx, session) + if err != nil { + return nil, err + } + task.Prompt = prompt + return task, nil + case session.Mode == "Finetune" && session.Type == "Text": + return nil, nil + case session.Mode == "Finetune" && session.Type == "Image": + return nil, nil + } + return nil, nil +} - // if we didn't continue here, we need to read from the various channels - // until we read from the FinishChan - debugStream := make(chan string) - outputStream := make(chan string) - finishChan := make(chan error) - - switch { - case session.Mode == "Create" && session.Type == "Text": - // session for text generation - llm := LanguageModel{ - Interactions: session.Interactions, - DebugStream: debugStream, - OutputStream: outputStream, - FinishChan: finishChan, - } - go llm.Mistral_7B_Instruct_v0_1(ctx) +// add the given session onto the end of the queue +// unless it's already waiting and present in the queue +// in which case let's replace it at it's current position +func (c *Controller) PushSessionQueue(ctx context.Context, session *types.Session) error { + c.sessionQueueMtx.Lock() + defer c.sessionQueueMtx.Unlock() + + existing := false + newQueue := []*types.Session{} + for _, existingSession := range c.sessionQueue { + if existingSession.ID == session.ID { + newQueue = append(newQueue, session) + existing = true + } else { + newQueue = append(newQueue, existingSession) + } + } + if !existing { + newQueue = append(newQueue, session) + } - case session.Mode == "Create" && session.Type == "Image": - // session for image generation + c.sessionQueue = newQueue + return nil +} - // TODO: set Prompt, etc, from interactions +func (c *Controller) AddActiveSession(ctx context.Context, session *types.Session) error { + c.activeSessionMtx.Lock() + defer c.activeSessionMtx.Unlock() - t2i := TextToImage{ - DebugStream: debugStream, - OutputStream: outputStream, - FinishChan: finishChan, - } - go t2i.SDXL_1_0_Base(ctx) + c.activeSessions[session.ID] = session - case session.Mode == "Finetune" && session.Type == "Text": - // session for text finetuning + // spawn a new text stream to listen in for responses + if session.Type == "Text" && session.Mode == "Create" { + sessionModel, err := model.GetLanguageModel(session.ModelName) + if err != nil { + return err + } - // TODO: we might want to check that we have the QA correctly edited - // and have the user click an explicit "Start" button before - // proceeding here + // this knows how to parse the output of the model + textStream, err := sessionModel.GetTextStream(ctx) + if err != nil { + return err + } - // TODO: set InputDataset + c.activeTextStreamsMtx.Lock() + defer c.activeTextStreamsMtx.Unlock() + c.activeTextStreams[session.ID] = textStream - llm_ft := FinetuneLanguageModel{ - DebugStream: debugStream, - OutputStream: outputStream, - FinishChan: finishChan, - } - go llm_ft.Mistral_7B_Instruct_v0_1(ctx) + go textStream.Start(ctx) - case session.Mode == "Finetune" && session.Type == "Image": - // session for image finetuning + // this is what will listen to the text stream and send messages to the + // database and the websockets + go func() { + for { + select { + case msg := <-textStream.Output: + func() { + c.activeSessionMtx.Lock() + defer c.activeSessionMtx.Unlock() - // TODO: we might want to check that we have the image labels - // correctly added and have the user click an explicit "Start" - // button (an interaction type) before proceeding here + msgs := session.Interactions.Messages + latest := msgs[len(msgs)-1] + latest.Message += msg + msgs[len(msgs)-1] = latest + session.Interactions.Messages = msgs - // TODO: set InputPath, OutputPath + _, err := c.Options.Store.UpdateSession(ctx, *session) + if err != nil { + log.Printf("Error adding message: %s", err) + } - t2i_ft := FinetuneTextToImage{ - DebugStream: debugStream, - OutputStream: outputStream, - FinishChan: finishChan, - } - go t2i_ft.SDXL_1_0_Base_Finetune(ctx) - - default: - return fmt.Errorf("invalid mode or session type") - } - - firstMessage := true - addMessage := func(msg string, finished bool) { - // need to add a system response (computer always has the last word) - if firstMessage { - msgs = append(msgs, types.UserMessage{ - User: "system", - Message: msg, - Uploads: []string{}, // cool, computer can create images here - Finished: finished, - }) - firstMessage = false - } else { - latest := msgs[len(msgs)-1] - latest.Message += msg - if finished { - latest.Finished = true + c.SessionUpdatesChan <- session + }() + fmt.Print("Got message from text stream: ", msg) } - msgs[len(msgs)-1] = latest } - session.Interactions.Messages = msgs + }() + } + return nil +} - // write it to the database. we'll inform any connected webuis over the - // web interface as well - s, err := st.UpdateSession(ctx, *session) - if err != nil { - log.Printf("Error adding message: %s", err) - } - // can i send websockets? - c.SessionUpdatesChan <- s +func (c *Controller) GetActiveSession(ctx context.Context, id string) (*types.Session, error) { + c.activeSessionMtx.Lock() + defer c.activeSessionMtx.Unlock() + session, ok := c.activeSessions[id] + if !ok { + return nil, fmt.Errorf("session not found") + } + return session, nil +} + +func (c *Controller) GetActiveTextStream(ctx context.Context, id string) (*model.TextStream, error) { + c.activeTextStreamsMtx.Lock() + defer c.activeTextStreamsMtx.Unlock() + textStream, ok := c.activeTextStreams[id] + if !ok { + return nil, fmt.Errorf("text stream not found") + } + return textStream, nil +} + +func (c *Controller) RemoveActiveSession(ctx context.Context, id string) error { + c.activeSessionMtx.Lock() + defer c.activeSessionMtx.Unlock() + if _, ok := c.activeSessions[id]; !ok { + return fmt.Errorf("session not found") + } + delete(c.activeSessions, id) + return nil +} + +func (c *Controller) RemoveActiveTextStream(ctx context.Context, id string) error { + c.activeTextStreamsMtx.Lock() + defer c.activeTextStreamsMtx.Unlock() + if _, ok := c.activeTextStreams[id]; !ok { + return fmt.Errorf("text stream not found") + } + delete(c.activeTextStreams, id) + return nil +} + +// if the action is "begin" - then we need to ceate a new textstream that is hooked up correctly +// then we stash that in a map +// if the action is "continue" - load the textstream and write to it +// if the action is "end" - unload the text stream +func (c *Controller) HandleWorkerResponse(ctx context.Context, taskResponse *types.WorkerTaskResponse) (*types.WorkerTaskResponse, error) { + session, err := c.GetActiveSession(ctx, taskResponse.SessionID) + if err != nil { + return nil, err + } + + switch { + case session.Mode == "Create" && session.Type == "Text": + return c.handleWorkerResponseLanguageInference(ctx, taskResponse, session) + case session.Mode == "Create" && session.Type == "Image": + return c.handleWorkerResponseImageInference(ctx, taskResponse, session) + case session.Mode == "Finetune" && session.Type == "Text": + return nil, nil + case session.Mode == "Finetune" && session.Type == "Image": + return nil, nil + } + return nil, nil +} + +func (c *Controller) handleWorkerResponseLanguageInference(ctx context.Context, taskResponse *types.WorkerTaskResponse, session *types.Session) (*types.WorkerTaskResponse, error) { + if taskResponse.Action == types.WorkerTaskResponseAction_Begin { + session.Interactions.Messages = append(session.Interactions.Messages, types.UserMessage{ + User: "system", + Message: taskResponse.Message, + Uploads: []string{}, // cool, computer can create images here + Finished: false, + }) + _, err := c.Options.Store.UpdateSession(ctx, *session) + if err != nil { + return nil, err + } + c.SessionUpdatesChan <- session + return taskResponse, nil + } else if taskResponse.Action == types.WorkerTaskResponseAction_Continue { + textStream, err := c.GetActiveTextStream(ctx, taskResponse.SessionID) + if err != nil { + return nil, err + } + textStream.Write([]byte(taskResponse.Message)) + return taskResponse, nil + } else if taskResponse.Action == types.WorkerTaskResponseAction_End { + textStream, err := c.GetActiveTextStream(ctx, taskResponse.SessionID) + if err != nil { + return nil, err + } + err = textStream.Close(ctx) + if err != nil { + return nil, err + } + err = c.RemoveActiveTextStream(ctx, taskResponse.SessionID) + if err != nil { + return nil, err } + return taskResponse, nil + } else { + return nil, nil + } +} - // TODO: handle images coming out of the models - for { - select { - case debugMsg := <-debugStream: - fmt.Println("Debug message:", debugMsg) - if DEBUG { - addMessage(debugMsg, false) - } - case outputMsg := <-outputStream: - fmt.Println("Output message:", outputMsg) - addMessage(outputMsg, false) - - case err := <-finishChan: - fmt.Println("Finish chan:", err) - if err != nil { - fmt.Println("Error:", err) - addMessage("\nError: "+err.Error(), true) - } else { - fmt.Println("Finished successfully") - addMessage("", true) - } - // maybe factor the main body of this loop into a separate fn so - // we can just return instead of using goto, heh - goto nextSession - } +func (c *Controller) handleWorkerResponseImageInference(ctx context.Context, taskResponse *types.WorkerTaskResponse, session *types.Session) (*types.WorkerTaskResponse, error) { + fmt.Printf(" --------------------------------------\n") + spew.Dump(taskResponse) + return taskResponse, nil +} + +// load the session queues from the database in case of restart +func (c *Controller) loadSessionQueues(ctx context.Context) error { + c.sessionQueueMtx.Lock() + defer c.sessionQueueMtx.Unlock() + + sessionQueue := []*types.Session{} + + st := c.Options.Store + + // fetch all sessions - this is in DESC order so we need to reverse the array + sessions, err := st.GetSessions(ctx, store.GetSessionsQuery{}) + if err != nil { + return err + } + + for i := len(sessions) - 1; i >= 0; i-- { + session := sessions[i] + + msgs := session.Interactions.Messages + if len(msgs) == 0 { + // should never happen, sessions are always initiated by the user + // creating an initial message + continue + } + + latest := msgs[len(msgs)-1] + if latest.User == "system" { + // we've already given a response, don't need to do anything + continue } - nextSession: + + sessionQueue = append(sessionQueue, session) } + + // now we have the queue in oldest first order + c.sessionQueue = sessionQueue return nil } diff --git a/api/pkg/job/modules.go b/api/pkg/job/modules.go deleted file mode 100644 index b8dc1fa607..0000000000 --- a/api/pkg/job/modules.go +++ /dev/null @@ -1,46 +0,0 @@ -package job - -import ( - "embed" - "encoding/json" - "fmt" - "io" - - "github.com/bacalhau-project/lilysaas/api/pkg/types" -) - -//go:embed modules.json -var jsonFile embed.FS - -func GetModules() ([]types.Module, error) { - file, err := jsonFile.Open("modules.json") - if err != nil { - return []types.Module{}, err - } - defer file.Close() - - content, err := io.ReadAll(file) - if err != nil { - return []types.Module{}, err - } - - var moduleList []types.Module - if err := json.Unmarshal(content, &moduleList); err != nil { - return []types.Module{}, err - } - - return moduleList, nil -} - -func GetModule(id string) (types.Module, error) { - modules, err := GetModules() - if err != nil { - return types.Module{}, err - } - for _, module := range modules { - if module.ID == id { - return module, nil - } - } - return types.Module{}, fmt.Errorf("module not found: %s", id) -} diff --git a/api/pkg/job/modules.json b/api/pkg/job/modules.json deleted file mode 100644 index 3b13c7f49d..0000000000 --- a/api/pkg/job/modules.json +++ /dev/null @@ -1,11 +0,0 @@ -[{ - "id": "cowsay:v0.0.1", - "title": "Cow Say", - "cost": 10, - "template": "text-to-text" -}, { - "id": "sdxl:v0.0.1", - "title": "SDXL", - "cost": 10, - "template": "text-to-image" -}] \ No newline at end of file diff --git a/api/pkg/job/options.go b/api/pkg/job/options.go deleted file mode 100644 index 7abbfae1f0..0000000000 --- a/api/pkg/job/options.go +++ /dev/null @@ -1,57 +0,0 @@ -package job - -import ( - "fmt" - - "github.com/bacalhau-project/lilypad/pkg/jobcreator" - optionsfactory "github.com/bacalhau-project/lilypad/pkg/options" - "github.com/bacalhau-project/lilysaas/api/pkg/types" -) - -func checkJobCreatorOptions(options jobcreator.JobCreatorOptions, withModule bool) error { - if withModule { - err := optionsfactory.CheckModuleOptions(options.Offer.Module) - if err != nil { - return err - } - } - - err := optionsfactory.CheckWeb3Options(options.Web3) - if err != nil { - return err - } - err = optionsfactory.CheckServicesOptions(options.Offer.Services) - if err != nil { - return err - } - - if options.Mediation.CheckResultsPercentage < 0 || options.Mediation.CheckResultsPercentage > 100 { - return fmt.Errorf("mediation-chance must be between 0 and 100") - } - - return nil -} - -func ProcessJobCreatorOptions(options jobcreator.JobCreatorOptions, request types.JobSpec, withModule bool) (jobcreator.JobCreatorOptions, error) { - if withModule { - options.Offer.Module.Name = request.Module - options.Offer.Inputs = request.Inputs - moduleOptions, err := optionsfactory.ProcessModuleOptions(options.Offer.Module) - if err != nil { - return options, err - } - options.Offer.Module = moduleOptions - } - - newWeb3Options, err := optionsfactory.ProcessWeb3Options(options.Web3) - if err != nil { - return options, err - } - options.Web3 = newWeb3Options - - return options, checkJobCreatorOptions(options, withModule) -} - -func GetJobOptions(request types.JobSpec, withModule bool) (jobcreator.JobCreatorOptions, error) { - return ProcessJobCreatorOptions(optionsfactory.NewJobCreatorOptions(), request, withModule) -} diff --git a/api/pkg/job/runner.go b/api/pkg/job/runner.go deleted file mode 100644 index d6a20a7175..0000000000 --- a/api/pkg/job/runner.go +++ /dev/null @@ -1,93 +0,0 @@ -package job - -import ( - "context" - "time" - - "github.com/bacalhau-project/lilypad/pkg/data" - "github.com/bacalhau-project/lilypad/pkg/jobcreator" - lilypadsystem "github.com/bacalhau-project/lilypad/pkg/system" - "github.com/bacalhau-project/lilypad/pkg/web3" - "github.com/bacalhau-project/lilysaas/api/pkg/types" - "github.com/spf13/cobra" -) - -type JobRunner struct { - Ctx context.Context - Options jobcreator.JobCreatorOptions - Web3SDK *web3.Web3SDK - JobCreator *jobcreator.JobCreator - ErrorChan chan error -} - -func NewJobRunner(ctx context.Context) (*JobRunner, error) { - // get options without a job to bootstrap the sdk and jobcreator - options, err := GetJobOptions(types.JobSpec{ - Module: "", - Inputs: map[string]string{}, - }, false) - if err != nil { - return nil, err - } - web3SDK, err := web3.NewContractSDK(options.Web3) - if err != nil { - return nil, err - } - jobCreatorService, err := jobcreator.NewJobCreator(options, web3SDK) - if err != nil { - return nil, err - } - tmpCommand := &cobra.Command{} - tmpCommand.SetContext(ctx) - cmdCtx := lilypadsystem.NewCommandContext(tmpCommand) - - jobCreatorErrors := jobCreatorService.Start(cmdCtx.Ctx, cmdCtx.Cm) - - // wait a short period because we've just started the job creator service - time.Sleep(100 * time.Millisecond) - return &JobRunner{ - Options: options, - Web3SDK: web3SDK, - JobCreator: jobCreatorService, - ErrorChan: jobCreatorErrors, - }, nil -} - -func (runner *JobRunner) Subscribe(ctx context.Context, callback jobcreator.JobOfferSubscriber) { - runner.JobCreator.SubscribeToJobOfferUpdates(callback) -} - -func (runner *JobRunner) GetJobOffer(ctx context.Context, request types.JobSpec) (data.JobOffer, error) { - options, err := GetJobOptions(request, true) - if err != nil { - return data.JobOffer{}, err - } - return runner.JobCreator.GetJobOfferFromOptions(options.Offer) -} - -func (runner *JobRunner) GetJobContainer(ctx context.Context, request types.JobSpec) (data.JobOfferContainer, error) { - jobOffer, err := runner.GetJobOffer(ctx, request) - if err != nil { - return data.JobOfferContainer{}, err - } - id, err := data.GetJobOfferID(jobOffer) - if err != nil { - return data.JobOfferContainer{}, err - } - jobOffer.ID = id - container := data.GetJobOfferContainer(jobOffer) - return container, nil -} - -func (runner *JobRunner) RunJob(ctx context.Context, request types.JobSpec) (data.JobOfferContainer, error) { - jobOffer, err := runner.GetJobOffer(ctx, request) - if err != nil { - return data.JobOfferContainer{}, err - } - return runner.JobCreator.AddJobOffer(jobOffer) - - // result, err := jobCreatorService.GetResult(finalJobOffer.DealID) - // if err != nil { - // return nil, err - // } -} diff --git a/api/pkg/model/mistral7b.go b/api/pkg/model/mistral7b.go new file mode 100644 index 0000000000..bddd509d09 --- /dev/null +++ b/api/pkg/model/mistral7b.go @@ -0,0 +1,30 @@ +package model + +import ( + "context" + "fmt" + + "github.com/lukemarsden/helix/api/pkg/types" +) + +type Mistral7bInstruct01 struct { +} + +func (l *Mistral7bInstruct01) GetPrompt(ctx context.Context, session *types.Session) (string, error) { + if len(session.Interactions.Messages) == 0 { + return "", fmt.Errorf("session has no messages") + } + lastMessage := session.Interactions.Messages[len(session.Interactions.Messages)-1] + return fmt.Sprintf("[INST]%s[/INST]", lastMessage.Message), nil +} + +func (l *Mistral7bInstruct01) GetTextStream(ctx context.Context) (*TextStream, error) { + return NewTextStream( + splitOnSpace, + "[/INST]", + "", + ), nil +} + +// Compile-time interface check: +var _ LanguageModel = (*Mistral7bInstruct01)(nil) diff --git a/api/pkg/model/models.go b/api/pkg/model/models.go new file mode 100644 index 0000000000..cbe108fec8 --- /dev/null +++ b/api/pkg/model/models.go @@ -0,0 +1,32 @@ +package model + +import ( + "context" + "fmt" + + "github.com/lukemarsden/helix/api/pkg/types" +) + +// given a model name - reutrn the correct language model +func GetLanguageModel(model types.ModelName) (LanguageModel, error) { + if model == types.Model_Mistral7b { + return &Mistral7bInstruct01{}, nil + } + return nil, fmt.Errorf("no model for model name %s", model) +} + +func GetImageModel(model types.ModelName) (ImageModel, error) { + if model == types.Model_SDXL { + return &SDXL{}, nil + } + return nil, fmt.Errorf("no model for model name %s", model) +} + +func GetModelNameForSession(ctx context.Context, session *types.Session) (types.ModelName, error) { + if session.Type == "Image" { + return types.Model_SDXL, nil + } else if session.Type == "Text" { + return types.Model_Mistral7b, nil + } + return types.Model_None, fmt.Errorf("no model for session type %s", session.Type) +} diff --git a/api/pkg/model/sdxl.go b/api/pkg/model/sdxl.go new file mode 100644 index 0000000000..410181817c --- /dev/null +++ b/api/pkg/model/sdxl.go @@ -0,0 +1,22 @@ +package model + +import ( + "context" + "fmt" + + "github.com/lukemarsden/helix/api/pkg/types" +) + +type SDXL struct { +} + +func (l *SDXL) GetPrompt(ctx context.Context, session *types.Session) (string, error) { + if len(session.Interactions.Messages) == 0 { + return "", fmt.Errorf("session has no messages") + } + lastMessage := session.Interactions.Messages[len(session.Interactions.Messages)-1] + return lastMessage.Message, nil +} + +// Compile-time interface check: +var _ ImageModel = (*SDXL)(nil) diff --git a/api/pkg/model/textstream.go b/api/pkg/model/textstream.go new file mode 100644 index 0000000000..9121db1785 --- /dev/null +++ b/api/pkg/model/textstream.go @@ -0,0 +1,67 @@ +package model + +import ( + "bufio" + "context" + "io" + "log" + "strings" +) + +// a configurable text stream to process llm output +type TextStream struct { + reader *io.PipeReader + writer *io.PipeWriter + // this is normally splitOnSpace + splitter bufio.SplitFunc + start string + ignore string + Output chan string +} + +func NewTextStream( + splitter bufio.SplitFunc, + start string, + ignore string, +) *TextStream { + reader, writer := io.Pipe() + stream := &TextStream{ + reader: reader, + writer: writer, + splitter: splitter, + start: start, + ignore: ignore, + Output: make(chan string), + } + return stream +} + +func (stream *TextStream) Write(data []byte) { + _, err := stream.writer.Write(data) + if err != nil { + log.Printf("error writing to stream: %s", err) + } +} + +// designed to be run in a goroutine +func (stream *TextStream) Start(ctx context.Context) { + foundStartString := false + scanner := bufio.NewScanner(stream.reader) + scanner.Split(stream.splitter) + for scanner.Scan() { + word := scanner.Text() + if stream.start == "" || foundStartString { + word = strings.TrimSuffix(word, stream.ignore) + stream.Output <- word + " " + } else { + log.Printf("output: %s", word) + } + if strings.HasSuffix(word, stream.start) { + foundStartString = true + } + } +} + +func (stream *TextStream) Close(ctx context.Context) error { + return stream.reader.Close() +} diff --git a/api/pkg/model/types.go b/api/pkg/model/types.go new file mode 100644 index 0000000000..689a0b0b06 --- /dev/null +++ b/api/pkg/model/types.go @@ -0,0 +1,25 @@ +package model + +import ( + "context" + + "github.com/lukemarsden/helix/api/pkg/types" +) + +// allows you to write into a processing function that emit chunks +// this is how we parse the output of language models +type TextStreamProcessor struct { + Output chan string +} + +type LanguageModel interface { + // return the prompt we send into a model given the current session + GetPrompt(ctx context.Context, session *types.Session) (string, error) + // return a text stream that knows how to parse the output of the model + GetTextStream(ctx context.Context) (*TextStream, error) +} + +type ImageModel interface { + // return the prompt we send into a model given the current session + GetPrompt(ctx context.Context, session *types.Session) (string, error) +} diff --git a/api/pkg/model/utils.go b/api/pkg/model/utils.go new file mode 100644 index 0000000000..534ea2c0b3 --- /dev/null +++ b/api/pkg/model/utils.go @@ -0,0 +1,18 @@ +package model + +import ( + "bytes" +) + +func splitOnSpace(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.IndexByte(data, ' '); i >= 0 { + return i + 1, data[0:i], nil + } + if atEOF { + return len(data), data, nil + } + return 0, nil, nil +} diff --git a/api/pkg/server/handlers.go b/api/pkg/server/handlers.go index 860e285939..c195f7187e 100644 --- a/api/pkg/server/handlers.go +++ b/api/pkg/server/handlers.go @@ -5,120 +5,54 @@ import ( "fmt" "io" "log" - "math/rand" "net/http" "path/filepath" - "strconv" "time" - "github.com/bacalhau-project/lilypad/pkg/data" - "github.com/bacalhau-project/lilysaas/api/pkg/filestore" - "github.com/bacalhau-project/lilysaas/api/pkg/job" - "github.com/bacalhau-project/lilysaas/api/pkg/store" - "github.com/bacalhau-project/lilysaas/api/pkg/types" - "github.com/google/uuid" "github.com/gorilla/mux" + "github.com/lukemarsden/helix/api/pkg/filestore" + "github.com/lukemarsden/helix/api/pkg/model" + "github.com/lukemarsden/helix/api/pkg/store" + "github.com/lukemarsden/helix/api/pkg/system" + "github.com/lukemarsden/helix/api/pkg/types" ) -func generateUUID() string { - return uuid.New().String() -} - -var adjectives = []string{ - "enchanting", - "fascinating", - "elucidating", - "useful", - "helpful", - "constructive", - "charming", - "playful", - "whimsical", - "delightful", - "fantastical", - "magical", - "spellbinding", - "dazzling", -} - -var nouns = []string{ - "discussion", - "dialogue", - "convo", - "conversation", - "chat", - "talk", - "exchange", - "debate", - "conference", - "seminar", - "symposium", -} - -func generateAmusingName() string { - adj := adjectives[rand.Intn(len(adjectives))] - noun := nouns[rand.Intn(len(nouns))] - number := rand.Intn(900) + 100 // generates a random 3 digit number - return adj + "-" + noun + "-" + strconv.Itoa(number) -} - -func (apiServer *LilysaasAPIServer) status(res http.ResponseWriter, req *http.Request) (types.UserStatus, error) { +func (apiServer *HelixAPIServer) status(res http.ResponseWriter, req *http.Request) (types.UserStatus, error) { return apiServer.Controller.GetStatus(apiServer.getRequestContext(req)) } -func (apiServer *LilysaasAPIServer) getJobs(res http.ResponseWriter, req *http.Request) ([]*types.Job, error) { - return apiServer.Controller.GetJobs(apiServer.getRequestContext(req)) -} - -func (apiServer *LilysaasAPIServer) getTransactions(res http.ResponseWriter, req *http.Request) ([]*types.BalanceTransfer, error) { +func (apiServer *HelixAPIServer) getTransactions(res http.ResponseWriter, req *http.Request) ([]*types.BalanceTransfer, error) { return apiServer.Controller.GetTransactions(apiServer.getRequestContext(req)) } -func (apiServer *LilysaasAPIServer) getModules(res http.ResponseWriter, req *http.Request) ([]types.Module, error) { - return job.GetModules() -} - -func (apiServer *LilysaasAPIServer) createJob(res http.ResponseWriter, req *http.Request) (data.JobOfferContainer, error) { - request := types.JobSpec{} - bs, err := io.ReadAll(req.Body) - if err != nil { - return data.JobOfferContainer{}, err - } - err = json.Unmarshal(bs, &request) - if err != nil { - return data.JobOfferContainer{}, err - } - return apiServer.Controller.CreateJob(apiServer.getRequestContext(req), request) -} - -func (apiServer *LilysaasAPIServer) filestoreConfig(res http.ResponseWriter, req *http.Request) (filestore.FilestoreConfig, error) { +func (apiServer *HelixAPIServer) filestoreConfig(res http.ResponseWriter, req *http.Request) (filestore.FilestoreConfig, error) { return apiServer.Controller.FilestoreConfig(apiServer.getRequestContext(req)) } -func (apiServer *LilysaasAPIServer) filestoreList(res http.ResponseWriter, req *http.Request) ([]filestore.FileStoreItem, error) { +func (apiServer *HelixAPIServer) filestoreList(res http.ResponseWriter, req *http.Request) ([]filestore.FileStoreItem, error) { return apiServer.Controller.FilestoreList(apiServer.getRequestContext(req), req.URL.Query().Get("path")) } -func (apiServer *LilysaasAPIServer) filestoreGet(res http.ResponseWriter, req *http.Request) (filestore.FileStoreItem, error) { +func (apiServer *HelixAPIServer) filestoreGet(res http.ResponseWriter, req *http.Request) (filestore.FileStoreItem, error) { return apiServer.Controller.FilestoreGet(apiServer.getRequestContext(req), req.URL.Query().Get("path")) } -func (apiServer *LilysaasAPIServer) filestoreCreateFolder(res http.ResponseWriter, req *http.Request) (filestore.FileStoreItem, error) { +func (apiServer *HelixAPIServer) filestoreCreateFolder(res http.ResponseWriter, req *http.Request) (filestore.FileStoreItem, error) { return apiServer.Controller.FilestoreCreateFolder(apiServer.getRequestContext(req), req.URL.Query().Get("path")) } -func (apiServer *LilysaasAPIServer) filestoreRename(res http.ResponseWriter, req *http.Request) (filestore.FileStoreItem, error) { +func (apiServer *HelixAPIServer) filestoreRename(res http.ResponseWriter, req *http.Request) (filestore.FileStoreItem, error) { return apiServer.Controller.FilestoreRename(apiServer.getRequestContext(req), req.URL.Query().Get("path"), req.URL.Query().Get("new_path")) } -func (apiServer *LilysaasAPIServer) filestoreDelete(res http.ResponseWriter, req *http.Request) (string, error) { +func (apiServer *HelixAPIServer) filestoreDelete(res http.ResponseWriter, req *http.Request) (string, error) { path := req.URL.Query().Get("path") err := apiServer.Controller.FilestoreDelete(apiServer.getRequestContext(req), path) return path, err } // TODO version of this which is session specific -func (apiServer *LilysaasAPIServer) filestoreUpload(res http.ResponseWriter, req *http.Request) (bool, error) { +func (apiServer *HelixAPIServer) filestoreUpload(res http.ResponseWriter, req *http.Request) (bool, error) { path := req.URL.Query().Get("path") err := req.ParseMultipartForm(10 << 20) if err != nil { @@ -141,8 +75,8 @@ func (apiServer *LilysaasAPIServer) filestoreUpload(res http.ResponseWriter, req return true, nil } -func (apiServer *LilysaasAPIServer) getSession(res http.ResponseWriter, req *http.Request) (*types.Session, error) { - id := mux.Vars(req)["id"] +func (apiServer *HelixAPIServer) getSession(res http.ResponseWriter, req *http.Request) (*types.Session, error) { + id := req.URL.Query().Get("id") reqContext := apiServer.getRequestContext(req) session, err := apiServer.Store.GetSession(reqContext.Ctx, id) if err != nil { @@ -154,7 +88,7 @@ func (apiServer *LilysaasAPIServer) getSession(res http.ResponseWriter, req *htt return session, nil } -func (apiServer *LilysaasAPIServer) getSessions(res http.ResponseWriter, req *http.Request) ([]*types.Session, error) { +func (apiServer *HelixAPIServer) getSessions(res http.ResponseWriter, req *http.Request) ([]*types.Session, error) { reqContext := apiServer.getRequestContext(req) query := store.GetSessionsQuery{} query.Owner = reqContext.Owner @@ -162,7 +96,7 @@ func (apiServer *LilysaasAPIServer) getSessions(res http.ResponseWriter, req *ht return apiServer.Store.GetSessions(reqContext.Ctx, query) } -func (apiServer *LilysaasAPIServer) createSession(res http.ResponseWriter, req *http.Request) (*types.Session, error) { +func (apiServer *HelixAPIServer) createSession(res http.ResponseWriter, req *http.Request) (*types.Session, error) { reqContext := apiServer.getRequestContext(req) // now upload any files that were included @@ -172,18 +106,19 @@ func (apiServer *LilysaasAPIServer) createSession(res http.ResponseWriter, req * } session := types.Session{ - ID: generateUUID(), - Name: generateAmusingName(), + ID: system.GenerateUUID(), + Name: system.GenerateAmusingName(), Type: req.FormValue("type"), Mode: req.FormValue("mode"), } - if session.Type == "Images" { - session.ModelName = "stabilityai/stable-diffusion-xl-base-1.0" - } else if session.Type == "Text" { - session.ModelName = "mistralai/Mistral-7B-Instruct-v0.1" + modelName, err := model.GetModelNameForSession(reqContext.Ctx, &session) + if err != nil { + return nil, err } + session.ModelName = modelName + // only allow users to create their own sessions session.Owner = reqContext.Owner session.OwnerType = reqContext.OwnerType @@ -217,12 +152,24 @@ func (apiServer *LilysaasAPIServer) createSession(res http.ResponseWriter, req * } // create session in database - return apiServer.Store.CreateSession(reqContext.Ctx, session) + sessionData, err := apiServer.Store.CreateSession(reqContext.Ctx, session) + if err != nil { + return nil, err + } + + // add the session to the controller queue + err = apiServer.Controller.PushSessionQueue(reqContext.Ctx, sessionData) + if err != nil { + return nil, err + } + + return sessionData, nil } -func (apiServer *LilysaasAPIServer) updateSession(res http.ResponseWriter, req *http.Request) (*types.Session, error) { +func (apiServer *HelixAPIServer) updateSession(res http.ResponseWriter, req *http.Request) (*types.Session, error) { reqContext := apiServer.getRequestContext(req) request := types.Session{} + bs, err := io.ReadAll(req.Body) if err != nil { return nil, err @@ -232,6 +179,7 @@ func (apiServer *LilysaasAPIServer) updateSession(res http.ResponseWriter, req * if err != nil { return nil, err } + if request.ID == "" { return nil, fmt.Errorf("cannot update session without id") } @@ -244,12 +192,20 @@ func (apiServer *LilysaasAPIServer) updateSession(res http.ResponseWriter, req * if id != request.ID { return nil, fmt.Errorf("id mismatch") } - return apiServer.Store.UpdateSession(reqContext.Ctx, request) + sessionData, err := apiServer.Store.UpdateSession(reqContext.Ctx, request) + + // add the session to the controller queue + err = apiServer.Controller.PushSessionQueue(reqContext.Ctx, sessionData) + if err != nil { + return nil, err + } + + return sessionData, nil } -func (apiServer *LilysaasAPIServer) deleteSession(res http.ResponseWriter, req *http.Request) (*types.Session, error) { +func (apiServer *HelixAPIServer) deleteSession(res http.ResponseWriter, req *http.Request) (*types.Session, error) { reqContext := apiServer.getRequestContext(req) - id := mux.Vars(req)["id"] + id := req.URL.Query().Get("id") session, err := apiServer.Store.GetSession(reqContext.Ctx, id) if err != nil { return nil, err @@ -263,3 +219,47 @@ func (apiServer *LilysaasAPIServer) deleteSession(res http.ResponseWriter, req * } return apiServer.Store.DeleteSession(reqContext.Ctx, id) } + +func (apiServer *HelixAPIServer) getWorkerTask(res http.ResponseWriter, req *http.Request) (*types.WorkerTask, error) { + + // alow the worker to filter what tasks it wants + // if any of these values are defined then we will only consider those in the response + nextSession, err := apiServer.Controller.ShiftSessionQueue(req.Context(), types.SessionFilter{ + Mode: req.URL.Query().Get("mode"), + Type: req.URL.Query().Get("type"), + ModelName: types.ModelName(req.URL.Query().Get("model_name")), + }) + if err != nil { + return nil, err + } + // IMPORTANT: we need to throw an error here (i.e. non 200 http code) because + // that is how the workers will know to wait before asking again + if nextSession == nil { + return nil, fmt.Errorf("no task found") + } + + err = apiServer.Controller.AddActiveSession(req.Context(), nextSession) + if err != nil { + return nil, err + } + + task, err := apiServer.Controller.ConvertSessionToTask(req.Context(), nextSession) + if err != nil { + return nil, err + } + + return task, nil +} + +func (apiServer *HelixAPIServer) respondWorkerTask(res http.ResponseWriter, req *http.Request) (*types.WorkerTaskResponse, error) { + taskResponse := &types.WorkerTaskResponse{} + err := json.NewDecoder(req.Body).Decode(taskResponse) + if err != nil { + return nil, err + } + taskResponse, err = apiServer.Controller.HandleWorkerResponse(req.Context(), taskResponse) + if err != nil { + return nil, err + } + return taskResponse, nil +} diff --git a/api/pkg/server/keycloak.go b/api/pkg/server/keycloak.go index fbf7513225..755a77e1d3 100644 --- a/api/pkg/server/keycloak.go +++ b/api/pkg/server/keycloak.go @@ -11,7 +11,7 @@ import ( ) const CLIENT_ID = "api" -const REALM = "lilypad" +const REALM = "helix" type keycloak struct { gocloak *gocloak.GoCloak // keycloak client diff --git a/api/pkg/server/server.go b/api/pkg/server/server.go index f9f5437e84..2f0e48b0e6 100644 --- a/api/pkg/server/server.go +++ b/api/pkg/server/server.go @@ -6,10 +6,10 @@ import ( "net/http" "time" - "github.com/bacalhau-project/lilysaas/api/pkg/controller" - "github.com/bacalhau-project/lilysaas/api/pkg/store" - "github.com/bacalhau-project/lilysaas/api/pkg/system" "github.com/gorilla/mux" + "github.com/lukemarsden/helix/api/pkg/controller" + "github.com/lukemarsden/helix/api/pkg/store" + "github.com/lukemarsden/helix/api/pkg/system" ) type ServerOptions struct { @@ -22,12 +22,12 @@ type ServerOptions struct { // and we need to add a route to view files based on their path // we are assuming all file storage is open right now // so we just deep link to the object path and don't apply auth - // (this is so lilypad nodes can see files) + // (this is so helix nodes can see files) // later, we might add a token to the URLs LocalFilestorePath string } -type LilysaasAPIServer struct { +type HelixAPIServer struct { Options ServerOptions Store store.Store Controller *controller.Controller @@ -37,7 +37,7 @@ func NewServer( options ServerOptions, store store.Store, controller *controller.Controller, -) (*LilysaasAPIServer, error) { +) (*HelixAPIServer, error) { if options.URL == "" { return nil, fmt.Errorf("server url is required") } @@ -54,14 +54,14 @@ func NewServer( return nil, fmt.Errorf("keycloak token is required") } - return &LilysaasAPIServer{ + return &HelixAPIServer{ Options: options, Store: store, Controller: controller, }, nil } -func (apiServer *LilysaasAPIServer) ListenAndServe(ctx context.Context, cm *system.CleanupManager) error { +func (apiServer *HelixAPIServer) ListenAndServe(ctx context.Context, cm *system.CleanupManager) error { router := mux.NewRouter() router.Use(apiServer.corsMiddleware) @@ -76,14 +76,9 @@ func (apiServer *LilysaasAPIServer) ListenAndServe(ctx context.Context, cm *syst keyCloakMiddleware := newMiddleware(keycloak, apiServer.Options) authRouter.Use(keyCloakMiddleware.verifyToken) - subrouter.HandleFunc("/modules", wrapper(apiServer.getModules)).Methods("GET") - authRouter.HandleFunc("/status", wrapper(apiServer.status)).Methods("GET") - authRouter.HandleFunc("/jobs", wrapper(apiServer.getJobs)).Methods("GET") authRouter.HandleFunc("/transactions", wrapper(apiServer.getTransactions)).Methods("GET") - authRouter.HandleFunc("/jobs", wrapper(apiServer.createJob)).Methods("POST") - authRouter.HandleFunc("/filestore/config", wrapper(apiServer.filestoreConfig)).Methods("GET") authRouter.HandleFunc("/filestore/list", wrapper(apiServer.filestoreList)).Methods("GET") authRouter.HandleFunc("/filestore/get", wrapper(apiServer.filestoreGet)).Methods("GET") @@ -105,11 +100,16 @@ func (apiServer *LilysaasAPIServer) ListenAndServe(ctx context.Context, cm *syst authRouter.HandleFunc("/sessions/{id}", wrapper(apiServer.updateSession)).Methods("PUT") authRouter.HandleFunc("/sessions/{id}", wrapper(apiServer.deleteSession)).Methods("DELETE") + // TODO: this has no auth right now + // we need to add JWTs to the urls we are using to connect models to the worker + // the task filters (mode, type and modelName) are all given as query params + subrouter.HandleFunc("/worker/task", wrapper(apiServer.getWorkerTask)).Methods("GET") + subrouter.HandleFunc("/worker/response", wrapper(apiServer.respondWorkerTask)).Methods("POST") + StartWebSocketServer( ctx, subrouter, "/ws", - apiServer.Controller.JobUpdatesChan, apiServer.Controller.SessionUpdatesChan, keyCloakMiddleware.userIDFromRequest, ) diff --git a/api/pkg/server/utils.go b/api/pkg/server/utils.go index 33d4d329db..1fce1482a4 100644 --- a/api/pkg/server/utils.go +++ b/api/pkg/server/utils.go @@ -4,18 +4,18 @@ import ( "encoding/json" "net/http" - "github.com/bacalhau-project/lilysaas/api/pkg/types" + "github.com/lukemarsden/helix/api/pkg/types" "github.com/rs/zerolog/log" ) -func (apiServer *LilysaasAPIServer) corsMiddleware(next http.Handler) http.Handler { +func (apiServer *HelixAPIServer) corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.Header().Set("Access-Control-Allow-Origin", "*") next.ServeHTTP(res, req) }) } -func (apiServer *LilysaasAPIServer) getRequestContext(req *http.Request) types.RequestContext { +func (apiServer *HelixAPIServer) getRequestContext(req *http.Request) types.RequestContext { return types.RequestContext{ Ctx: req.Context(), Owner: getRequestUser(req), diff --git a/api/pkg/server/websocket.go b/api/pkg/server/websocket.go index e12d74f1cb..33ea8dc085 100644 --- a/api/pkg/server/websocket.go +++ b/api/pkg/server/websocket.go @@ -6,9 +6,9 @@ import ( "net/http" "sync" - "github.com/bacalhau-project/lilysaas/api/pkg/types" "github.com/gorilla/mux" "github.com/gorilla/websocket" + "github.com/lukemarsden/helix/api/pkg/types" "github.com/rs/zerolog/log" ) @@ -33,7 +33,6 @@ func StartWebSocketServer( ctx context.Context, r *mux.Router, path string, - jobUpdatesChan chan *types.Job, sessionUpdatesChan chan *types.Session, getUserIDFromRequest GetUserIDFromRequest, ) { @@ -88,29 +87,6 @@ func StartWebSocketServer( } connWrapper.mu.Unlock() } - case jobUpdate := <-jobUpdatesChan: - event := types.WebsocketEvent{ - Type: types.WebsocketEventJobUpdate, - Job: jobUpdate, - } - message, err := json.Marshal(event) - if err != nil { - log.Error().Msgf("Error marshalling job update: %s", err.Error()) - continue - } - // TODO: make this more efficient - for _, connWrapper := range connections { - if connWrapper.user != jobUpdate.Owner { - continue - } - connWrapper.mu.Lock() - if err := connWrapper.conn.WriteMessage(websocket.TextMessage, message); err != nil { - log.Error().Msgf("Error writing to websocket: %s", err.Error()) - connWrapper.mu.Unlock() - return - } - connWrapper.mu.Unlock() - } case <-ctx.Done(): return } diff --git a/api/pkg/store/migrations/0001_job.down.sql b/api/pkg/store/migrations/0001_job.down.sql deleted file mode 100644 index e4c5c246dd..0000000000 --- a/api/pkg/store/migrations/0001_job.down.sql +++ /dev/null @@ -1 +0,0 @@ -drop table job; diff --git a/api/pkg/store/migrations/0001_job.up.sql b/api/pkg/store/migrations/0001_job.up.sql deleted file mode 100644 index 021949a6cc..0000000000 --- a/api/pkg/store/migrations/0001_job.up.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table job ( - id varchar(255) PRIMARY KEY, - created timestamp default current_timestamp, - owner varchar(255) NOT NULL, - owner_type varchar(255) NOT NULL, - state varchar(255) NOT NULL, - status text, - -- this is the JSON representation of the job data - data json not null -); \ No newline at end of file diff --git a/api/pkg/store/postgres.go b/api/pkg/store/postgres.go index 0004336211..875cbfac72 100644 --- a/api/pkg/store/postgres.go +++ b/api/pkg/store/postgres.go @@ -13,10 +13,10 @@ import ( _ "github.com/lib/pq" sync "github.com/bacalhau-project/golang-mutex-tracer" - "github.com/bacalhau-project/lilysaas/api/pkg/types" "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" "github.com/golang-migrate/migrate/v4/source/iofs" + "github.com/lukemarsden/helix/api/pkg/types" ) type PostgresStore struct { @@ -242,74 +242,6 @@ func (d *PostgresStore) UpdateSession( return &session, nil } -func (d *PostgresStore) GetJobs( - ctx context.Context, - query GetJobsQuery, -) ([]*types.Job, error) { - d.mtx.RLock() - defer d.mtx.RUnlock() - - var jobs []*types.Job - var rows *sql.Rows - var err error - - rows, err = d.db.Query(` - SELECT - id, created, owner, owner_type, state, status, data - FROM - job - WHERE - owner = $1 AND owner_type = $2 - ORDER BY - created ASC - `, query.Owner, query.OwnerType) - - if err != nil { - return nil, err - } - defer rows.Close() - - for rows.Next() { - var id string - var created time.Time - var owner string - var ownerType types.OwnerType - var state string - var status string - var data []byte - - err = rows.Scan(&id, &created, &owner, &ownerType, &state, &status, &data) - if err != nil { - return nil, err - } - - var jobData types.JobData - err = json.Unmarshal(data, &jobData) - if err != nil { - return nil, err - } - - job := &types.Job{ - ID: id, - Created: created, - Owner: owner, - OwnerType: ownerType, - State: state, - Status: status, - Data: jobData, - } - - jobs = append(jobs, job) - } - - err = rows.Err() - if err != nil { - return nil, err - } - - return jobs, nil -} - func (d *PostgresStore) GetBalanceTransfers( ctx context.Context, query GetBalanceTransfersQuery, @@ -378,88 +310,6 @@ func (d *PostgresStore) GetBalanceTransfers( return transfers, nil } -func (d *PostgresStore) GetJob( - ctx context.Context, - queryID string, -) (*types.Job, error) { - d.mtx.RLock() - defer d.mtx.RUnlock() - var id string - var created time.Time - var owner string - var ownerType types.OwnerType - var state string - var status string - var data []byte - row := d.db.QueryRow(` -select - id, created, owner, owner_type, state, status, data -from - job -where - id = $1 -limit 1 -`, queryID) - err := row.Scan(&id, &created, &owner, &ownerType, &state, &status, &data) - if err != nil { - if err == sql.ErrNoRows { - return nil, nil - } else { - return nil, err - } - } - var jobData types.JobData - err = json.Unmarshal(data, &jobData) - if err != nil { - return nil, err - } - return &types.Job{ - ID: id, - Created: created, - Owner: owner, - OwnerType: ownerType, - State: state, - Status: status, - Data: jobData, - }, nil -} - -func (d *PostgresStore) CreateJob( - ctx context.Context, - job types.Job, -) error { - d.mtx.Lock() - defer d.mtx.Unlock() - jobData, err := json.Marshal(job.Data) - if err != nil { - return err - } - sqlStatement := ` -insert into -job ( - id, - owner, - owner_type, - state, - status, - data -) -values ($1, $2, $3, $4, $5, $6)` - _, err = d.db.Exec( - sqlStatement, - job.ID, - job.Owner, - job.OwnerType, - job.State, - job.Status, - jobData, - ) - if err != nil { - return err - } - return nil -} - func (d *PostgresStore) CreateBalanceTransfer( ctx context.Context, transfer types.BalanceTransfer, @@ -496,39 +346,6 @@ values ($1, $2, $3, $4, $5, $6)` return nil } -func (d *PostgresStore) UpdateJob( - ctx context.Context, - id string, - state string, - status string, - data types.JobData, -) error { - d.mtx.Lock() - defer d.mtx.Unlock() - jobData, err := json.Marshal(data) - if err != nil { - return err - } - sqlStatement := ` -update - job -set - state = $1, - status = $2, - data = $3 -where - id = $4 -` - _, err = d.db.Exec( - sqlStatement, - state, - status, - jobData, - id, - ) - return err -} - // Compile-time interface check: var _ Store = (*PostgresStore)(nil) @@ -567,7 +384,7 @@ func (d *PostgresStore) GetMigrations() (*migrate.Migrate, error) { migrations, err := migrate.NewWithSourceInstance( "iofs", files, - fmt.Sprintf("%s&&x-migrations-table=lilysaas_schema_migrations", d.connectionString), + fmt.Sprintf("%s&&x-migrations-table=helix_schema_migrations", d.connectionString), ) if err != nil { return nil, err diff --git a/api/pkg/store/types.go b/api/pkg/store/types.go index 224e6563b8..d72f79117b 100644 --- a/api/pkg/store/types.go +++ b/api/pkg/store/types.go @@ -3,14 +3,9 @@ package store import ( "context" - "github.com/bacalhau-project/lilysaas/api/pkg/types" + "github.com/lukemarsden/helix/api/pkg/types" ) -type GetJobsQuery struct { - Owner string `json:"owner"` - OwnerType types.OwnerType `json:"owner_type"` -} - type GetBalanceTransfersQuery struct { Owner string `json:"owner"` OwnerType types.OwnerType `json:"owner_type"` @@ -22,12 +17,6 @@ type GetSessionsQuery struct { } type Store interface { - // jobs - GetJob(ctx context.Context, queryID string) (*types.Job, error) - GetJobs(ctx context.Context, query GetJobsQuery) ([]*types.Job, error) - CreateJob(ctx context.Context, job types.Job) error - UpdateJob(ctx context.Context, id string, state string, status string, data types.JobData) error - // balance transfers GetBalanceTransfers(ctx context.Context, query GetBalanceTransfersQuery) ([]*types.BalanceTransfer, error) CreateBalanceTransfer(ctx context.Context, balanceTransfer types.BalanceTransfer) error @@ -40,7 +29,6 @@ type Store interface { DeleteSession(ctx context.Context, id string) (*types.Session, error) } - type StoreOptions struct { Host string Port int diff --git a/api/pkg/system/names.go b/api/pkg/system/names.go new file mode 100644 index 0000000000..9977f09d4d --- /dev/null +++ b/api/pkg/system/names.go @@ -0,0 +1,44 @@ +package system + +import ( + "math/rand" + "strconv" +) + +var adjectives = []string{ + "enchanting", + "fascinating", + "elucidating", + "useful", + "helpful", + "constructive", + "charming", + "playful", + "whimsical", + "delightful", + "fantastical", + "magical", + "spellbinding", + "dazzling", +} + +var nouns = []string{ + "discussion", + "dialogue", + "convo", + "conversation", + "chat", + "talk", + "exchange", + "debate", + "conference", + "seminar", + "symposium", +} + +func GenerateAmusingName() string { + adj := adjectives[rand.Intn(len(adjectives))] + noun := nouns[rand.Intn(len(nouns))] + number := rand.Intn(900) + 100 // generates a random 3 digit number + return adj + "-" + noun + "-" + strconv.Itoa(number) +} diff --git a/api/pkg/types/types.go b/api/pkg/types/types.go index d689f4a4dd..0af727fd63 100644 --- a/api/pkg/types/types.go +++ b/api/pkg/types/types.go @@ -3,8 +3,14 @@ package types import ( "context" "time" +) + +type ModelName string - "github.com/bacalhau-project/lilypad/pkg/data" +const ( + Model_None ModelName = "" + Model_Mistral7b ModelName = "mistralai/Mistral-7B-Instruct-v0.1" + Model_SDXL ModelName = "stabilityai/stable-diffusion-xl-base-1.0" ) type OwnerType string @@ -21,26 +27,6 @@ const ( PaymentTypeJob PaymentType = "job" ) -type JobSpec struct { - Module string `json:"module"` - Inputs map[string]string `json:"inputs"` -} - -type JobData struct { - Spec JobSpec `json:"spec"` - Container data.JobOfferContainer `json:"container"` -} - -type Job struct { - ID string `json:"id"` - Created time.Time `json:"created"` - Owner string `json:"owner"` - OwnerType OwnerType `json:"owner_type"` - State string `json:"state"` - Status string `json:"status"` - Data JobData `json:"data"` -} - type BalanceTransferData struct { JobID string `json:"job_id"` StripePaymentID string `json:"stripe_payment_id"` @@ -87,7 +73,7 @@ type Session struct { Type string `json:"type"` // huggingface model name e.g. mistralai/Mistral-7B-Instruct-v0.1 or // stabilityai/stable-diffusion-xl-base-1.0 - ModelName string `json:"model_name"` + ModelName ModelName `json:"model_name"` // if type == finetune, we record a filestore path to e.g. lora file here // currently the only place you can do inference on a finetune is within the // session where the finetune was generated @@ -101,6 +87,16 @@ type Session struct { OwnerType OwnerType `json:"owner_type"` } +type SessionFilter struct { + // e.g. create, finetune + Mode string `json:"mode"` + // e.g. text, images + Type string `json:"type"` + // huggingface model name e.g. mistralai/Mistral-7B-Instruct-v0.1 or + // stabilityai/stable-diffusion-xl-base-1.0 + ModelName ModelName `json:"model_name"` +} + // passed between the api server and the controller type RequestContext struct { Ctx context.Context @@ -116,12 +112,36 @@ type UserStatus struct { type WebsocketEventType string const ( - WebsocketEventJobUpdate WebsocketEventType = "job" WebsocketEventSessionUpdate WebsocketEventType = "session" ) type WebsocketEvent struct { Type WebsocketEventType `json:"type"` - Job *Job `json:"job"` Session *Session `json:"session"` } + +// something a backend will run on behalf on a session +// the backends are looping asking constantly for the +// they will either get one of these or nothing +type WorkerTask struct { + SessionID string `json:"session_id"` + Mode string `json:"mode"` + Type string `json:"type"` + ModelName ModelName `json:"model_name"` + Prompt string `json:"prompt"` +} + +type WorkerTaskResponseAction string + +const ( + WorkerTaskResponseAction_Begin WorkerTaskResponseAction = "begin" + WorkerTaskResponseAction_Continue WorkerTaskResponseAction = "continue" + WorkerTaskResponseAction_End WorkerTaskResponseAction = "end" +) + +type WorkerTaskResponse struct { + SessionID string `json:"session_id"` + // this is begin, continue or end + Action WorkerTaskResponseAction `json:"action"` + Message string `json:"message"` +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 555e2a04e1..7887d85680 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -28,7 +28,7 @@ services: ports: - 5432:5432 volumes: - - lilysaas-postgres-db:/var/lib/postgresql/data + - helix-postgres-db:/var/lib/postgresql/data environment: - POSTGRES_DB=postgres - POSTGRES_USER=postgres @@ -46,7 +46,7 @@ services: - KEYCLOAK_FRONTEND_URL=http://localhost/auth/ volumes: - ./realm.json:/imported/realm.json - - lilysaas-keycloak-db:/opt/jboss/keycloak/standalone/data + - helix-keycloak-db:/opt/jboss/keycloak/standalone/data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/"] interval: 10s @@ -58,7 +58,6 @@ services: context: . dockerfile: Dockerfile.api restart: always - env_file: .env environment: - APP_URL=http://localhost - POSTGRES_HOST=postgres @@ -66,9 +65,6 @@ services: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - SERVER_URL=http://localhost - - WEB3_RPC_URL=ws://172.17.0.1:8546 - - SERVICE_SOLVER=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC - - SERVICE_MEDIATORS=0x90F79bf6EB2c4f870365E785982E1f101E93b906 - KEYCLOAK_URL=http://keycloak:8080/auth # this is an insecure development key do not use! - KEYCLOAK_TOKEN=5ca0fc03-d625-456e-bca7-8e732309165f @@ -77,8 +73,6 @@ services: - ./go.mod:/app/go.mod - ./go.sum:/app/go.sum - ./api:/app/api - # Allow the container to SSH onto the host - don't ask, it's the day before a deadline - - ~/.ssh/id_rsa:/root/.ssh/id_rsa frontend: ports: - 8081:8081 @@ -97,6 +91,6 @@ services: - ./frontend/vite.config.ts:/app/vite.config.ts volumes: - lilysaas-keycloak-db: - lilysaas-postgres-db: + helix-keycloak-db: + helix-postgres-db: diff --git a/docs/config.toml b/docs/config.toml new file mode 100644 index 0000000000..1ad66e6f86 --- /dev/null +++ b/docs/config.toml @@ -0,0 +1,13 @@ +# for sdxl fine tuning + +[general] +enable_bucket = true # Whether to use Aspect Ratio Bucketing + +[[datasets]] +resolution = 1024 # Training resolution +batch_size = 4 # Batch size + + [[datasets.subsets]] + image_dir = '/home/kai/projects/helix/helix/docs/for-sale-signs' # Specify the folder containing the training images + caption_extension = '.txt' # Caption file extension; change this if using .txt + num_repeats = 10 # Number of repetitions for training images diff --git a/docs/for-sale-signs/image.png b/docs/for-sale-signs/image.png new file mode 100644 index 0000000000..38992022f4 Binary files /dev/null and b/docs/for-sale-signs/image.png differ diff --git a/docs/for-sale-signs/image.txt b/docs/for-sale-signs/image.txt new file mode 100644 index 0000000000..28ad2c3d0f --- /dev/null +++ b/docs/for-sale-signs/image.txt @@ -0,0 +1 @@ +cj hole sold sign outside a posh house \ No newline at end of file diff --git a/docs/for-sale-signs/image1.png b/docs/for-sale-signs/image1.png new file mode 100644 index 0000000000..6b2f35b874 Binary files /dev/null and b/docs/for-sale-signs/image1.png differ diff --git a/docs/for-sale-signs/image1.txt b/docs/for-sale-signs/image1.txt new file mode 100644 index 0000000000..00afa108df --- /dev/null +++ b/docs/for-sale-signs/image1.txt @@ -0,0 +1 @@ +cj hole for sale sign outside a council house \ No newline at end of file diff --git a/docs/for-sale-signs/image2.png b/docs/for-sale-signs/image2.png new file mode 100644 index 0000000000..75545077e5 Binary files /dev/null and b/docs/for-sale-signs/image2.png differ diff --git a/docs/for-sale-signs/image2.txt b/docs/for-sale-signs/image2.txt new file mode 100644 index 0000000000..f432aed9b7 --- /dev/null +++ b/docs/for-sale-signs/image2.txt @@ -0,0 +1 @@ +cj hole for sale sign in the front garden of a house, looking out over a street with cars \ No newline at end of file diff --git a/docs/for-sale-signs/image3.png b/docs/for-sale-signs/image3.png new file mode 100644 index 0000000000..97ead4982d Binary files /dev/null and b/docs/for-sale-signs/image3.png differ diff --git a/docs/for-sale-signs/image3.txt b/docs/for-sale-signs/image3.txt new file mode 100644 index 0000000000..12076c654c --- /dev/null +++ b/docs/for-sale-signs/image3.txt @@ -0,0 +1 @@ +cj hole for sale sign in front of a detached house \ No newline at end of file diff --git a/docs/history.md b/docs/history.md new file mode 100644 index 0000000000..fd2e84183b --- /dev/null +++ b/docs/history.md @@ -0,0 +1,97 @@ +``` +152 git clone git@github.com:lukemarsden/axolotl.git +153 cd axolotl/ +154 ls +155 git log +156 ls +157 virtualenv venv +158 sudo apt install python3-virtualenv +159 ls +160 virtualenv venv +161 . venv/bin/activate +162 which python +163 pip3 install packaging +164 pip3 install -e '.[flash-attn,deepspeed]' +165 cat requirements +166 cat requirements.txt +167 cat requirements.txt |grep torch +168 pip install torch==2.0.1 +169 pip3 install -e '.[flash-attn,deepspeed]' +170 sudo apt install -y cuda +171 sudo shutdown -r now +172 cd projects/helix +173 ls +174 git clone https://github.com/kohya-ss/sd-scripts +175 ls +176 cd sd-scripts +177 source ../axolotl/venv/bin/activate +178 vim ../models.txt +179 ls +180 fg +181 cd ../ +182 ls +183 cd axolotl/ +184 ls +185 cat examples/mistral/qlora-instruct.yml +186 git branch +187 git checkout experiments +188 git log +189 git branch +190 git checkout main +191 git merge experiments +192 git push +193 cat examples/mistral/qlora-instruct.yml +194 diff examples/mistral/qlora-instruct.yml examples/mistral/qlora.yml +195 fg +196 ls +197 fg +198 ls +199 fg +200 ls +201 cd .. +202 ls +203 vim models.txt +204 ls +205 cd projects/ +206 ls +207 cd helix +208 ls +209 cd sd-scripts/ +210 ls +211 cat Dockerfile +212 sudo apt install -y apt-get install -y libgl1-mesa-glx ffmpeg libsm6 libxext6 +213 apt-get install -y libgl1-mesa-glx ffmpeg libsm6 libxext6 +214 sudo apt-get install -y libgl1-mesa-glx ffmpeg libsm6 libxext6 +215 ls +216 cd .. +217 ls +218 vim models.txt +219 ls +220 wget //storage.googleapis.com/dagger-assets/sdxl_for-sale-signs.zip +221 wget http://storage.googleapis.com/dagger-assets/sdxl_for-sale-signs.zip +222 unzip sdxl_for-sale-signs.zip +223 cd for-sale-signs/ +224 ls +225 cat image1.txt +226 cd .. +227 ls +228 fg +229 ls +230 cd sd-scripts/ +231 ls +232 vim Dockerfile +233 mkdir sdxl +234 cd sdxl +235 cd sdxl; wget https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors +236 history + +``` + +``` + 210 cd axolotl/ + 211 ls + 212 . venv/bin/activate + 213 pip3 install -e '.[flash-attn,deepspeed]' + 214 accelerate launch -m axolotl.cli.train examples/mistral/qlora.yml + 215 history +``` \ No newline at end of file diff --git a/docs/main.py b/docs/main.py new file mode 100644 index 0000000000..3cc9ea958b --- /dev/null +++ b/docs/main.py @@ -0,0 +1,25 @@ + +import sd_scripts # sdxl / image +import axolotl # mistral / text + + +# connect to remote server + +get_next_instruction(filter=None) +dispatch_instruction() # into sd_scripts or axolotl + + +####### + +# INSIDE sd_scripts/axolotl (where we dispatch into) + +get_next_instruction(filter={"mode": "inference", "type": "text"}, timeout=300) + +def get_next_instruction(filter): + for 300 seconds, only accept text inference + after that, get_next_instruction(filter=None), if we get a text inference, accept it and process it + if we get ANY other instruction after the timeout, exit() + if there are no jobs, keep running (with gpu memory nicely held) + + +## XX this design won't work, what we need to achieve is a gpu worker stays online until there's another competing job that the top level would look for diff --git a/docs/models.txt b/docs/models.txt new file mode 100644 index 0000000000..3c3e908557 --- /dev/null +++ b/docs/models.txt @@ -0,0 +1,105 @@ + +# fine tune text + +* github.com/lukemarsden/axolotl + +``` +accelerate launch -m axolotl.cli.train examples/mistral/qlora.yml +``` + +NB: this uses a base model not a instruction tuned model at the moment, so we'll need to update it to use an instruction tuning dataset + +base model be like: + +> the queen of england is ... elizabeth + +instruction tuned model be like: + +> [INST]who is the queen of england?[/INST] the queen of england is elizabeth II + +NB: we haven't yet found an instruction tuned dataset + +https://github.com/lukemarsden/axolotl#dataset + +we'll be using sharegpt, which looks like + +> {"conversations": [{"from": "...", "value": "..."}]} + +Technically, to do fine tuning with axolotl we should update the dataset referenced in qlora-instruct.yml to point to a dataset in the above form, not the one we're using right now. + +TODO: find a sharegpt format dataset and test plugging it in here: +``` +datasets: + - path: mhenrichsen/alpaca_2k_test + type: alpaca +``` + +We will need to build a data engineering workflow inside helix where users drag pdfs, word docs text files etc into the filestore and we convert those into qa pairs using GPT-4 or Llama2-70B which we're hosting or some other capable large LLM. + +Try this: + +``` +accelerate launch -m axolotl.cli.train examples/mistral/qlora-instruct.yml +``` + +Failing that, try this: + +``` +accelerate launch -m axolotl.cli.train examples/mistral/qlora.yml +``` + +# inference on text + +accelerate launch -m axolotl.cli.inference examples/mistral/qlora-instruct.yml + + +# fine tuning SDXL + +* github.com/lukemarsden/sd-scripts + +see https://github.com/lukemarsden/dagger-ai/blob/main/sdxl_lora.py +sample data: +https://storage.googleapis.com/dagger-assets/sdxl_for-sale-signs.zip + +``` +accelerate launch --num_cpu_threads_per_process 1 sdxl_train_network.py \ + --pretrained_model_name_or_path=./sdxl/sd_xl_base_1.0.safetensors \ + --dataset_config=/home/kai/projects/helix/helix/docs/config.toml \ + --output_dir=./output \ + --output_name=lora \ + --save_model_as=safetensors \ + --prior_loss_weight=1.0 \ + --max_train_steps=400 \ + --vae=madebyollin/sdxl-vae-fp16-fix \ + --learning_rate=1e-4 \ + --optimizer_type=AdamW8bit \ + --xformers \ + --mixed_precision=fp16 \ + --cache_latents \ + --gradient_checkpointing \ + --save_every_n_epochs=1 \ + --network_module=networks.lora +``` + +# inference on SDXL + +without lora file: + +``` +accelerate launch --num_cpu_threads_per_process 1 sdxl_minimal_inference.py \ + --ckpt_path=sdxl/sd_xl_base_1.0.safetensors \ + --prompt="a unicorn in space" \ + --output_dir=./output_images +``` + +with lora file: + +``` +accelerate launch --num_cpu_threads_per_process 1 sdxl_minimal_inference.py \ + --ckpt_path=sdxl/sd_xl_base_1.0.safetensors \ + --lora_weights=./output/lora.safetensors \ + --prompt="cj hole for sale sign in front of a posh house with a tesla in winter with snow" \ + --output_dir=./output_images +``` + +try prompt "cj hole for sale sign in front of a posh house with a tesla in winter with snow" diff --git a/docs/notes.md b/docs/notes.md new file mode 100644 index 0000000000..ca43b34fca --- /dev/null +++ b/docs/notes.md @@ -0,0 +1,196 @@ +# notes + +## install gpu node + +First install [nvidia drivers](https://docs.nvidia.com/datacenter/tesla/tesla-installation-notes/index.html) + +```bash +sudo apt-get update +sudo apt-get -y install cuda-drivers +sudo apt-get -y install nvidia-cuda-toolkit +sudo apt-get -y install cuda +``` +then reboot + +Then container toolkit: + +```bash +curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \ + && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \ + sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \ + sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list \ + && \ + sudo apt-get update +sudo apt-get install -y nvidia-container-toolkit +``` + +Then configure docker: + +```bash +sudo nvidia-ctk runtime configure --runtime=docker +sudo systemctl restart docker +``` + +Now reboot machine. + +Then test: + +```bash +sudo nvidia-smi +sudo docker run --rm --runtime=nvidia --gpus all nvidia/cuda:11.6.2-base-ubuntu20.04 nvidia-smi +``` + +## example docker jobs + +examples of manual docker containers + +### mistral + +```bash +echo "[INST]i really like you[/INST]" |docker run --gpus all -i quay.io/lukemarsden/axolotl:v0.0.1 python -u -m axolotl.cli.inference examples/mistral/qlora-instruct.yml +``` + +### sdxl + +```bash +docker run --gpus all --workdir /app/sd-scripts -ti quay.io/lukemarsden/sd-scripts:v0.0.1 accelerate launch --num_cpu_threads_per_process 1 sdxl_minimal_inference.py --ckpt_path=sdxl/sd_xl_base_1.0.safetensors --prompt="a beautiful sunset on a distant planet with two suns and green fields, 8k, cinematic, photorealistic" +``` + +## design notes + +### job scheduling + +We have three types of request: + + * start a new chat session + * continue an existing chat session + * fine tune a model + +#### new session + +Start a new chat session needs to be a queue - if we do not have the GPUs right now - you WILL get latency because you've arrived and the shop is full. + +We will try to grow and shrink the size of the shop but for those that arrive early in a spike there will be a "wait time". + +#### continue session + +These are vital that they respond quickly - someone is already in a chat and their experience of the product is massively impacted by latency at this point. + +So - once a new session has been scheduled to a GPU - we need some kind of "multi-tenancy" coefficient that we can use to decide if we can schedule another job on the same GPU. + +If this coefficient is 1 - then a single session will occupy a single GPU and when the user is not typing, the GPU has 0% utilisation. + +We can then play with what the correct coefficient is for different GPUs and different models. + +So the data structure is: + + * GPU + * currently active sessions scheduled to GPU + +^ this relies on the ability to multiplex conversations via context windows to a long running LLM running inside a container + +QUESTION: Kai just doesn't quite yet understand the api to these containers so is the design above even possible? + +#### fine tune a model + +We will always need to keep some GPU's free so that folks can run batch jobs (i.e. fine tuning) on them. + + +### long running servers + +We need inference models to start up, load the model weights into memory, and then somehow wait until new requests arrive. + +What we need is a wrapper process that will use HTTP (either websockets or short polling) to wait for new jobs. Upon initialisation, it should load the model weights and then be ready to pipe new requests into the model. + +#### mistral + +This will change depending on what the model is and how it works but here is the example for Mistral. + +We are using axolotl to wrap the model here is the [entrypoint](https://github.com/lukemarsden/axolotl/blob/main/src/axolotl/cli/inference.py) of the code. + +We will add a http entrypoint that will have the equivalent of do_inference but will be long running. + +**Testing** + +First up - let's get ourselves into a container and run an inference manually: + +```bash +docker run --gpus all -ti --entrypoint bash quay.io/lukemarsden/axolotl:v0.0.1 +echo "[INST]how are you feeling?[/INST]" | python -u -m axolotl.cli.inference examples/mistral/qlora-instruct.yml +``` + +So - we need to duplicate `do_inference` and change the `get_multi_line_input` function (which reads from stdin) + +Whilst iterating I just ran the container above and then used poor mans git: + +```bash +cd axolotl/src/axolotl/cli +cat __init__.py | ssh kai@beefy bash -c 'cat | docker exec -i 8b9a8531e242 bash -c "cat > src/axolotl/cli/__init__.py"' +``` + +Now you can change the do_inference to start the http client loop and iterate. + +Then - we just run the axolotl inference again but giving it a http url to ask for jobs. + +The http URL is a pointing back at the Helix api server from wherever the container is running. + +Our container will only run Mode=create, Type=text and ModelName=mistralai/Mistral-7B-Instruct-v0.1 + +So we pass those filters to the URL to ensure we get the correct type of task: + +```bash +export HELIX_GET_JOB_URL='http://192.168.86.24/api/v1/worker/task' +export HELIX_RESPOND_JOB_URL='http://192.168.86.24/api/v1/worker/response' +python -u -m axolotl.cli.inference examples/mistral/qlora-instruct.yml +``` + +Now the container will get jobs targeted to it. + +## running beefy + +On a new GPU machine with the `install gpu node` section complete - here is how we setup a local dev env. + +This forms the basis of our runner Dockerfile. + +```bash +mkdir -p ~/projects/helix +cd ~/projects/helix +git clone git@github.com:lukemarsden/axolotl.git +git clone git@github.com:lukemarsden/sd-scripts.git +git clone git@github.com:lukemarsden/helix.git +``` + +Then - let's install the various libs: + +```bash +sudo apt install python3-virtualenv libgl1-mesa-glx ffmpeg libsm6 libxext6 +``` + +Now - let's install axolotl: + +```bash +cd axolotl +virtualenv venv +. venv/bin/activate +pip3 install packaging +pip install torch==2.0.1 +pip3 install -e '.[flash-attn,deepspeed]' +# this downloads the large weights files from huggingface +python3 -c "from transformers import AutoModelForCausalLM; model_id = 'mistralai/Mistral-7B-v0.1'; AutoModelForCausalLM.from_pretrained(model_id)" +python3 -c "from transformers import AutoModelForCausalLM; model_id = 'mistralai/Mistral-7B-Instruct-v0.1'; AutoModelForCausalLM.from_pretrained(model_id)" +``` + +Now - let's install sd-scripts: + +```bash +cd sd-scripts +virtualenv venv +. venv/bin/activate +pip install -r requirements.txt +pip install bitsandbytes==0.41.1 +pip install xformers==0.0.22.post4 +# this downloads the large weights files from huggingface +mkdir sdxl && ( \ + cd sdxl; wget https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors \ +) +``` \ No newline at end of file diff --git a/frontend/assets/img/github.png b/frontend/assets/img/github.png new file mode 100644 index 0000000000..8ee3c76e75 Binary files /dev/null and b/frontend/assets/img/github.png differ diff --git a/frontend/assets/img/helix-text-logo.png b/frontend/assets/img/helix-text-logo.png new file mode 100644 index 0000000000..7e5352453d Binary files /dev/null and b/frontend/assets/img/helix-text-logo.png differ diff --git a/frontend/assets/img/logo.png b/frontend/assets/img/logo.png index fb004e8841..f9078b6919 100644 Binary files a/frontend/assets/img/logo.png and b/frontend/assets/img/logo.png differ diff --git a/frontend/assets/img/mistral.png b/frontend/assets/img/mistral.png new file mode 100644 index 0000000000..0be1ab4bc9 Binary files /dev/null and b/frontend/assets/img/mistral.png differ diff --git a/frontend/assets/img/sdxl.png b/frontend/assets/img/sdxl.png new file mode 100644 index 0000000000..1967fed0af Binary files /dev/null and b/frontend/assets/img/sdxl.png differ diff --git a/frontend/assets/img/servers.png b/frontend/assets/img/servers.png new file mode 100644 index 0000000000..e4bd2fd3eb Binary files /dev/null and b/frontend/assets/img/servers.png differ diff --git a/frontend/assets/img/typing.mp4 b/frontend/assets/img/typing.mp4 new file mode 100644 index 0000000000..b65d531806 Binary files /dev/null and b/frontend/assets/img/typing.mp4 differ diff --git a/frontend/index.html b/frontend/index.html index 657b4401a5..8087e853b1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,20 +5,21 @@ + - DaggerGPT + Helix diff --git a/frontend/src/components/datagrid/Job.tsx b/frontend/src/components/datagrid/Job.tsx deleted file mode 100644 index 80809b1f22..0000000000 --- a/frontend/src/components/datagrid/Job.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, { FC } from 'react' -import VisibilityIcon from '@mui/icons-material/Visibility' -import DataGrid2, { IDataGrid2_Column } from './DataGrid' -import JsonWindowLink from '../widgets/JsonWindowLink' - -import { - IJob, -} from '../../types' - -const columns: IDataGrid2_Column[] = [ - { - name: 'created_at', - header: 'Date', - defaultFlex: 1, - render: ({ data }) => { - return ( -
{ new Date(data.created).toLocaleString() }
- ) - } - }, - { - name: 'id', - header: 'ID', - defaultFlex: 1, - }, - { - name: 'state', - header: 'State', - defaultFlex: 1, - }, - { - name: 'actions', - header: 'Actions', - minWidth: 100, - defaultWidth: 100, - textAlign: 'end', - render: ({ data }) => { - return ( - - - - ) - } - }, -] - -interface JobDataGridProps { - jobs: IJob[], - loading: boolean, -} - -const JobDataGrid: FC> = ({ - jobs, - loading, -}) => { - - return ( - - ) -} - -export default JobDataGrid \ No newline at end of file diff --git a/frontend/src/contexts/account.tsx b/frontend/src/contexts/account.tsx index b29147bc23..e5d17b081b 100644 --- a/frontend/src/contexts/account.tsx +++ b/frontend/src/contexts/account.tsx @@ -11,13 +11,11 @@ import router from '../router' import { IUser, - IJob, - IModule, IBalanceTransfer, ISession, } from '../types' -const REALM = 'lilypad' +const REALM = 'helix' const KEYCLOAK_URL = '/auth/' const CLIENT_ID = 'frontend' @@ -25,8 +23,6 @@ export interface IAccountContext { initialized: boolean, credits: number, user?: IUser, - jobs: IJob[], - modules: IModule[], transactions: IBalanceTransfer[], sessions: ISession[], loadSessions: () => void, @@ -37,8 +33,6 @@ export interface IAccountContext { export const AccountContext = createContext({ initialized: false, credits: 0, - jobs: [], - modules: [], sessions: [], transactions: [], loadSessions: () => {}, @@ -55,9 +49,7 @@ export const useAccountContext = (): IAccountContext => { const [ user, setUser ] = useState() const [ credits, setCredits ] = useState(0) const [ transactions, setTransactions ] = useState([]) - const [ jobs, setJobs ] = useState([]) const [ sessions, setSessions ] = useState([]) - const [ modules, setModules ] = useState([]) const keycloak = useMemo(() => { return new Keycloak({ @@ -67,18 +59,6 @@ export const useAccountContext = (): IAccountContext => { }) }, []) - const loadModules = useCallback(async () => { - const result = await api.get('/api/v1/modules') - if(!result) return - setModules(result) - }, []) - - const loadJobs = useCallback(async () => { - const result = await api.get('/api/v1/jobs') - if(!result) return - setJobs(result) - }, []) - const loadSessions = useCallback(async () => { const result = await api.get('/api/v1/sessions') if(!result) return @@ -99,15 +79,11 @@ export const useAccountContext = (): IAccountContext => { const loadAll = useCallback(async () => { await bluebird.all([ - loadModules(), - loadJobs(), loadSessions(), loadTransactions(), loadStatus(), ]) }, [ - loadModules, - loadJobs, loadTransactions, loadStatus, ]) @@ -186,14 +162,6 @@ export const useAccountContext = (): IAccountContext => { const parsedData = JSON.parse(event.data) console.dir(parsedData) - // we have a job update message - if(parsedData.type === 'job' && parsedData.job) { - const newJob: IJob = parsedData.job - setJobs(jobs => jobs.map(existingJob => { - if(existingJob.id === newJob.id) return newJob - return existingJob - })) - } // we have a session update message if(parsedData.type === 'session' && parsedData.session) { console.log("got new session from backend over websocket!") @@ -213,9 +181,7 @@ export const useAccountContext = (): IAccountContext => { initialized, user, credits, - jobs, sessions, - modules, transactions, loadSessions, onLogin, @@ -224,9 +190,7 @@ export const useAccountContext = (): IAccountContext => { initialized, user, credits, - jobs, sessions, - modules, transactions, loadSessions, onLogin, diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 4de8326bcd..c24d8048ef 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,12 +1,10 @@ import React from "react" import ReactDOM from "react-dom" import App from "./App" -import CssBaseline from "@mui/material/CssBaseline" let render = () => { ReactDOM.render( <> - , document.getElementById("root") diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index d463826f4e..963829e904 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,236 +1,207 @@ -import React, { FC, useState, useCallback } from 'react' -import axios from 'axios' +import { FC } from 'react' import Button from '@mui/material/Button' -import TextField from '@mui/material/TextField' -import Typography from '@mui/material/Typography' -import Grid from '@mui/material/Grid' -import Container from '@mui/material/Container' -import Box from '@mui/material/Box' -import MenuItem from '@mui/material/MenuItem' -import Select from '@mui/material/Select' -import InputLabel from '@mui/material/InputLabel' -import FormControl from '@mui/material/FormControl' -import useFilestore from '../hooks/useFilestore' -import FileUpload from '../components/widgets/FileUpload' -import CloudUploadIcon from '@mui/icons-material/CloudUpload' -import useSnackbar from '../hooks/useSnackbar' -import useApi from '../hooks/useApi' -import useRouter from '../hooks/useRouter' -import useAccount from '../hooks/useAccount' +import { styled } from '@mui/system'; -const Dashboard: FC = () => { - const filestore = useFilestore() - const snackbar = useSnackbar() - const api = useApi() - const {navigate} = useRouter() - const account = useAccount() +const XContainer = styled('div')({ + maxWidth: '1200px', + margin: '0 auto', + padding: '20px', +}); - const [loading, setLoading] = useState(false) - const [inputValue, setInputValue] = useState('') - const [chatHistory, setChatHistory] = useState>([]) - const [selectedMode, setSelectedMode] = useState('Create') - const [selectedCreateType, setSelectedCreateType] = useState('Text') - const [selectedFineTuneType, setSelectedFineTuneType] = useState('Text') - const [files, setFiles] = useState([]) +const Header = styled('div')({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100px', +}); - const handleInputChange = (event: React.ChangeEvent) => { - setInputValue(event.target.value) - } +const Block = styled('div')({ + display: 'flex', + alignItems: 'center', + padding: '40px 20px', + marginBottom: '40px', + // boxShadow: '0 4px 8px rgba(0,0,0,0.1)', +}); - const onSend = async () => { - // const statusResult = await axios.post('/api/v1/sessions', { - // files: files, - // }) - try { - const formData = new FormData() - files.forEach((file) => { - formData.append("files", file) - }) +const RightMedia = styled('div')({ + flex: '1', + // paddingRight: '39px', +}); - formData.set('input', inputValue) - formData.set('mode', selectedMode) - if (selectedMode == "Create") { - formData.set("type", selectedCreateType) - } else { - formData.set("type", selectedFineTuneType) - } +const RightContent = styled('div')({ + flex: '1', + textAlign: 'left', + fontWeight: 500, +}); - await api.post('/api/v1/sessions', formData, { - // params: { - // path, - // }, - // onUploadProgress: (progressEvent) => { - // const percent = progressEvent.total && progressEvent.total > 0 ? - // Math.round((progressEvent.loaded * 100) / progressEvent.total) : - // 0 - // setUploadProgress({ - // percent, - // totalBytes: progressEvent.total || 0, - // uploadedBytes: progressEvent.loaded || 0, - // }) - // } - }).then((response) => { - account.loadSessions() - setFiles([]) - setInputValue("") - console.log("ABOUT TO NAVIGATE") - navigate('session', {session_id: response.id}) - console.log("DONE NAVIGATE") - }) - // result = true - } catch(e) { - console.log(e) - } - // setUploadProgress(undefined) - // return result +const LeftMedia = styled('div')({ + flex: '1', + paddingRight: '40px', +}); - // TODO: put this in state, when user clicks send, POST all three things - // (files, text, type) to a new endpoint which accepts files +const LeftContent = styled('div')({ + flex: '1', + textAlign: 'left', + fontWeight: 499, + paddingRight: '40px', +}); - // const result = await filestore.upload("lhwoo", files) - // if(!result) return - // await filestore.loadFiles(filestore.path) - // snackbar.success('Files Uploaded') - } +function OpenAIBlock() { + return ( + + + Helix Logo +

Open AI 😉

+

Deploy the best open source models securely in your cloud

+

Or let us run them for you

+
+ + + +
+ ); +} - const onUpload = useCallback(async (files: File[]) => { - console.log(files) - setFiles(files) - }, [ - filestore.path, - ]) +function ImageModelsBlock() { + return ( + + + Stable Diffusion XL + + +

Image models

+

Train your own SDXL customized to your brand or style

+ +
+ +
+
+ ); +} - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter' && (event.shiftKey || event.ctrlKey)) { - onSend() - event.preventDefault() - } - } +function LanguageModelsBlock() { + return ( + + +

Language models

+

Small open source LLMs are beating proprietary models

+ +
+ +
+ + Mistral-8B + +
+ ); +} - return ( - - - - - - - - - - - - - - {chatHistory.map((chat, index) => ( - {chat.user}: {chat.message} - ))} - - - - - {selectedMode === 'Finetune' && selectedFineTuneType === 'Images' && ( - - - - + + Servers in a data center + + +

Deployment

+
    +
  • GPU scheduler
  • +
  • Smart runners
  • +
  • Autoscaler
  • +
+ -
-
-
- ) + sx={{mb:2, fontSize: "large"}} + >CONNECT RUNNER +
+ + + + ); +} + +function Footer() { + return ( + + +

Clone us from GitHub

+

Customize it for your DevOps – or add models – to the open stack

+ +
+ + Helix Logo +
+ + GitHub users collaborating + +
+ ); +} + +// export default App; +const Dashboard: FC = () => { + return ( + + + + + +