From 4ca6fd6ebc6d6ab890b06187171e6ab0e37ae1d5 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Mon, 12 Aug 2024 12:28:38 -0400 Subject: [PATCH 01/30] WIP docs --- .github/CODE_OF_CONDUCT.md | 134 ++++++ .github/CONTRIBUTING.md | 54 +++ .github/ISSUE_TEMPLATE/bug_report.md | 19 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 + .github/ISSUE_TEMPLATE/tech_debt.md | 7 +- .github/SECURITY.md | 9 + .github/pull_request_template.md | 16 + .markdownlint.json | 16 + .markdownlintignore | 5 + CONTRIBUTING.md | 66 --- LICENSE | 2 +- README.md | 97 ++-- .../dev/cpu/uds-bundle.yaml | 0 .../dev/cpu/uds-config.yaml | 0 .../dev/gpu/uds-bundle.yaml | 0 .../dev/gpu/uds-config.yaml | 0 .../latest/cpu/uds-bundle.yaml | 0 .../latest/cpu/uds-config.yaml | 0 .../latest/gpu/uds-bundle.yaml | 0 .../latest/gpu/uds-config.yaml | 0 uds-bundles/dev/README.md | 118 ----- website/.markdownlint.json | 6 - website/LICENSE | 2 +- website/README.md | 10 +- .../docs/leapfrogai/tadpole/tadpole-deploy.md | 111 ----- .../en/docs/local deploy guide/_index.md | 13 - .../en/docs/local deploy guide/components.md | 35 -- .../en/docs/local deploy guide/deploy.md | 428 ------------------ .../en/docs/local deploy guide/quick_start.md | 152 ------- .../docs/local deploy guide/requirements.md | 90 ---- .../en/docs/local-deploy-guide/_index.md | 9 + .../en/docs/local-deploy-guide/components.md | 61 +++ .../dependencies.md | 61 ++- .../en/docs/local-deploy-guide/quick_start.md | 180 ++++++++ .../docs/local-deploy-guide/requirements.md | 51 +++ .../_index.md | 2 +- website/hugo.toml | 10 +- 37 files changed, 637 insertions(+), 1129 deletions(-) create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/SECURITY.md create mode 100644 .github/pull_request_template.md create mode 100644 .markdownlint.json create mode 100644 .markdownlintignore delete mode 100644 CONTRIBUTING.md rename {uds-bundles => bundles}/dev/cpu/uds-bundle.yaml (100%) rename {uds-bundles => bundles}/dev/cpu/uds-config.yaml (100%) rename {uds-bundles => bundles}/dev/gpu/uds-bundle.yaml (100%) rename {uds-bundles => bundles}/dev/gpu/uds-config.yaml (100%) rename {uds-bundles => bundles}/latest/cpu/uds-bundle.yaml (100%) rename {uds-bundles => bundles}/latest/cpu/uds-config.yaml (100%) rename {uds-bundles => bundles}/latest/gpu/uds-bundle.yaml (100%) rename {uds-bundles => bundles}/latest/gpu/uds-config.yaml (100%) delete mode 100644 uds-bundles/dev/README.md delete mode 100644 website/.markdownlint.json delete mode 100644 website/content/en/docs/leapfrogai/tadpole/tadpole-deploy.md delete mode 100644 website/content/en/docs/local deploy guide/_index.md delete mode 100644 website/content/en/docs/local deploy guide/components.md delete mode 100644 website/content/en/docs/local deploy guide/deploy.md delete mode 100644 website/content/en/docs/local deploy guide/quick_start.md delete mode 100644 website/content/en/docs/local deploy guide/requirements.md create mode 100644 website/content/en/docs/local-deploy-guide/_index.md create mode 100644 website/content/en/docs/local-deploy-guide/components.md rename website/content/en/docs/{local deploy guide => local-deploy-guide}/dependencies.md (64%) create mode 100644 website/content/en/docs/local-deploy-guide/quick_start.md create mode 100644 website/content/en/docs/local-deploy-guide/requirements.md rename website/content/en/docs/{prod deployment => production-guide}/_index.md (66%) diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..2db16038a --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,134 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +`leapfrogai [@] defenseunicorns.com`. + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..e8672f740 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,54 @@ +# Welcome to LeapfrogAI + +Thank you for your interest in LeapfrogAI! + +This document describes the process and requirements for contributing. + +## Developer Experience + +Continuous Delivery is core to our development philosophy. Check out [https://minimumcd.org](https://minimumcd.org) for a good baseline agreement on what that means. + +Specifically: + +- We do trunk-based development (main) with short-lived feature branches that originate from the trunk, get merged into the trunk, and are deleted after the merge +- We don't merge code into main that isn't releasable +- We perform automated testing on all changes before they get merged to main +- Continuous integration (CI) pipeline tests are definitive +- We create immutable release artifacts + +### Developer Workflow + +:key: == Required by automation + +1. Drop a comment in any issue to let everyone know you're working on it and submit a Draft PR (step 4) as soon as you are able. +2. :key: Set up your Git config to GPG sign all commits. [Here's some documentation on how to set it up](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). You won't be able to merge your PR if you have any unverified commits. +3. Use the [pre-commit](https://pre-commit.com/) hooks to provide localized checks against your new or modified code to catch mistakes before pushing. + + - Pre-commit must be activated in a Python-enabled environment and installed to the local `.git` repository to activate the commit hooks properly + - UDS and Zarf Lints require the [UDS tasks](../tasks.schema.json), [UDS](../uds.schema.json), and [Zarf](<(../zarf.schema.json)>) JSON schemas to be up-to-date with the current UDS CLI version (e.g., v0.14.0) + + ```bash + wget https://raw.githubusercontent.com/defenseunicorns/uds-cli/v0.14.0/uds.schema.json + wget https://raw.githubusercontent.com/defenseunicorns/uds-cli/v0.14.0/zarf.schema.json + wget https://raw.githubusercontent.com/defenseunicorns/uds-cli/v0.14.0/tasks.schema.json + ``` + +4. Create a Draft Pull Request as soon as you can, even if it is just 5 minutes after you started working on it. We lean towards working in the open as much as we can. + + > ⚠️ **NOTE:** _:key: We use [Conventional Commit messages](https://www.conventionalcommits.org/) in PR titles so, if you can, use one of `fix:`, `feat:`, `chore:`, `docs:` or similar. If you need help, just use with `wip:` and we'll help with the rest_ + +5. :key: Automated tests will begin based on the paths you have edited in your Pull Request. + + > ⚠️ **NOTE:** _If you are an external third-party contributor, the pipelines won't run until a [CODEOWNER](./CODEOWNERS) approves the pipeline run._ + +6. :key: Be sure to heed the `needs-adr`,`needs-docs`,`needs-tests` labels as appropriate for the PR. Once you have addressed all of the needs, remove the label or request a maintainer to remove it. +7. Once the review is complete and approved, a core member of the project will merge your PR. If you are an external third-party contributor, two core members (CODEOWNERS) of the project will be required to approve the PR. +8. Close the issue if it is fully resolved by your PR. _Hint: You can add "Fixes #XX" to the PR description to automatically close an issue when the PR is merged._ + +### Release Please + +We've chosen Google's [release-please](https://github.com/googleapis/release-please#release-please) as our automated tag and release solution. Below are some basic usage instructions. Read the documentation provided in the link for more advanced usage. + +- Use the conventional commits specification for all PRs that are merged into the `main` branch. +- To specify a specific version, like a patch or minor, you must provide an empty commit like this: `git commit --allow-empty -m "chore: release 0.1.0" -m "Release-As: 0.1.0"` +- Maintain and provide a `secrets.RELEASE_PLEASE_TOKEN` Personal Access Token (PAT) as identified in the GitHub workflow YAML. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d35438a69..00236e769 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,21 +7,30 @@ assignees: '' --- ### Environment -Device and OS: -App/package versions: -Kubernetes distro being used: -Other: + +1. OS and Architecture: +2. App or Package Name: +2. App or Package Version: +3. Kubernetes Distribution: +4. Kubernetes Version: +5. Other: ### Steps to reproduce + 1. ### Expected result +- + ### Actual Result +- + ### Visual Proof (screenshots, videos, text, etc) -### Severity/Priority +- ### Additional Context + Add any other context or screenshots about the technical debt here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index cbbc3ddb9..19ea5246d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -19,7 +19,9 @@ assignees: '' **Then** [something happens] ### Describe alternatives you've considered + (optional) A clear and concise description of any alternative solutions or features you've considered. ### Additional context + Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/tech_debt.md b/.github/ISSUE_TEMPLATE/tech_debt.md index 729bf25ce..b718b9fce 100644 --- a/.github/ISSUE_TEMPLATE/tech_debt.md +++ b/.github/ISSUE_TEMPLATE/tech_debt.md @@ -7,10 +7,13 @@ assignees: '' --- ### Describe what should be investigated or refactored + A clear and concise description of what should be changed/researched. Ex. This piece of the code is not DRY enough [...] ### Links to any relevant code -(optional) i.e. - https://github.com/defenseunicorns/uds-software-factory/blob/main/README.md?plain=1#L1 + +(optional) i.e. - ### Additional context -Add any other context or screenshots about the technical debt here. \ No newline at end of file + +Add any other context or screenshots about the technical debt here. diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 000000000..6a4387f85 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Supported Versions + +As the LeapfrogAI has not yet reached v1.0.0, only the current latest minor release is supported. + +## Reporting a Vulnerability + +Please email `leapfrogai [@] defenseunicorns.com` to report a vulnerability for more details. If you are unable to disclose details via email, please let us know and we can coordinate alternate communications. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..8074a83da --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,16 @@ +## Description + +### BREAKING CHANGES + +### CHANGES + +## Related Issue + +Fixes # + +Relates to # + +## Checklist before merging + +- [ ] Tests, documentation, ADR added or updated as needed +- [ ] Followed the [Contributor Guide Steps](https://github.com/defenseunicorns/leapfrogai/blob/main/.github/CONTRIBUTING.md) diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 000000000..9a9ca0851 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,16 @@ +{ + "MD053": false, + "MD034": false, + "MD013": false, + "MD029": false, + "MD041": false, + "MD033": false, + "MD004": false, + "MD024": false, + "MD036": false, + "MD028": false, + "MD049": false, + "MD007": false, + "MD022": false, + "MD025": false +} diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 000000000..2066086cd --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,5 @@ +LICENSE +CHANGELOG.md +CODEOWNERS +.github/ISSUE_TEMPLATE/ +.github/pull_request_template.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index b764d1ccd..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,66 +0,0 @@ -# Contributing to LeapfrogAI (LFAI) - -First off, thanks so much for wanting to help out! :tada: - -This document describes the steps and requirements for contributing a bug fix or feature in a Pull Request to all LeapfrogAI products! If you have any questions about the process or the pull request you are working on feel free to reach out in the [LFAI Discord Channel](https://discord.gg/s2Ja5cmZRQ). - -## Developer Experience - -Continuous Delivery is core to our development philosophy. Check out [https://minimumcd.org](https://minimumcd.org/) for a good baseline agreement on what that means. - -Specifically: - -- We do trunk-based development (`main`) with short-lived feature branches that originate from the trunk, get merged into the trunk, and are deleted after the merge -- We don't merge code into `main` that isn't releasable -- We perform automated testing on all changes before they get merged to `main` -- We create ADRs for all architectural decisions -- Merges are always squashed and use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) -- We create immutable release artifacts - -### Developer Workflow - -:key: == Required by automation - -1. Look at the next due [issue] and pick an issue that you want to work on. If you don't see anything that interests you, create an issue and assign it to yourself. -2. Drop a comment in the issue to let everyone know you're working on it and submit a Draft PR (step 4) as soon as you are able. If you have any questions as you work through the code, reach out in the [LFAI Discord Channel](https://discord.gg/s2Ja5cmZRQ). -3. :key: Set up your Git config to GPG sign all commits. [Here's some documentation on how to set it up](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). You won't be able to merge your PR if you have any unverified commits. -4. Create a Draft Pull Request as soon as you can, even if it is just 5 minutes after you started working on it. We lean towards working in the open as much as we can. If you're not sure what to put in the PR description, just put a link to the issue you're working on. If you're not sure what to put in the PR title, just put "WIP" (Work In Progress) and we'll help you out with the rest. -5. :key: Automated tests will begin based on the paths you have edited in your Pull Request. - > ⚠️ **NOTE:** _If you are an external third-party contributor, the pipelines won't run until a [CODEOWNER](https://github.com/zarf-dev/zarf/blob/main/CODEOWNERS) approves the pipeline run._ -6. :key: Be sure to use the [needs-adr,needs-docs,needs-tests](https://github.com/zarf-dev/zarf/labels?q=needs) labels as appropriate for the PR. Once you have addressed all of the needs, remove the label. -7. Once the review is complete and approved, a core member of the LeapfrogAI project will merge your PR. If you are an external third-party contributor, two core members of the zarf project will be required to approve the PR. -8. Close the issue if it is fully resolved by your PR. _Hint: You can add "Fixes #XX" to the PR description to automatically close an issue when the PR is merged._ - -## Testing - -TBD - -## Documentation - -### Updating Our Documentation - -Under construction. - -### Architecture Decision Records (ADR) - -We've chosen to use ADRs to document architecturally significant decisions. We primarily use the guidance found in [this article by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) with a couple of tweaks: - -- The criteria for when an ADR is needed is undefined. The team will decide when the team needs an ADR. -- We will use the tool [adr-tools](https://github.com/npryce/adr-tools) to make it easier on us to create and maintain ADRs. -- We will keep ADRs in the repository under `adr/NNNN-name-of-adr.md`. `adr-tools` is configured with a dotfile to automatically use this directory and format. - -### How to use `adr-tools` - -```bash -# Create a new ADR titled "Use Bisquick for all waffle making" -adr new Use Bisquick for all waffle making - -# Create a new ADR that supersedes a previous one. Let's say, for example, that the previous ADR about Bisquick was ADR number 9. -adr new -s 9 Use scratch ingredients for all waffle making - -# Create a new ADR that amends a previous one. Let's say the previous one was ADR number 15 -adr new -l "15:Amends:Amended by" Use store-bought butter for all waffle making - -# Get full help docs. There are all sorts of other helpful commands that help manage the decision log. -adr help -``` diff --git a/LICENSE b/LICENSE index 1bf24df1d..8dd2f8876 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [2023] [Defense Unicorns] + Copyright 2024 Defense Unicorns Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 92a1b8af6..191641e63 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,12 @@ - [Components](#components) - [API](#api) - [Backends](#backends) + - [Repeater](#repeater) - [SDK](#sdk) - - [User Interface](#user-interface) - - [Repeater](#repeater) - - [Image Hardening](#image-hardening) + - [UI](#ui) - [Usage](#usage) - - [UDS](#uds) - - [UDS Latest](#uds-latest) - - [UDS Dev](#uds-dev) - - [Local Dev](#local-dev) +- [Local Development](#local-development) +- [Contributing](#contributing) - [Community](#community) ## Overview @@ -43,7 +40,7 @@ Large Language Models (LLMs) are a powerful resource for AI-driven decision maki The LeapfrogAI repository follows a monorepo structure based around an [API](#api) with each of the [components](#components) included in a dedicated `packages` directory. Each of these package directories contains the source code for each component as well as the deployment infrastructure. The UDS bundles that handle the development and latest deployments of LeapfrogAI are in the `uds-bundles` directory. The structure looks as follows: -```shell +```bash leapfrogai/ ├── src/ │ ├── leapfrogai_api/ # source code for the API @@ -52,13 +49,13 @@ leapfrogai/ ├── packages/ │ ├── api/ # deployment infrastructure for the API │ ├── llama-cpp-python/ # source code & deployment infrastructure for the llama-cpp-python backend -│ ├── repeater/ # source code & deployment infrastructure for the repeater model backend +│ ├── repeater/ # source code & deployment infrastructure for the repeater model backend │ ├── supabase/ # deployment infrastructure for the Supabase backend and postgres database │ ├── text-embeddings/ # source code & deployment infrastructure for the text-embeddings backend │ ├── ui/ # deployment infrastructure for the UI │ ├── vllm/ # source code & deployment infrastructure for the vllm backend │ └── whisper/ # source code & deployment infrastructure for the whisper backend -├── uds-bundles/ +├── bundles/ │ ├── dev/ # uds bundles for local uds dev deployments │ └── latest/ # uds bundles for the most current uds deployments ├── Makefile @@ -69,7 +66,9 @@ leapfrogai/ ## Getting Started -The preferred method for running LeapfrogAI is a local [Kubernetes](https://kubernetes.io/) deployment using [UDS](https://github.com/defenseunicorns/uds-core). Refer to the [Quick Start](https://docs.leapfrog.ai/docs/local-deploy-guide/quick_start/) section of the LeapfrogAI documentation site for instructions on this type of deployment. +The preferred method for running LeapfrogAI is a local [Kubernetes](https://kubernetes.io/) deployment using [UDS](https://github.com/defenseunicorns/uds-core). + +Please refer to the [Quick Start](https://docs.leapfrog.ai/docs/local-deploy-guide/quick_start/) section of the LeapfrogAI documentation site for system requirements and instructions. ## Components @@ -81,69 +80,53 @@ LeapfrogAI provides an API that closely matches that of OpenAI's. This feature a LeapfrogAI provides several backends for a variety of use cases. -> Available Backends: -> | Backend | AMD64 Support | ARM64 Support | Cuda Support | Docker Ready | K8s Ready | Zarf Ready | -> | --- | --- | --- | --- | --- | --- | --- | -> | [llama-cpp-python](packages/llama-cpp-python/) | ✅ | 🚧 | ✅ | ✅ | ✅ | ✅ | -> | [whisper](packages/whisper/) | ✅ | 🚧 | ✅ | ✅ | ✅ | ✅ | -> | [text-embeddings](packages/text-embeddings/) | ✅ | 🚧 | ✅ | ✅ | ✅ | ✅ | -> | [vllm](packages/vllm/) | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | - -### SDK - -The LeapfrogAI [SDK](src/leapfrogai_sdk/) provides a standard set of protobuff and python utilities for implementing backends and gRPC. +Backends support and compatibility matrix: -### User Interface +| Backend | AMD64 | ARM64 | CUDA | Docker | Kubernetes | UDS | +| ---------------------------------------------- | ------ | ------ | ------ | ------ | ---------- | ------- | +| [llama-cpp-python](packages/llama-cpp-python/) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [whisper](packages/whisper/) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [text-embeddings](packages/text-embeddings/) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [vllm](packages/vllm/) | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | -LeapfrogAI provides a [User Interface](src/leapfrogai_ui/) with support for common use-cases such as chat, summarization, and transcription. - -### Repeater +#### Repeater The [repeater](packages/repeater/) "model" is a basic "backend" that parrots all inputs it receives back to the user. It is built out the same way all the actual backends are and it primarily used for testing the API. -### Image Hardening - -> GitHub Repo: -> -> - [leapfrogai-images](https://github.com/defenseunicorns/leapfrogai-images) - -LeapfrogAI leverages Chainguard's [apko](https://github.com/chainguard-dev/apko) to harden base python images - pinning Python versions to the latest supported version by the other components of the LeapfrogAI stack. - -## Usage +### SDK -### UDS +The LeapfrogAI [SDK](src/leapfrogai_sdk/) provides a standard set of protobufs and Python utilities for implementing backends with gRPC. -LeapfrogAI can be deployed and run locally via UDS and Kubernetes, built out using [Zarf](https://zarf.dev) packages. See the [Quick Start](https://docs.leapfrog.ai/docs/local-deploy-guide/quick_start/#prerequisites) for a list of prerequisite packages that must be installed first. +### UI -Prior to deploying any LeapfrogAI packages, a UDS Kubernetes cluster must be deployed using the most recent k3d bundle: +LeapfrogAI provides a [UI](src/leapfrogai_ui/) with support for common use-cases such as general chat and "Q&A with your documents". -```sh -make create-uds-cpu-cluster -``` +## Usage -#### UDS Latest +To build a LeapfrogAI UDS bundle and deploy it, please refer to the [LeapfrogAI Documentation Website](https://docs.leapfrog.ai/docs/). In the documentation website, you'll find system requirements and instructions for all things LeapfrogAI that aren't associated to local development and contributing. -This type of deployment pulls the most recent package images and is the most stable way of running a local LeapfrogAI deployment. These instructions can be found on the [LeapfrogAI Docs](https://docs.leapfrog.ai/docs/) site. +For contributing and local deployment and development for each component in a local Python or Node.js environment please continue on to the [next section](#local-development). -#### UDS Dev +## Local Development -If you want to make some changes to LeapfrogAI before deploying via UDS (for example in a dev environment), follow the [UDS Dev Instructions](/uds-bundles/dev/README.md). +Each of the LeapfrogAI components can also be run individually outside of a Kubernetes or Containerized environment. This is useful when testing changes to a specific component, but will not assist in a full deployment of LeapfrogAI. Please refer to the [above section](#usage) for deployment instructions. +Please refer to the linked READMEs for each individual packages local development instructions: -### Local Dev +- [API](/src/leapfrogai_api/README.md) +- [LLaMA C++ Python](/packages/llama-cpp-python/README.md) +- [vLLM](/packages/vllm/README.md) +- [Supabase](/packages/supabase/README.md) +- [Text Embeddings](/packages/text-embeddings/README.md) +- [UI](/src/leapfrogai_ui/README.md) +- [Faster Whisper](/packages/whisper/README.md) +- [Repeater](/packages/repeater/README.md) -Each of the LFAI components can also be run individually outside of a Kubernetes environment via local development. This is useful when testing changes to a specific component, but will not assist in a full deployment of LeapfrogAI. Please refer to the above sections for deployment instructions. +## Contributing -Please refer to the linked READMEs for each individual packages local development instructions: +All potential and current contributors must ensure that they have read the [Contributing documentation](.github/CONTRIBUTING.md), [Security Policies](.github/SECURITY.md) and [Code of Conduct](.github/CODE_OF_CONDUCT.md) prior to opening an issue or pull request to this repository. -- [API](/src/leapfrogai_api/README.md) -- [llama-cpp-python](/packages/llama-cpp-python/README.md) -- [repeater](/packages/repeater/README.md) -- [supabase](/packages/supabase/README.md) -- [text-embeddings](/packages/text-embeddings/README.md) -- [ui](/src/leapfrogai_ui/README.md) -- [vllm](/packages/vllm/README.md) -- [whisper](/packages/whisper/README.md) +When submitting an issue or opening a PR, please first ensure that you have searched your potential issue or PR against the existing or closed issues and PRs. Perceived duplicates will be closed, so please reference and differentiate your contributions from tangential or similar issues and PRs. ## Community @@ -162,4 +145,4 @@ LeapfrogAI is supported by a community of users and contributors, including: [![Defense Unicorns logo](/docs/imgs/user-logos/defense-unicorns.png)](https://defenseunicorns.com)[![Beast Code logo](/docs/imgs/user-logos/beast-code.png)](https://beast-code.com)[![Hypergiant logo](/docs/imgs/user-logos/hypergiant.png)](https://hypergiant.com)[![Pulze logo](/docs/imgs/user-logos/pulze.png)](https://pulze.ai) -*Want to add your organization or logo to this list? [Open a PR!](https://github.com/defenseunicorns/leapfrogai/edit/main/README.md)* +_Want to add your organization or logo to this list? [Open a PR!](https://github.com/defenseunicorns/leapfrogai/edit/main/README.md)_ diff --git a/uds-bundles/dev/cpu/uds-bundle.yaml b/bundles/dev/cpu/uds-bundle.yaml similarity index 100% rename from uds-bundles/dev/cpu/uds-bundle.yaml rename to bundles/dev/cpu/uds-bundle.yaml diff --git a/uds-bundles/dev/cpu/uds-config.yaml b/bundles/dev/cpu/uds-config.yaml similarity index 100% rename from uds-bundles/dev/cpu/uds-config.yaml rename to bundles/dev/cpu/uds-config.yaml diff --git a/uds-bundles/dev/gpu/uds-bundle.yaml b/bundles/dev/gpu/uds-bundle.yaml similarity index 100% rename from uds-bundles/dev/gpu/uds-bundle.yaml rename to bundles/dev/gpu/uds-bundle.yaml diff --git a/uds-bundles/dev/gpu/uds-config.yaml b/bundles/dev/gpu/uds-config.yaml similarity index 100% rename from uds-bundles/dev/gpu/uds-config.yaml rename to bundles/dev/gpu/uds-config.yaml diff --git a/uds-bundles/latest/cpu/uds-bundle.yaml b/bundles/latest/cpu/uds-bundle.yaml similarity index 100% rename from uds-bundles/latest/cpu/uds-bundle.yaml rename to bundles/latest/cpu/uds-bundle.yaml diff --git a/uds-bundles/latest/cpu/uds-config.yaml b/bundles/latest/cpu/uds-config.yaml similarity index 100% rename from uds-bundles/latest/cpu/uds-config.yaml rename to bundles/latest/cpu/uds-config.yaml diff --git a/uds-bundles/latest/gpu/uds-bundle.yaml b/bundles/latest/gpu/uds-bundle.yaml similarity index 100% rename from uds-bundles/latest/gpu/uds-bundle.yaml rename to bundles/latest/gpu/uds-bundle.yaml diff --git a/uds-bundles/latest/gpu/uds-config.yaml b/bundles/latest/gpu/uds-config.yaml similarity index 100% rename from uds-bundles/latest/gpu/uds-config.yaml rename to bundles/latest/gpu/uds-config.yaml diff --git a/uds-bundles/dev/README.md b/uds-bundles/dev/README.md deleted file mode 100644 index deb25ddde..000000000 --- a/uds-bundles/dev/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# LeapfrogAI UDS Dev Deployment Instructions - -Follow these instructions to create a local development deployment of LeapfrogAI using [UDS](https://github.com/defenseunicorns/uds-core). - -Make sure your system has the [required dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/quick_start/#prerequisites). - -For ease, it's best to create a virtual environment: - -```shell -python -m venv .venv -source .venv/bin/activate -``` - -#### Linux and Windows (via WSL2) - -Each component is built into its own Zarf package. You can build all of the packages you need at once with the following `Make` targets: -> ***Note:*** You need to build with `make build-* LOCAL_VERSION=dev` to set the tag to `dev` instead of the commit hash locally. - -> ***NOTE:*** Some of the packages have Python dev dependencies that need to be installed when building them locally. These dependencies are used to download the model weights that will be included in the final Zarf package. These dependencies are listed as `dev` in the `project.optional-dependencies` section of each models `pyproject.toml`. - -You can build all of the packages you need at once with the following `Make` targets: - -```shell -LOCAL_VERSION=dev make build-cpu # api, llama-cpp-python, text-embeddings, whisper, supabase -LOCAL_VERSION=dev make build-gpu # api, vllm, text-embeddings, whisper, supabase -LOCAL_VERSION=dev make build-all # all of the backends -``` - -**OR** - -You can build components individually using the following `Make` targets: - -```shell -LOCAL_VERSION=dev make build-api -LOCAL_VERSION=dev make build-supabase -LOCAL_VERSION=dev make build-vllm # if you have GPUs (macOS not supported) -LOCAL_VERSION=dev make build-llama-cpp-python # if you have CPU only -LOCAL_VERSION=dev make build-text-embeddings -LOCAL_VERSION=dev make build-whisper -``` - -**NOTE: If you do not prepend your commands with `LOCAL_VERSION=dev`, uds will not find the generated zarf packages, as -they will be tagged with your current git hash instead of `dev` which uds expects** - -#### macOS - -To run the same commands in macOS, you will need to prepend your command with a couple of env vars like so: - -All Macs: `REG_PORT=5001` - -Apple Silicon (M1/M2/M3/M4 series) Macs: `ARCH=arm64` - -To demonstrate what this would look like for an Apple Silicon Mac: -``` shell -REG_PORT=5001 ARCH=arm64 LOCAL_VERSION=dev make build-cpu -``` - -To demonstrate what this would look like for an older Intel Mac (not officially supported): -``` shell -REG_PORT=5001 LOCAL_VERSION=dev make build-cpu -``` - -**OR** - -You can build components individually using the following `Make` targets, just like in the Linux section except ensuring -to prepend the env vars detailed above. - -#### Once the packages are created, you can deploy either a CPU or GPU-enabled deployment via one of the UDS bundles (macOS only supports cpu) - -# Deploying via UDS bundle - -## CPU UDS Deployment - -Create the uds CPU bundle: -```shell -cd uds-bundles/dev/cpu -uds create . -``` - -Deploy a [UDS cluster](/README.md#uds) if one isn't deployed already - -Deploy the LeapfrogAI bundle: -```shell -uds deploy uds-bundle-leapfrogai*.tar.zst -``` - -## GPU UDS Deployment - -Create the uds GPU bundle: -```shell -cd uds-bundles/dev/gpu -uds create . -``` - -Deploy a [UDS cluster](/README.md#uds) with the following flags, as so: - -```shell -uds deploy {k3d-cluster-name} --set K3D_EXTRA_ARGS="--gpus=all --image=ghcr.io/justinthelaw/k3d-gpu-support:v1.27.4-k3s1-cuda" -``` - - -Deploy the LeapfrogAI bundle: -```shell -uds deploy uds-bundle-leapfrogai-*.tar.zst --confirm -``` - -Once running you can access the various components, if deployed and exposed, at the following URLS: - -```shell -https://ai.uds.dev # UI -https://leapfrogai-api.uds.dev # API -https://supabase-kong.uds.dev # Supabase Kong -https://keycloak.uds.dev # Keycloak -``` - -## Checking and Managing the Deployment - -For tips on how to monitor the deployment, accessing the UI, and clean up, please reference the [Quick Start](https://docs.leapfrog.ai/docs/local-deploy-guide/quick_start/#checking-deployment) guide in the LeapfrogAI docs. diff --git a/website/.markdownlint.json b/website/.markdownlint.json deleted file mode 100644 index 721182d36..000000000 --- a/website/.markdownlint.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "default": true, - "MD033": false, - "MD013": false, - "MD036": false -} diff --git a/website/LICENSE b/website/LICENSE index 1bf24df1d..8dd2f8876 100644 --- a/website/LICENSE +++ b/website/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [2023] [Defense Unicorns] + Copyright 2024 Defense Unicorns Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/website/README.md b/website/README.md index 374420575..3e4434799 100644 --- a/website/README.md +++ b/website/README.md @@ -10,10 +10,12 @@ which is a fork of the Google Docsy theme. The Docsy documentation can be used a This repository enforces [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) messages. See the documentation for [`release-please`](https://github.com/googleapis/release-please#how-should-i-write-my-commits) for correctly formatting commit messages. [This video](https://www.youtube.com/watch?v=lwGcnDgwmFc&ab_channel=Syntax) does a good job of showing how to add the `Conventional Commit` VSCode extension to use when creating the commit messages. -#### Prerequisites +### Pre-Requisites [Hugo](https://gohugo.io/documentation/) is required in order to utilize the doc site template. You can run `brew install hugo` to quickly install or see the [installation page](https://gohugo.io/installation/) for additional install methods. +Go and Node are also required dependencies for running a Hugo site. Refer to the [Go installation documentation](https://go.dev/doc/install) and [NVM documentation](https://github.com/nvm-sh/nvm) for details. + ## Getting Started Create a new repository from this template: @@ -23,9 +25,9 @@ Create a new repository from this template: Clone your new site: ```bash -git clone -cd -npm ci +git clone https://github.com/defenseunicorns/leapfrogai.git +cd website +npm install ``` To run the site for local development: diff --git a/website/content/en/docs/leapfrogai/tadpole/tadpole-deploy.md b/website/content/en/docs/leapfrogai/tadpole/tadpole-deploy.md deleted file mode 100644 index 4a735113f..000000000 --- a/website/content/en/docs/leapfrogai/tadpole/tadpole-deploy.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: Sandbox Deployment -type: docs -draft: true ---- - -## Overview - -The Tadpole sandbox deployment is the lightweight method to initiate your LeapfrogAI experience and is exclusively designed for **local testing and development purposes only**. Tadpole facilitates a non-Kubernetes deployment that executes a `docker compose` build of the LeapfrogAI API, language backend, and user interface. To ensure a smooth start, there are a collection of straightforward basic recipes. Executing any of these recipes initiates the automated processes within Tadpole, encompassing the build, configuration, and initiation of the necessary components. The culmination of this process results in a locally hosted "Chat with an LLM" demonstration. - -### Prerequisites - -- Have [Docker](https://docs.docker.com/get-docker/) installed. -- Have [Continue.dev](https://continue.dev/) installed. - -### System Requirements - -- `chat` and `code` recipes require a minimum of 16GB RAM. -- `chat-gpu` recipe requires a minimum of 8GB VRAM and a CUDA capable NVIDIA GPU with drivers setup in order to function correctly with Docker. - -{{% alert-note %}} -To set up your CUDA capable NVIDIA GPU, please see the following instructions: - -- Prepare your machine for [NVIDIA Driver installation.](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#pre-installation-actions) -- Install the proper [NVIDIA Drivers.](https://docs.nvidia.com/datacenter/tesla/tesla-installation-notes/index.html#pre-install) -- Find the correct [CUDA for your environment.](https://developer.nvidia.com/cuda-downloads) -- [Install CUDA](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#pre-installation-actions) properly. -- Prepare Docker for [GPU accessibility.](https://docs.docker.com/config/containers/resource_constraints/#gpu) -- Obtain the [NVIDIA device plugin](https://github.com/NVIDIA/k8s-device-plugin) for Kubernetes. -{{% /alert-note %}} - -### Operating Systems - -- macOS: Only CPU recipes are compatible at this time. -- Windows: CPU recipes are compatible. GPU recipes require that the [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/install) is installed. -- Linux: CPU and GPU recipes are compatible. - -## Getting Started - -### Clone - -Clone into the [Tadpole GitHub repository](https://github.com/defenseunicorns/tadpole). - -{{% alert-note %}} -Cloning into this repository requires that users have an SSH key associated with your GitHub account. Please follow the [GitHub SSH documentation](https://docs.github.com/en/authentication/connecting-to-github-with-ssh) to obtain this key. -{{% /alert-note %}} - -### Chat - -To spin up the Tadpole chatbot to your local environment: - -```git -make chat -``` - -The Leapfrog-UI will be running at `http://localhost:3000/`. - -### Code - -{{% alert-note %}} -This recipe is intended for use with a code extension such as [Continue.dev](https://continue.dev/) and has been tested with the v0.7.53 prerelease. -{{% /alert-note %}} - -To build and run the code backend: - -```git -make code -``` - -Navigate to `$HOME/.continue/config.json` and modify your [Continue.dev](https://continue.dev/) configuration: - -```git -{ - "models": - [{ - "title": "leapfrogai", - "provider": "openai", - "model": "leapfrogai", - "apiKey": "freeTheModels", - "apiBase": "http://localhost:8080/openai" - }], - "modelRoles": - { - "default": "leapfrogai" - } -} -``` - -### Chat-GPU - -{{% alert-note %}} -This requires a CUDA capable NVIDIA GPU with drivers setup. -{{% /alert-note %}} - -To activate GPU resources and increase response time for your chatbot: - -```git -make chat-gpu -``` - -The Leapfrog-UI will be running at `http://localhost:3000/`. - -### Cleanup - -When you are finished, run this cleanup command to remove Tadpole from your system: - -```git -make clean -``` - -For any additional information, or to report an issue, please see the [Tadpole GitHub repository.](https://github.com/defenseunicorns/tadpole/tree/main) diff --git a/website/content/en/docs/local deploy guide/_index.md b/website/content/en/docs/local deploy guide/_index.md deleted file mode 100644 index 7e0a67448..000000000 --- a/website/content/en/docs/local deploy guide/_index.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Local Deployment Guide -type: docs -weight: 1 ---- - -This documentation serves as a comprehensive guide for users, providing instructions on the system requirements, setup, and deployment of LeapfrogAI to their local environment. Please note that the specified order of steps must be followed to ensure an efficient deployment process. - -## Overview - -LeapfrogAI stands as a cutting-edge self-hosted generative AI platform, strategically designed for secure and disconnected environments, offering a ChatGPT-like experience that mirrors OpenAI and Hugging Face API surfaces. This unique solution empowers your teams to seamlessly navigate a ChatGPT-like interface without the need for internet connectivity. Beyond its core capabilities, LeapfrogAI excels in efficient similarity searches across large-scale databases, providing robust generative embeddings for applications such as semantic similarity, clustering, and more. - -LeapfrogAI has the ability to leverage customer-specific data for fine-tuning models. This capability allows LeapfrogAI to gain insights into specific domains, ensuring the delivery of highly accurate contextual outputs tailored to your team's unique requirements and objectives. diff --git a/website/content/en/docs/local deploy guide/components.md b/website/content/en/docs/local deploy guide/components.md deleted file mode 100644 index d2a0e3d63..000000000 --- a/website/content/en/docs/local deploy guide/components.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Components -type: docs -weight: 3 ---- - -## Components - -### LeapfrogAI API - -LeapfrogAI offers an API closely aligned with OpenAI's, facilitating seamless compatibility for tools developed with OpenAI/ChatGPT to operate seamlessly with a LeapfrogAI backend. The LeapfrogAI API is a Python API that exposes LLM backends, via FastAPI and gRPC, in the OpenAI API specification. - -### Backend - -LeapfrogAI offers several backends for a variety of use cases: - -| Backend | Support | -| ------------------------------------------------------------------------------------------ | ------------------------------- | -| [llama-cpp-python](https://github.com/defenseunicorns/leapfrogai/tree/main/packages/llama-cpp-python) | AMD64, Docker, Kubernetes, Zarf | -| [whisper](https://github.com/defenseunicorns/leapfrogai/tree/main/packages/whisper) | AMD64, Docker, Kubernetes, Zarf | -| [text-embeddings](https://github.com/defenseunicorns/leapfrogai/tree/main/packages/text-embeddings) | AMD64, Docker, Kubernetes, Zarf | -| [VLLM](https://github.com/defenseunicorns/leapfrogai/tree/main/packages/vllm) | AMD64, Docker, Kubernetes, Zarf | -| [RAG](https://github.com/defenseunicorns/leapfrogai-backend-rag) | AMD64, Docker, Kubernetes, Zarf | - -### Image Hardening - -LeapfrogAI utilizes Chainguard's [apko](https://github.com/chainguard-dev/apko) to fortify base Python images by adhering to a version-pinning approach, ensuring compatibility with the latest supported version by other components within the LeapfrogAI stack. Please see the [leapfrogai-images](https://github.com/defenseunicorns/leapfrogai-images) GitHub repository for additional information. - -### Software Development Kit - -The LeapfrogAI SDK offers a standardized collection of Protobuf and Python utilities designed to facilitate the implementation of backends and gRPC. Please see the [leapfrogai-sdk](https://github.com/defenseunicorns/leapfrogai-sdk) GitHub repository for additional information. - -### User Interface - -LeapfrogAI offers user-friendly interfaces tailored for common use-cases, including chat, summarization, and transcription, providing accessible options for users to initiate these tasks. Please see the [leapfrogai-ui](https://github.com/defenseunicorns/leapfrogai-ui) GitHub repository for additional information. diff --git a/website/content/en/docs/local deploy guide/deploy.md b/website/content/en/docs/local deploy guide/deploy.md deleted file mode 100644 index 74d0dec78..000000000 --- a/website/content/en/docs/local deploy guide/deploy.md +++ /dev/null @@ -1,428 +0,0 @@ ---- -title: Advanced Deployments & Air Gap -type: docs -weight: 6 ---- - -These instructions are for users who are looking for a more customizable deployment of LeapfrogAI or require air gap deployment considerations. - -To successfully proceed with the installation and deployment of LeapfrogAI, steps must be executed in the order that they are presented in the following instructions. The LeapfrogAI deployment instructions are designed to guide advanced users through the process of deploying the latest version of LeapfrogAI on Kubernetes. - -## Switch to Sudo - -```bash -# login as required -sudo su -``` - -## Deploy Tools - -### Zarf - -Internet Access: - -```bash -# deploys latest version of Zarf -brew install zarf -``` - -Isolated Network: - -```bash -# download and store on removable media -wget https://github.com/zarf-dev/zarf/releases/download/v0.31.0/zarf_v0.31.0_Linux_amd64 - -# upload from removable media and install -mv zarf_v0.31.0_Linux_amd64 /usr/local/bin/zarf -chmod +x /usr/local/bin/zarf - -# check -zarf version -``` - -### Kubectl - -Internet Access: - -```bash -apt install kubectl -``` - -Isolated Network: - -```bash -# download and store on removable media -wget https://dl.k8s.io/release/v1.28.3/bin/linux/amd64/kubectl - -# upload from removable media and install -install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl - -# check -kubectl version -``` - -## Deploy Kubernetes Cluster - -The following commands are divided into three parts: download, create, and deploy. The main variation between "Internet Access" and "Isolated Network" is that, for an isolated network, users will execute the download and create steps outside the isolated network's environment, while the deploy step is done inside the isolated network. - -### Bootstrap k3d - -```git -# download -git clone https://github.com/zarf-dev/zarf-package-k3d-airgap.git -cd zarf-package-k3d-airgap - -# create -zarf package create --confirm - -zarf tools download-init - -cd metallb -zarf package create --confirm -``` - -```git -# deploy -cd ../ # if still in metallb folder -mkdir temp && cd temp -zarf package deploy --set enable_traefik=false --set enable_service_lb=true --set enable_metrics_server=false --set enable_gpus=false ../zarf-package-*.tar.zst - -cd ../ -zarf init --components git-server --confirm - -cd metallb -zarf package deploy --confirm zarf-package-*.tar.zst -``` - -Additional considerations are necessary for GPU deployments: - -```git -# deploy -cd ../ # if still in metallb folder -# temp folder to catch extra files generated during deploy -mkdir temp && cd temp -# largest difference is setting `enable_gpus` to `true` -zarf package deploy --set enable_traefik=false --set enable_service_lb=true --set enable_metrics_server=false --set enable_gpus=true ../zarf-package-*.tar.zst - -cd ../ -zarf init --components git-server --confirm - -cd metallb -zarf package deploy --confirm zarf-package-*.tar.zst -``` - -### UDS DUBBD - -```git -# download -git clone https://github.com/defenseunicorns/uds-package-dubbd.git -cd uds-package-dubbd/k3d/ - -# create -docker login registry1.dso.mil # account creation is required -zarf package create --confirm - -# deploy -zarf package deploy --confirm zarf-package-*.tar.zst -``` - -### Kyverno Configuration - -As of UDS DUBBD, v0.12+, a recently implemented Kyverno policy is causing certain LeapfrogAI pods to be restricted from execution. As we undergo refactoring efforts transitioning towards [Pepr](https://github.com/defenseunicorns/pepr), Kyverno's abstract replacement, the following guidelines outline the process for temporarily modifying the policy status from `Enforce` to `Audit`. - -```git -zarf tools kubectl patch clusterpolicy require-non-root-user --type='json' -p='[{"op": "replace", "path": "/spec/validationFailureAction", "value":"Audit"}]' -zarf tools kubectl patch clusterpolicy require-non-root-group --type='json' -p='[{"op": "replace", "path": "/spec/validationFailureAction", "value":"Audit"}]' -``` - -### GPU Support Test (Optional) - -The following support test is an optional addition for GPU deployments and helps confirm that the cluster's pods have access to expected GPU resources: - -```git -# download -git clone https://github.com/justinthelaw/gpu-support-test -cd leapfrogai-gpu-support-test - -# create -zarf package create --confirm - -# deploy -zarf package deploy zarf-package-*.tar.zst -# press "y" for prompt on deployment confirmation -# enter the number of GPU(s) that are expected to be available when prompted - -# clean-up -zarf package remove gpu-support-test -zarf tools registry prune --confirm -``` - -## Deploy LeapfrogAI - -### LeapfrogAI API - -```git -# download -git clone https://github.com/defenseunicorns/leapfrogai-api.git -cd leapfrogai-api/ - -# create -zarf package create --confirm - -# deploy -zarf package deploy zarf-package-*.zst --set ISTIO_ENABLED=true --set ISTIO_INJECTION=enabled --set ISTIO_GATEWAY=leapfrogai --components metallb-config --confirm -# if used without the `--confirm` flag, there are many prompted variables -# please read the variable descriptions in the zarf.yaml for more details -# after deploying the leapfrogai gateway, you may need to terminate the existing tenant gateway - -# configure, this will be removed in a future API release -zarf tools kubectl patch virtualservice leapfrogai -n leapfrogai --type='json' -p ' -[ - { - "op": "replace", - "path": "/spec", - "value": { - "gateways": [ - "istio-system/leapfrogai" - ], - "hosts": [ - "*" - ], - "http": [ - { - "match": [ - { - "uri": { - "prefix": "/leapfrogai-api/" - } - } - ], - "rewrite": { - "uri": "/" - }, - "route": [ - { - "destination": { - "host": "api", - "port": { - "number": 8080 - } - } - } - ] - }, - { - "match": [ - { - "uri": { - "prefix": "/openapi.json" - } - } - ], - "redirect": { - "uri": "/leapfrogai-api/openapi.json" - } - } - ] - } - } -]' -``` - -### Whisper Model (Optional) - -Deploy the Whisper Model for automatic speech recognition that transcribes speech to text. The Whisper Model backend is bundled with pre-packaged components, including Whisper-Base (limited to English language) and Faster-Whisper, which serves as the dedicated inferencing engine. - -```git -# download -git clone https://github.com/defenseunicorns/leapfrogai-backend-whisper.git -cd leapfrogai-backend-whisper - -# create -zarf package create --confirm - -# deploy -zarf package deploy zarf-package-*.tar.zst --confirm -``` - -Additional considerations are necessary for GPU deployments: - -The package deployment command is modified for GPU deployments: - -```git -# deploy -zarf package deploy zarf-package-*.tar.zst --set GPU_ENABLED=true --confirm -``` - -### LLaMA CPP Python - -This backend comes pre-packaged with [synthia-7b-v2.0.Q4_K_M](https://huggingface.co/TheBloke/SynthIA-7B-v2.0-GGUF#:~:text=v2.0.Q4_K_M.gguf-,Q4_K_M,-4), and `llama-cpp-python` as the inferencing engine and is primarily aimed at single user CPU deployments. - -```git -# download -git clone https://github.com/defenseunicorns/leapfrogai-backend-llama-cpp-python.git -cd leapfrogai-backend-llama-cpp-python - -# create -zarf package create --confirm -``` - -Additional considerations are necessary for GPU deployments: - -The package deployment command is modified for GPU deployments: - -```git -# deploy -zarf package deploy zarf-package-*.tar.zst --set GPU_ENABLED=true --confirm -``` - -### VLLM - -This backend comes pre-packaged with [synthia-7b-v2.0-awq](https://huggingface.co/TheBloke/SynthIA-7B-v2.0-AWQ), and `vllm` as the inferencing engine and is primarily aimed at multi-user GPU deployments. - -```git -# download -git clone https://github.com/defenseunicorns/leapfrogai-backend-vllm.git -cd leapfrogai-backend-vllm - -# create -zarf package create --confirm -``` - -Additional considerations are necessary for GPU deployments: - -The package deployment command is modified for GPU deployments: - -```git -# deploy -zarf package deploy zarf-package-*.tar.zst --set GPU_ENABLED=true --set REQUESTS_GPU=1 --set LIMITS_GPU=1 --set REQUESTS_CPU=0 --set LIMITS_CPU=0 --confirm -``` - -### LeapfrogAI UI (Optional) - -```git -# download -git clone https://github.com/defenseunicorns/leapfrogai-ui -cd leapfrogai-ui - -# create -zarf package create --confirm - -# deploy -cd leapfrogai-ui -zarf package deploy zarf-package-*.tar.zst --confirm -# if used without the `--confirm` flag, there are many prompted variables -# please read the variable descriptions in the zarf.yaml for more details -``` - -### Setup Ingress/Egress - -```git -k3d cluster edit zarf-k3d --port-add "443:30535@loadbalancer" -k3d cluster edit zarf-k3d --port-add "8080:30535@loadbalancer" - -# if the load balancer does not restart -k3d cluster start zarf-k3d -``` - -## LeapfrogAI UI and API - -- Navigate to `https://localhost:8080` to interact with LeapfrogAI UI. -- Navigate to `https://localhost:8080/leapfrogai-api/docs` to see usage details for the LeapfrogAI API. - -## Termination and Cleanup Procedures - -### Stop k3d Cluster - -Perform one of the following cleanup methods. The k3d command is the preferred method: - -```git -k3d cluster stop zarf-k3d -``` - -OR: - -```git -docker ps -# obtain the k3d cluster's container ID -docker stop -``` - -### Stop Zarf Registry - -```git -docker ps -# obtain the registry container ID -docker stop -``` - -### Cleanup - -Executing this command will remove all entities that are not associated with an active process. - -```git -docker system prune -a -f && docker volume prune -f -zarf tools clear-cache -rm -rf /tmp/zarf-* -``` - -## Troubleshooting - -The following outlines occasional deployment issues our teams have identified, which you may also encounter. - -### Cluster Connection - -**Issue:** After performing a restart or restarting the docker service, the cluster cannot be connected with. - -**Action:** - -```git -k3d cluster list -# verify that the cluster has `LOADBALANCER` set to true -# if not, try the following -k3d cluster stop zarf-k3d -k3d cluster start zarf-k3d -``` - -### Disk Pressure - -**Issue:** In certain scenarios, uploading multiple large AI models may lead to storage issues. To address this, there are several measures you can take to either optimize disk space usage or augment available space within a designated partition. - -**Action:** Remove unused files and storage. *Executing this command will remove all entities that are not associated with an active process.* Execute the following command sets to eliminate dangling or extraneous items. Additionally, consider deleting any previously deployed Zarf Packages to free up storage space. - -```git -# prune images stored in the local registry -zarf tools registry prune --confirm -# prune docker images, press "y" to confirm -docker image prune -# prune volumes, press "y" to confirm -docker volume prune -# clear zarf cache and temp files -zarf tools clear-cache -rm -rf /tmp/zarf-* -``` - -OR: - -**Action:** - -Check your disk's or mount's remaining space and utilization. - -```git -df -h -``` - -Go to the disk or mount in question, and check on the following paths: - -```git -ls -la /tmp -ls -la /var/lib/docker -``` - -In addition to your present working directory, the above paths are commonly identified as potential sources of excessive space consumption. To resolve this issue, it may be necessary to conduct manual cleanup or allocate additional space for the disks or mounts associated with these paths. - -### GPU Acceleration - -**Issue:** GPU access for Docker containers or pods in the Kubernetes cluster. - -**Action:** Please navigate to and read the [`gpu-support-test`](https://github.com/justinthelaw/gpu-support-test) repository. diff --git a/website/content/en/docs/local deploy guide/quick_start.md b/website/content/en/docs/local deploy guide/quick_start.md deleted file mode 100644 index 2b94d21b9..000000000 --- a/website/content/en/docs/local deploy guide/quick_start.md +++ /dev/null @@ -1,152 +0,0 @@ ---- -title: Quick Start -type: docs -weight: 2 ---- - -# LeapfrogAI UDS Deployment - -The fastest and easiest way to get started with a deployment of LeapfrogAI is by using [UDS](https://github.com/defenseunicorns/uds-core). These quick start instructions show how to deploy LeapfrogAI in either a CPU or GPU-enabled environment. - -## System Requirements - -Please review the following table to ensure your system meets the minimum requirements. LFAI can be run with or without GPU-access, but GPU-enabled systems are recommended due to the performance gains. The following assumes a single personal device: - -| | Minimum | Recommended (Performance) | -|-----|-------------------|---------------------------| -| RAM | 32 GB | 128 GB | -| CPU | 8 Cores @ 3.0 GHz | 32 Cores @ 3.0 GHz | -| GPU | N/A | 2x NVIDIA RTX 4090 GPUs | - -Additionally, please check the list of tested [operating systems](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/#operating-systems) for compatibility. - -## Prerequisites - -- [Python 3.11](https://www.python.org/downloads/release/python-3116/) - - NOTE: Different model packages will require different Python libraries. The libraries required will be listed in the `dev` optional dependencies in each projects `pyproject.toml` file. -- [Docker](https://docs.docker.com/engine/install/) -- [K3D](https://k3d.io/) -- [Zarf](https://docs.zarf.dev/getting-started/install/) -- [UDS CLI](https://github.com/defenseunicorns/uds-cli) - -GPU considerations (NVIDIA GPUs only): - -- NVIDIA GPU must have the most up-to-date drivers installed. -- NVIDIA GPU drivers compatible with CUDA (>=12.2). -- NVIDIA Container Toolkit is available via internet access, pre-installed, or on a mirrored package repository in the air gap. - -## Default Models -LeapfrogAI deploys with certain default models. The following models were selected to balance portability and performance for a base deployment: - -| Backend | CPU/GPU Support | Default Model | -|------------------|-----------------|------------------------------------------------------------------------------| -| llama-cpp-python | CPU | [SynthIA-7B-v2.0-GGUF](https://huggingface.co/TheBloke/SynthIA-7B-v2.0-GGUF) | -| vllm | GPU | [Synthia-7B-v2.0-GPTQ](https://huggingface.co/TheBloke/SynthIA-7B-v2.0-GPTQ) | -| text-embeddings | CPU/GPU | [Instructor-XL](https://huggingface.co/hkunlp/instructor-xl) | -| whisper | CPU/GPU | [OpenAI whisper-base](https://huggingface.co/openai/whisper-base) | - -**NOTE:** If a user's system specifications exceed the minimum requirements, advanced users are able to swap out the default model choices with larger or fine-tuned models. - -## Disclaimers - -The default configuration when deploying with GPU support assumes a single GPU. `vllm` is assigned the GPU resource. GPU workloads **_WILL NOT_** run if GPU resources are unavailable to the pod(s). You must provide sufficient NVIDIA GPU scheduling or else the pod(s) will go into a crash loop. - -If you have additional GPU resources, set `gpu_limit: ` in `uds-config.yaml`. The total number of GPUs in your configuration should be less than or equal to the number of GPUs for your hardware. - -If `vllm` is being used with: - -- A quantized model, then `QUANTIZATION` must be set to the quantization method (e.g., `awq`, `gptq`, etc.) -- Tensor parallelism for spreading a model's heads across multiple GPUs, then `TENSOR_PARALLEL_SIZE` must be set to an integer value that: - a) falls within the number of GPU resources (`nvidia.com/gpu`) that are allocatable in the cluster - b) divisible by the number of attention heads in the model architecture (if number of heads is 32, then `TENSOR_PARALLEL_SIZE` could be 2, 4, etc.) - -These `vllm` specific environment variables must be set at the model skeleton level or when the model is deployed into the cluster. - -## Instructions - -Start by cloning the [LeapfrogAI Repository](https://github.com/defenseunicorns/leapfrogai.git): - -``` bash -git clone https://github.com/defenseunicorns/leapfrogai.git -``` - -### CPU - -From within the cloned repository, deploy K3D and the LeapfrogAI bundle: - -``` bash -make create-uds-cpu-cluster - -cd uds-bundles/latest/cpu/ -uds create . -uds deploy uds-bundle-leapfrogai-*.tar.zst --confirm -``` - -### GPU - -In order to test the GPU deployment locally on K3d, use the following command when deploying UDS-Core: - -```bash - make build-k3d-gpu # build the image - make create-uds-gpu-cluster # create a uds cluster equipped with the k3d-gpu image - make test-uds-gpu-cluster # deploy a test gpu pod to see if everything is working - - cd uds-bundles/latest/gpu/ - uds create . - uds deploy uds-bundle-leapfrogai-*.tar.zst --confirm -``` - -## Checking Deployment -Once the cluster and LFAI have deployed, the cluster and pods can be inspected using uds: - -```bash -uds zarf tools monitor -``` - -The following URLs should now also be available to view LFAI resources: - -**DISCLAIMER**: These URls will only be available *after* both K3D-core and LFAI have been deployed. They will also only be available on the host system that deployed the cluster. - -| Tool | URL | -| ---------- | ------------------------------------- | -| UI | | -| API | | - -## Accessing the UI - -LeapfrogAI is integrated with the UDS Core KeyCloak service, which provides authentication via SSO. Below are general instructions for accessing the LeapfrogAI UI after a successful UDS deployment of UDS Core and LeapfrogAI. - -1. Connect to the KeyCloak admin panel - a. Run the following to get a port-forwarded tunnel: `uds zarf connect keycloak` - b. Go to the resulting localhost URL and create an admin account -2. Go to ai.uds.dev and press "Login using SSO" -3. Register a new user by pressing "Register Here" -4. Fill-in all of the information - a. The bot detection requires you to scroll and click around in a natural way, so if the Register button is not activated despite correct information, try moving around the page until the bot detection says 100% verified -5. Using an authenticator, follow the MFA steps -6. Go to sso.uds.dev - a. Login using the admin account you created earlier -7. Approve the newly registered user - a. Click on the hamburger menu in the top left to open/close the sidebar - b. Go to the dropdown that likely says "Keycloak" and switch to the "uds" context - c. Click "Users" in the sidebar - d. Click on the newly registered user's username - e. Go to the "Email Verified" switch and toggle it to be "Yes" - f. Scroll to the bottom and press "Save" -8. Go back to ai.uds.dev and login as the registered user to access the UI - -## Clean-up - -To clean-up or perform a fresh install, run the following commands in the context in which you had previously installed UDS Core and LeapfrogAI: - -```bash -k3d cluster delete uds # kills a running uds cluster -uds zarf tools clear-cache # clears the Zarf tool cache -rm -rf ~/.uds-cache # clears the UDS cache -docker system prune -a -f # removes all hanging containers and images -docker volume prune -f # removes all hanging container volumes -``` - -## References - -- [UDS-Core](https://github.com/defenseunicorns/uds-core) diff --git a/website/content/en/docs/local deploy guide/requirements.md b/website/content/en/docs/local deploy guide/requirements.md deleted file mode 100644 index 279ac1403..000000000 --- a/website/content/en/docs/local deploy guide/requirements.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Requirements -type: docs -weight: 4 ---- - -Prior to deploying LeapfrogAI, ensure that the following tools, packages, and requirements are met and present in your environment. - -## Tested Environments - -The following operating systems, hardware, architectures, and system specifications have been tested and validated for our deployment instructions: - -### Operating Systems - -- Ubuntu LTS (jammy) - - 22.04.2 - - 22.04.3 - - 22.04.4 - - 22.04.5 -- Pop!_OS 22.04 LTS -- MacOS Sonoma 14.x / ARM64 (CPU-based deployments only) - -### Hardware - -- 64 CPU cores (`Unknown Compute via Virtual Machine`) and ~250 GB RAM, no GPU. -- 32 CPU cores (`AMD Ryzen Threadripper PRO 5955WX`) and ~250 GB RAM, 2x `NVIDIA RTX A4000` (16Gb vRAM each). -- 64 CPU cores (`Intel Xeon Platinum 8358 CPU`) and ~200Gb RAM, 1x `NVIDIA RTX A10` (16Gb vRAM each). -- 10 CPU cores (`Apple M1 Pro`) and ~32 GB of free RAM, 1x `Apple M1 Pro`. -- 32 CPU cores (`13th Gen Intel Core i9-13900KF`) and ~190GB RAM, 1x `NVIDIA RTX 4090` (24Gb vRAM each). -- 2x 128 CPU cores (`AMD EPYC 9004`) and ~1.4Tb RAM, 8x `NVIDIA H100` (80Gb vRAM each). -- 32 CPU cores (`13th Gen Intel Core i9-13900HX`) and ~64Gb RAM, 1x `NVIDIA RTX 4070` (8Gb vRAM each). - -### Architecure - -- Linux/AMD64 -- Linux/ARM64 - - Differentiated instructions will be provided for two scenarios: "Internet Access" and "Isolated Network": - -- **Internet Access:** - - Indicates a system capable of fetching and executing remote dependencies from the internet. -- **Isolated Network:** - - Indicates a system that is isolated and lacks connectivity to external networks or remote repositories. - - Note that "Isolated Network" instructions are also compatible with devices that have internet access. - - For all "Isolated Network" installs, `wget`, `git` `clone` and `zarf package create` commands are assumed to have been completed prior to entering the isolated network. - - For "Isolated Network" installs, ensure files and binaries from these commands are stored on a removable media device and subsequently uploaded to the isolated machine. - - For specific tool versions, it is recommended to follow the "Isolated Network" instructions. - -## System Requirements - -- Standard Unix-based operating system installed. - - Some commands may need to be modified depending on your CLI and package manager. -- Have root `sudo su` access. - - Rootless mode details can be found in the [Docker documentation](https://docs.docker.com/engine/security/rootless/). - -Additional considerations are necessary for GPU deployments: - -- NVIDIA GPU must have the most up-to-date drivers installed. -- NVIDIA GPU drivers compatible with CUDA (>=12.2). -- NVIDIA Container Toolkit is available via internet access, pre-installed, or on a mirrored package repository in the air gap. - -## GPU Deployments - -- The speed and quality of LeapfrogAI, along with its hosted AI models, are significantly influenced by the availability of a robust GPU for offloading model layers. -- By default, each backend is configured to request 1x GPU device. -- Presently, these instructions do not support time-slicing or configuring multi-instance GPU setups. -- Over-scheduling GPU resources beyond their availability may result in the crash of backend pods. -- To prevent crashing, install backends as CPU-only if all available GPU devices are already allocated. - -## Additional User Information - -- All `cd` commands should be executed with respect to your project's working directory (PWD) within the development environment. Each new step should be considered as initiating from the root of that directory. -- For optimal organization, we recommend creating a new PWD named `/leapfrogai` in your home directory and consolidating all components there. -- In cases where a tagged version of a LeapfrogAI or Defense Unicorns release is not desired, the option to build an image from source prior to executing `zarf package create` is available: - -``` bash -docker build -t "ghcr.io/defenseunicorns/leapfrogai/:" . -# find and replace any manifests referencing the image tag (e.g., zarf.yaml, zarf-config.yaml, etc.) -zarf package create zarf-package--*.tar.zst -``` - -- When building your Docker image from source, it is advisable to re-tag and push these images to a local registry container. This practice enhances the efficiency of zarf package creation. Below is an example of how to accomplish this using our whisper backend: - -``` bash -docker run -d -p 5000:5000 --restart=always --name registry registry:2 -docker build -t ghcr.io/defenseunicorns/leapfrogai/whisper:0.4.0 . -docker tag ghcr.io/defenseunicorns/leapfrogai/whisper:0.4.0 localhost:5000/defenseunicorns/leapfrogai/whisper:0.4.0 -docker push localhost:5000/defenseunicorns/leapfrogai/whisper:0.4.0 -zarf package create --registry-override ghcr.io=localhost:5000 --set IMG=defenseunicorns/leapfrogai/whisper:0.4.0 -``` diff --git a/website/content/en/docs/local-deploy-guide/_index.md b/website/content/en/docs/local-deploy-guide/_index.md new file mode 100644 index 000000000..e5d8a1b19 --- /dev/null +++ b/website/content/en/docs/local-deploy-guide/_index.md @@ -0,0 +1,9 @@ +--- +title: Local Deployment Guide +type: docs +weight: 1 +--- + +This documentation serves as a comprehensive guide for users, providing instructions on the system requirements, setup, and deployment of LeapfrogAI to their local environment. Please note that the specified order of steps must be followed to ensure an efficient deployment process. + +This guide should only be used for demonstration purposes and does not provide instructions for production deployments of LeapfrogAI. diff --git a/website/content/en/docs/local-deploy-guide/components.md b/website/content/en/docs/local-deploy-guide/components.md new file mode 100644 index 000000000..4cd5c9153 --- /dev/null +++ b/website/content/en/docs/local-deploy-guide/components.md @@ -0,0 +1,61 @@ +--- +title: Components +type: docs +weight: 3 +--- + +## Components + +### LeapfrogAI API + +LeapfrogAI offers an API closely aligned with OpenAI's, facilitating seamless compatibility for tools developed with OpenAI/ChatGPT to operate seamlessly with a LeapfrogAI backend. The LeapfrogAI API is a Python API that exposes LLM backends, via FastAPI and gRPC, in the OpenAI API specification. + +### Backend + +LeapfrogAI offers several backends for a variety of use cases: + +| Backend | AMD64 Support | ARM64 Support | Cuda Support | Docker Ready | K8s Ready | Zarf Ready | +| --- | --- | --- | --- | --- | --- | --- | +| [llama-cpp-python](https://github.com/defenseunicorns/leapfrogai/tree/main/packages/llama-cpp-python) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [whisper](https://github.com/defenseunicorns/leapfrogai/tree/main/packages/whisper) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [text-embeddings](https://github.com/defenseunicorns/leapfrogai/tree/main/packages/text-embeddings) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [vllm](https://github.com/defenseunicorns/leapfrogai/tree/main/packages/vllm) | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | + +### Flavors + +Each component has different images and values that refer to a specific image registry and/or hardening source. These images are packaged using [Zarf Flavors](https://docs.zarf.dev/ref/examples/package-flavors/): + +1. `upstream`: uses upstream vendor images from open source container registries and repositories +2. 🚧 `registry1`: uses [IronBank hardened images](https://repo1.dso.mil/dsop) from the Repo1 harbor registry +3. 🚧 `unicorn`: uses [Chainguard hardened images](https://www.chainguard.dev/chainguard-images) from the Chainguard registry + +### Artifact Support + +LeapfrogAI contains built-in embeddings for RAG and transcription / translation solutions that can handle many different file types. Many of these capabilities are accessible via the LeapfrogAI API. The support artifact types are as follows: + +#### Transcription / Translation + +- All formats supported by `ffmpeg -formats`, e.g., `.mp3`, `.wav`, `.mp4`, etc. + +#### Embeddings for RAG + +- `.pdf` +- `.txt` +- `.html` +- `.htm` +- `.csv` +- `.md` +- `.doc` +- `.docx` +- `.xlsx` +- `.xls` +- `.pptx` +- `.ppt` + +### Software Development Kit + +The LeapfrogAI SDK offers a standardized collection of Protobuf and Python utilities designed to facilitate the implementation of backends and gRPC. Please see the [LeapfrogAI SDK](https://github.com/defenseunicorns/leapfrogai/tree/main/src/leapfrogai_sdk) sub-directory for the source code and details. + +### User Interface + +LeapfrogAI offers user-friendly interfaces tailored for common use-cases, including chat, summarization, and transcription, providing accessible options for users to initiate these tasks. Please see the [LeapfrogAI UI](https://github.com/defenseunicorns/leapfrogai/tree/main/src/leapfrogai_ui)GitHub repository for additional information. diff --git a/website/content/en/docs/local deploy guide/dependencies.md b/website/content/en/docs/local-deploy-guide/dependencies.md similarity index 64% rename from website/content/en/docs/local deploy guide/dependencies.md rename to website/content/en/docs/local-deploy-guide/dependencies.md index a67440d35..e6a050c3f 100644 --- a/website/content/en/docs/local deploy guide/dependencies.md +++ b/website/content/en/docs/local-deploy-guide/dependencies.md @@ -1,5 +1,5 @@ --- -title: Dependencies +title: Dependencies type: docs weight: 5 --- @@ -12,22 +12,24 @@ Follow the outlined steps to ensure that your device is configured to execute Le Ensure that the following tools and packages are present in your environment: -- [Jq](https://jqlang.github.io/jq/) -- [Docker](https://www.docker.com/get-started/) - [build-essential](https://packages.ubuntu.com/focal/build-essential) - [iptables](https://help.ubuntu.com/community/IptablesHowTo?action=show&redirect=Iptables) - [Git](https://git-scm.com/) - [procps](https://gitlab.com/procps-ng/procps) +- [Python 3.11](https://www.python.org/downloads/release/python-3116/) +- [Docker](https://docs.docker.com/engine/install/) +- [K3D](https://k3d.io/) +- [UDS CLI](https://github.com/defenseunicorns/uds-cli) -### Install pyenv +### Install PyEnv - Follow the installation instructions outlined in the [pyenv](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) repository to install Python 3.11.6. - If your installation process completes successfully but indicates missing packages such as `sqlite3`, execute the following command to install the required packages then proceed with the reinstallation of Python 3.11.6: -```git -sudo apt-get install build-essential zlib1g-dev libffi-dev -libssl-dev libbz2-dev libreadline-dev libsqlite3-dev -liblzma-dev libncurses-dev +```bash +sudo apt-get install build-essential zlib1g-dev libffi-dev \ + libssl-dev libbz2-dev libreadline-dev libsqlite3-dev \ + liblzma-dev libncurses-dev ``` ### Install Homebrew @@ -37,7 +39,7 @@ liblzma-dev libncurses-dev ### Install Docker - Follow the [instructions](https://docs.docker.com/engine/install/) to install Docker onto your system. -- For systems using an NVIDIA GPU, it is necessary to modify the Docker runtime to NVIDIA. Refer to the GPU instructions below for guidance on making this adjustment. +- Systems using an NVIDIA GPU must also follow the [GPU instructions below](#gpu-specific-instructions) ### Install Kubectl @@ -47,28 +49,22 @@ liblzma-dev libncurses-dev - Follow the [instructions](https://k3d.io/) to install k3d onto your system. -### Install Zarf +### Install UDS CLI -- Install [Zarf](https://zarf.dev/) using Homebrew: +- Follow the [instructions](https://github.com/defenseunicorns/uds-cli#install) to install UDS CLI onto your system. -```git -brew tap defenseunicorns/tap && brew install zarf -``` +- As Homebrew does not install packages to the root directory, it is advisable to manually add the `uds` binary to the root +- In cases where Docker is installed in a rootless configuration, certain systems may encounter container access issues if Docker is not executed with root privileges +- To install `uds` as root, execute the following command in your terminal and ensure that the version number is replaced with the most recent [release](https://github.com/defenseunicorns/uds-cli/releases): -- As Homebrew does not install packages to the root directory, it is advisable to manually add the `zarf` binary to the root. Even in cases where Docker is installed in a rootless configuration, certain systems may encounter container access issues if Docker is not executed with root privileges. -- To install as root, execute the following command in your terminal and ensure that the version number is replaced with the most recent [release](https://github.com/zarf-dev/zarf/releases): - -```git -# switch to sudo -sudo su -# download and store on removable media -wget https://github.com/defenseunicorns/ uds-cli /releases/download/v0. 9.0/ uds-cli _v0. 9.0 _Linux_amd64 -# upload from removable media and install -mv uds-cli_v0.9.0_Linux_amd64 /bin/uds -chmod +x /bin/uds +```bash +# where $UDS_VERSION is the latest UDS CLI release +wget -O uds https://github.com/defenseunicorns/uds-cli/releases/download/$UDS_VERSION/uds-cli_$UDS_VERSION_Linux_amd64 && \ + sudo chmod +x uds && \ + sudo mv uds /usr/local/bin/ ``` -## GPU Specific Intructions +## GPU Specific Instructions LeapfrogAI exclusively supports NVIDIA GPUs at this point in time. The following instructions are tailored for users utilizing an NVIDIA GPU. @@ -84,19 +80,16 @@ LeapfrogAI exclusively supports NVIDIA GPUs at this point in time. The following ### NVIDIA Container Toolkit - Follow the [instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installing-with-apt) to download the NVIDIA container toolkit (>=1.14). -- After the successful installation of the toolkit, follow the [toolkit instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installing-with-apt) to verify that your default Docker runtime is configured for NVIDIA. +- After the successful installation off the toolkit, follow the [toolkit instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installing-with-apt) to verify that your default Docker runtime is configured for NVIDIA. +- Configure Docker to use the `nvidia` runtime by default by adding the `--set-as-default` flag during the container toolkit post-installation configuration step - Verify that the default runtime is changed by running the following command: -```git -docker info | grep "Default Runtime" -``` + ```bash + docker info | grep "Default Runtime" + ``` - The expected output should be similar to: `Default Runtime: nvidia`. -### GPU Support Test - -- Test that your GPU is visible through Docker by deploying the [GPU Support Test](https://github.com/justinthelaw/gpu-support-test). - ### Deploy LeapfrogAI - After ensuring that all system dependencies and requirements are fulfilled, refer to the LeapfrogAI deployment guide for comprehensive instructions on deploying LeapfrogAI within your local environment. diff --git a/website/content/en/docs/local-deploy-guide/quick_start.md b/website/content/en/docs/local-deploy-guide/quick_start.md new file mode 100644 index 000000000..bfb692c97 --- /dev/null +++ b/website/content/en/docs/local-deploy-guide/quick_start.md @@ -0,0 +1,180 @@ +--- +title: Quick Start +type: docs +weight: 2 +--- + +# LeapfrogAI UDS Deployment + +The fastest and easiest way to get started with a deployment of LeapfrogAI is by using [UDS](https://github.com/defenseunicorns/uds-core). These quick start instructions show how to deploy LeapfrogAI in either a CPU or GPU-enabled environment. + +## Pre-Requisites + +See the [Dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/) and [Requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) pages for more details. + +## Default Models + +LeapfrogAI deploys with certain default models. The following models were selected to balance portability and performance for a base deployment: + +| Backend | CPU/GPU Support | Default Model | +| ------------------ | ----------------- | ------------------------------------------------------------------------------ | +| llama-cpp-python | CPU | [SynthIA-7B-v2.0-GGUF](https://huggingface.co/TheBloke/SynthIA-7B-v2.0-GGUF) | +| vllm | GPU | [Synthia-7B-v2.0-GPTQ](https://huggingface.co/TheBloke/SynthIA-7B-v2.0-GPTQ) | +| text-embeddings | CPU/GPU | [Instructor-XL](https://huggingface.co/hkunlp/instructor-xl) | +| whisper | CPU/GPU | [OpenAI whisper-base](https://huggingface.co/openai/whisper-base) | + +If a user's system specifications exceed the [minimum requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/), advanced users are able to swap out the default model choices with larger or fine-tuned models. + +Examples of other models to put into vLLM or LLaMA C++ Python that are not sponsored nor owned by Defense Unicorns include: + +- [defenseunicorns/Hermes-2-Pro-Mistral-7B-4bit-32g](https://huggingface.co/defenseunicorns/Hermes-2-Pro-Mistral-7B-4bit-32g) +- [hugging-quants/Meta-Llama-3.1-70B-Instruct-AWQ-INT4](https://huggingface.co/hugging-quants/Meta-Llama-3.1-70B-Instruct-AWQ-INT4) +- [justinthelaw/Phi-3-mini-128k-instruct-4bit-128g](https://huggingface.co/justinthelaw/Phi-3-mini-128k-instruct-4bit-128g) +- [NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO-GGUF](https://huggingface.co/NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO-GGUF) + +> [!CAUTION] +> The default configuration when deploying with GPU support assumes a single GPU. `vllm` is assigned the GPU resource. GPU workloads **_WILL NOT_** run if GPU resources are unavailable to the pod(s). You must provide sufficient NVIDIA GPU scheduling or else the pod(s) will go into a crash loop. See the [Dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/) and [Requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) pages for more details. + +## Building the UDS Bundle + +The following instructions are split into two sections: + +1. [LeapfrogAI Latest](#leapfrogai-latest): for hassle-free deployment of the latest stable version of LeapfrogAI +2. [LeapfrogAI Development](#leapfrogai-development): for deployment of a unreleased branch, a fork or `main` + +If you already have a pre-built UDS bundle, please skip to [Deploying the UDS Bundle](#deploying-the-uds-bundle) + +If you are using MacOS, please skip to [MacOS Specific Instructions](#macos-specifics) + +### LeapfrogAI Latest + +1. Start by cloning the [LeapfrogAI Repository](https://github.com/defenseunicorns/leapfrogai): + + ``` bash + git clone https://github.com/defenseunicorns/leapfrogai.git + ``` + +2. From within the cloned repository create the LeapfrogAI bundle using **ONE** of the following: + + ```bash + cd bundles/latest/cpu/ + uds create . + uds deploy uds-bundle-leapfrogai-*.tar.zst --confirm + + cd bundles/latest/gpu/ + uds create . + uds deploy uds-bundle-leapfrogai-*.tar.zst --confirm + ``` + +3. Move on to [Deploying the UDS Bundle](#deploying-the-uds-bundle) + +### LeapfrogAI Development + +1. For ease, it's best to create a virtual environment for installing, managing and isolating package creation dependencies: + + ```bash + python -m venv .venv + source .venv/bin/activate + ``` + +2. Install all the necessary package creation dependencies: + + ```bash + python -m pip install "hugging_face[cli,hf_transfer]" "transformers[torch]" ctranslate2 + ``` + +3. Build all of the packages you need at once with **ONE** of the following `Make` targets: + + ```bash + LOCAL_VERSION=dev make build-cpu # ui, api, llama-cpp-python, text-embeddings, whisper, supabase + # OR + LOCAL_VERSION=dev make build-gpu # ui, api, vllm, text-embeddings, whisper, supabase + # OR + LOCAL_VERSION=dev make build-all # all of the components + ``` + + **OR** + + You can build components individually using the following `Make` targets: + + ```bash + LOCAL_VERSION=dev make build-ui + LOCAL_VERSION=dev make build-api + LOCAL_VERSION=dev make build-supabase + LOCAL_VERSION=dev make build-vllm # if you have GPUs (macOS not supported) + LOCAL_VERSION=dev make build-llama-cpp-python # if you have CPU only + LOCAL_VERSION=dev make build-text-embeddings + LOCAL_VERSION=dev make build-whisper + ``` + +## MacOS Specifics + +To run the same commands in MacOS, you will need to prepend your command with a couple of env vars like so: + +**All Macs:** `REG_PORT=5001` + +**Apple Silicon (M1/M2/M3/M4 series) Macs:** `ARCH=arm64` + +To demonstrate what this would look like for an Apple Silicon Mac: + +``` shell +REG_PORT=5001 ARCH=arm64 LOCAL_VERSION=dev make build-cpu +``` + +To demonstrate what this would look like for an older Intel Mac: + +``` shell +REG_PORT=5001 LOCAL_VERSION=dev make build-cpu +``` + +## Deploying the UDS bundle + +1. Deploy a UDS Kubernetes cluster with **ONE** of the following: + + ```bash + make create-uds-cpu-cluster # if you have CPUs only + # OR + make create-uds-gpu-cluster # if you have GPUs (macOS not supported) + ``` + +2. Deploy the bundle you created in the [previous steps](#building-the-uds-bundle): + + ```bash + # make sure you ar ein the directory with the UDS bundle archive + uds deploy uds-bundle-leapfrogai*.tar.zst + ``` + +## Checking Deployment + +Once the cluster and LFAI have deployed, the cluster and pods can be inspected using uds: + +```bash +uds zarf tools monitor +``` + +These URLs will only be accessible *after* the UDS Kubernetes cluster and LeapfrogAI have been deployed: + +| Tool | URL | +| --------------------- | ------------------------------------- | +| LeapfrogAI UI | | +| LeapfrogAI API | | +| Supabase Console | | +| KeyCloak User Page | | +| KeyCloak Admin Panel | | + +## Clean-up + +To clean-up or perform a fresh install, run the following commands in the context in which you had previously installed UDS Core and LeapfrogAI: + +```bash +k3d cluster delete uds # kills a running uds cluster +uds zarf tools clear-cache # clears the Zarf tool cache +rm -rf ~/.uds-cache && rm -rf /tmp/zarf-* # clears the UDS and Zarf temporary files +docker system prune -a -f # removes all hanging containers and images +docker volume prune -f # removes all hanging container volumes +``` + +## References + +- [UDS Core](https://github.com/defenseunicorns/uds-core) +- [UDS CLI](https://github.com/defenseunicorns/uds-cli) diff --git a/website/content/en/docs/local-deploy-guide/requirements.md b/website/content/en/docs/local-deploy-guide/requirements.md new file mode 100644 index 000000000..1df1fb0ef --- /dev/null +++ b/website/content/en/docs/local-deploy-guide/requirements.md @@ -0,0 +1,51 @@ +--- +title: Requirements +type: docs +weight: 4 +--- + +Prior to deploying LeapfrogAI, ensure that the following tools, packages, and requirements are met and present in your environment. See the [Dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/) page fro more details. + +## System Requirements + +Please review the following table to ensure your system meets the minimum requirements. GPU requirements only apply when your system is capable of deploying a GPU-accelerated version of the LeapfrogAI stack. + +| | Minimum | Recommended (Performance) | +|------|--------------------|---------------------------| +| DISK | 256 GB | 1 TB | +| RAM | 32 GB | 128 GB | +| CPU | 8 Cores @ 3.0 GHz | 32 Cores @ 3.0 GHz | +| GPU | N/A | 2x NVIDIA RTX 4090 GPUs | + +## Tested Environments + +The following operating systems, hardware, architectures, and system specifications have been tested and validated for our deployment instructions: + +### Operating Systems + +- Ubuntu LTS + - 22.04.2 + - 22.04.3 + - 22.04.4 + - 22.04.5 +- Ubuntu + - 20.04.6 +- Pop!_OS LTS + - 22.04.x +- MacOS Sonoma / ARM64 (CPU-only) + - 14.x + +### Hardware + +- 64 CPU cores (`Unknown Compute via Virtual Machine`) and ~250 GB RAM, no GPU. +- 32 CPU cores (`AMD Ryzen Threadripper PRO 5955WX`) and ~250 GB RAM, 2x `NVIDIA RTX A4000` (16Gb vRAM each). +- 64 CPU cores (`Intel Xeon Platinum 8358 CPU`) and ~200Gb RAM, 1x `NVIDIA RTX A10` (16Gb vRAM each). +- 10 CPU cores (`Apple M1 Pro`) and ~32 GB of free RAM, 1x `Apple M1 Pro`. +- 32 CPU cores (`13th Gen Intel Core i9-13900KF`) and ~190GB RAM, 1x `NVIDIA RTX 4090` (24Gb vRAM each). +- 2x 128 CPU cores (`AMD EPYC 9004`) and ~1.4Tb RAM, 8x `NVIDIA H100` (80Gb vRAM each). +- 32 CPU cores (`13th Gen Intel Core i9-13900HX`) and ~64Gb RAM, 1x `NVIDIA RTX 4070` (8Gb vRAM each). + +### Architectures + +- Linux/AMD64 +- Linux/ARM64 diff --git a/website/content/en/docs/prod deployment/_index.md b/website/content/en/docs/production-guide/_index.md similarity index 66% rename from website/content/en/docs/prod deployment/_index.md rename to website/content/en/docs/production-guide/_index.md index eba9b1810..23263d101 100644 --- a/website/content/en/docs/prod deployment/_index.md +++ b/website/content/en/docs/production-guide/_index.md @@ -5,4 +5,4 @@ weight: 1 draft: true --- -## Overview +## 🚧 _**UNDER CONSTRUCTION**_ 🚧 diff --git a/website/hugo.toml b/website/hugo.toml index f17719b46..85ce051b5 100644 --- a/website/hugo.toml +++ b/website/hugo.toml @@ -15,8 +15,8 @@ uglyURLs = false # project-relative or absolute and even a symbolic link. For other modules it must be # project-relative. # target -# Where it should be mounted into Hugo’s virtual filesystem. It must start with one of -# Hugo’s component folders: static, content, layouts, data, assets, i18n, or archetypes. +# Where it should be mounted into Hugo's virtual filesystem. It must start with one of +# Hugo's component folders: static, content, layouts, data, assets, i18n, or archetypes. # E.g. content/blog. # [[module.mounts]] @@ -49,9 +49,9 @@ proxy = "direct" archived_version = false copyright = "Defense Unicorns" - github_project_repo = "https://github.com/defenseunicorns/leapfrogai-docs" - github_repo = "https://github.com/defenseunicorns/leapfrogai-docs" - version = "v1.0.0" + github_project_repo = "https://github.com/defenseunicorns/leapfrogai" + github_repo = "https://github.com/defenseunicorns/leapfrogai" + version = "v0.10.0" # version_menu = "v1" # url_latest_version = "https://latest-version" From 93a2624c2ad03fdf76d927cbd1d432f176e7cb06 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Wed, 14 Aug 2024 11:59:43 -0400 Subject: [PATCH 02/30] update main README with more context --- README.md | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 191641e63..13e8b40d6 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,33 @@ Please refer to the [Quick Start](https://docs.leapfrog.ai/docs/local-deploy-gui LeapfrogAI provides an API that closely matches that of OpenAI's. This feature allows tools that have been built with OpenAI/ChatGPT to function seamlessly with a LeapfrogAI backend. +### SDK + +The LeapfrogAI [SDK](src/leapfrogai_sdk/) provides a standard set of protobufs and Python utilities for implementing backends with gRPC. + +### UI + +LeapfrogAI provides a [UI](src/leapfrogai_ui/) with support for common use-cases such as general chat and "Q&A with your documents". + ### Backends LeapfrogAI provides several backends for a variety of use cases. Backends support and compatibility matrix: -| Backend | AMD64 | ARM64 | CUDA | Docker | Kubernetes | UDS | -| ---------------------------------------------- | ------ | ------ | ------ | ------ | ---------- | ------- | +| Backend | AMD64 | ARM64 | CUDA | Docker | Kubernetes | UDS | +| ---------------------------------------------- | ------- | ------- | ------ | ------ | ---------- | ------- | | [llama-cpp-python](packages/llama-cpp-python/) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [whisper](packages/whisper/) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [text-embeddings](packages/text-embeddings/) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| [vllm](packages/vllm/) | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | +| [vllm](packages/vllm/) | ✅ | ❌[^1] | ✅ | ✅ | ✅ | ✅ | + +[^1]: vLLM requires a CUDA-enabled PyTorch built for ARM64, which is not available via pip or conda #### Repeater The [repeater](packages/repeater/) "model" is a basic "backend" that parrots all inputs it receives back to the user. It is built out the same way all the actual backends are and it primarily used for testing the API. -### SDK - -The LeapfrogAI [SDK](src/leapfrogai_sdk/) provides a standard set of protobufs and Python utilities for implementing backends with gRPC. - -### UI - -LeapfrogAI provides a [UI](src/leapfrogai_ui/) with support for common use-cases such as general chat and "Q&A with your documents". - ## Usage To build a LeapfrogAI UDS bundle and deploy it, please refer to the [LeapfrogAI Documentation Website](https://docs.leapfrog.ai/docs/). In the documentation website, you'll find system requirements and instructions for all things LeapfrogAI that aren't associated to local development and contributing. @@ -113,14 +115,19 @@ Each of the LeapfrogAI components can also be run individually outside of a Kube Please refer to the linked READMEs for each individual packages local development instructions: -- [API](/src/leapfrogai_api/README.md) -- [LLaMA C++ Python](/packages/llama-cpp-python/README.md) -- [vLLM](/packages/vllm/README.md) -- [Supabase](/packages/supabase/README.md) -- [Text Embeddings](/packages/text-embeddings/README.md) -- [UI](/src/leapfrogai_ui/README.md) -- [Faster Whisper](/packages/whisper/README.md) -- [Repeater](/packages/repeater/README.md) +- [API](src/leapfrogai_api/README.md)[^2] +- [SDK](src/leapfrogai_sdk/README.md)[^3] +- [UI](src/leapfrogai_ui/README.md)[^2] +- [LLaMA C++ Python](packages/llama-cpp-python/README.md) +- [vLLM](packages/vllm/README.md) +- [Supabase](packages/supabase/README.md) +- [Text Embeddings](packages/text-embeddings/README.md) +- [Faster Whisper](packages/whisper/README.md) +- [Repeater](packages/repeater/README.md) + +[^2]: Please be aware that the API and UI have artifacts under 2 sub-directories. The sub-directories related to `packages/` are focused on the Zarf packaging and Helm charts, whereas the sub-directories related to `src/` contains the actual source code and development instructions. + +[^3]: The SDK is not a functionally independent unit, and only becomes a functional unit when combined and packaged with the API and Backends as a dependency. ## Contributing From 7a256c99ba8b92a3cb728b707b524b707c9fed3d Mon Sep 17 00:00:00 2001 From: Justin Law Date: Wed, 14 Aug 2024 12:13:06 -0400 Subject: [PATCH 03/30] revert LICENSE changes --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 8dd2f8876..1bf24df1d 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2024 Defense Unicorns + Copyright [2023] [Defense Unicorns] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 1e2382a88cb43c644c8419bd1bca3cfcb6f76f3d Mon Sep 17 00:00:00 2001 From: Justin Law Date: Wed, 14 Aug 2024 14:08:30 -0400 Subject: [PATCH 04/30] add DEVELOPMENT.md --- README.md | 8 +- docs/DEVELOPMENT.md | 156 ++++++++++++++++++ .../docs/local-deploy-guide/dependencies.md | 2 + 3 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 docs/DEVELOPMENT.md diff --git a/README.md b/README.md index 13e8b40d6..dfe513569 100644 --- a/README.md +++ b/README.md @@ -86,9 +86,7 @@ LeapfrogAI provides a [UI](src/leapfrogai_ui/) with support for common use-cases ### Backends -LeapfrogAI provides several backends for a variety of use cases. - -Backends support and compatibility matrix: +LeapfrogAI provides several backends for a variety of use cases. Below is the backends support and compatibility matrix: | Backend | AMD64 | ARM64 | CUDA | Docker | Kubernetes | UDS | | ---------------------------------------------- | ------- | ------- | ------ | ------ | ---------- | ------- | @@ -111,9 +109,9 @@ For contributing and local deployment and development for each component in a lo ## Local Development -Each of the LeapfrogAI components can also be run individually outside of a Kubernetes or Containerized environment. This is useful when testing changes to a specific component, but will not assist in a full deployment of LeapfrogAI. Please refer to the [above section](#usage) for deployment instructions. +Each of the LeapfrogAI components can also be run individually outside of a Kubernetes or Containerized environment. This is useful when testing changes to a specific component, but will not assist in a full deployment of LeapfrogAI. Please refer to the [above section](#usage) for deployment instructions. Please refer to the [next section](#contributing) for rules on contributing to LeapfrogAI. -Please refer to the linked READMEs for each individual packages local development instructions: +**_First_** refer to the [DEVELOPMENT.md](docs/DEVELOPMENT.md) document for general development details, **_then_** refer to the linked READMEs for each individual packages local development instructions: - [API](src/leapfrogai_api/README.md)[^2] - [SDK](src/leapfrogai_sdk/README.md)[^3] diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 000000000..064021cb2 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,156 @@ +# Development + +> [!IMPORTANT] +> Please read the entirety of the root [README.md](../README.md) and the [LeapfrogAI documentation website](https://docs.leapfrog.ai/docs/local-deploy-guide/quick_start/) prior to reading this document. Also, please refer to the [CONTRIBUTING.md](../.github/CONTRIBUTING.md) for rules on contributing to the LeapfrogAI project. + +The purpose of this document is to describe how to run a development loop on the LeapfrogAI tech stack. Specifics for each component are within the sub-directories identified in the root [README.md](../README.md). + +## Local Development + +Please first see the pre-requisites listed on the LeapfrogAI documentation website's [Requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) and [Dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/) + +## UDS CLI Aliasing + +Below are instructions for adding UDS CLI aliases that are useful for deployments that occur in an air-gap where only the UDS CLI binary available to the engineer. + +For general CLI UX, put the following in your shell configuration (e.g., `/root/.bashrc`, `~/.zshrc`): + +```bash +alias k="uds zarf tools kubectl" +alias kubectl="uds zarf tools kubectl" +alias zarf='uds zarf' +alias k9s='uds zarf tools monitor' +alias udsclean="uds zarf tools clear-cache && rm -rf ~/.uds-cache && rm -rf /tmp/zarf-*" +``` + +For fulfilling `kubectl` binary requirements necessary for running some of the _optional_ deployment helper scripts and for full functionality within `uds zarf tools monitor`: + +```bash +touch /usr/local/bin/kubectl +echo -e '#!/bin/bash\nuds zarf tools kubectl "$@"' > /usr/local/bin/kubectl +chmod +x /usr/local/bin/kubectl +``` + +## Package Development + +If you don't want to build an entire bundle, or you want to dev-loop on a single package in an existing, Zarf-init'd cluster, you can do so by performing a `uds zarf package remove [PACKAGE_NAME]` and re-deploying the package into the cluster. + +For example, this is how you build and deploy a local DEV version of a package: + +```bash +# if package is already in the cluster, and you are deploying a new one +uds zarf package remove leapfrogai-api --confirm +uds zarf tools registry prune --confirm + +# create and deploy the new package +LOCAL_VERSION=dev REGISTRY_PORT=5000 ARCH=amd64 make build-api +LOCAL_VERSION=dev REGISTRY_PORT=5000 ARCH=amd64 make deploy-api +``` + +For example, this is how you pull and deploy a LATEST version of a package: + +```bash +# pull and deploy latest versions +uds zarf package pull oci://ghcr.io/defenseunicorns/leapfrogai/leapfrogai-api:latest -a amd64 +uds zarf package deploy zarf-package-*.tar.zst --confirm +``` + +## Troubleshooting + +Occasionally, a package you are trying to re-deploy, or a namespace you are trying to delete, may hang. To workaround this, be sure to check the events and logs of all resources, to include pods, deployments, daemonsets, clusterpolicies, etc. There may be finalizers, Pepr hooks, and etc. causing the re-deployment or deletion to fail. Use the [`k9s`](https://k9scli.io/topics/commands/) and `kubectl` tools that are vendored with UDS CLI, like in the examples below: + +### Clusters + +```bash +# k9s CLI for debugging +uds zarf tools monitor + +# kubectl command for logs +uds zarf tools kubectl logs -l app=api -n leapfrogai --all-containers=true --follow +``` + +To describe node-level data, like resource usage, non-terminated pods, taints, etc. run the following command: + +```bash +uds zarf tools kubectl describe node +``` + +### NVIDIA GPUs + +#### NVML Errors or Missing CUDA Dependencies + +None of the following should ever error or return `unknown version`: + +1. Check if your NVIDIA GPU drivers are installed: + + ```bash + nvidia-smi + ``` + +2. Check the version of your NVIDIA Container Toolkit: + + ```bash + nvidia-ctk --version + ``` + +3. Check the version of your CUDA Toolkit (if compiling vLLM locally): + + ```bash + nvcc --version + ``` + +Try looking at your Docker runtime information and make sure the following returns with several lines of information: + +```bash +docker info | grep "nvidia" +``` + +Try running the CUDA sample tests in the cluster: [CUDA Vector Add](../packages/k3d-gpu/test/cuda-vector-add.yaml). This can be deployed by executing the following on an existing cluster with NVIDIA GPU operator and/or NVIDIA device plugin daemonset installed: + +```bash +uds zarf tools kubectl apply packages/k3d-gpu/test/cuda-vector-add.yaml +``` + +#### Memory Errors or Process Locks + +If you are, + +1. not deploying a fresh cluster or fresh packages (e.g., vLLM is already deployed), or +2. you have a GPU that has other workloads on it (e.g., display) + +then there may not be enough resources to offload the model weights to the NVIDIA GPU. + +To see what host-level processes are on your NVIDIA GPU(s) run the following: + +```bash +nvidia-smi +``` + +To check which pods are sucking up GPUs in particular, you can run the following `yq` command: + +```bash +uds zarf tools kubectl get pods \ +--all-namespaces \ +--output=yaml \ +| uds zarf tools yq eval -o=json ' + ["Pod", "Namespace", "Container", "GPU"] as $header | + [$header] + [ + .items[] | + .metadata as $metadata | + .spec.containers[] | + select(.resources.requests["nvidia.com/gpu"]) | + [ + $metadata.name, + $metadata.namespace, + .name, + .resources.requests["nvidia.com/gpu"] + ] + ]' - \ +| uds zarf tools yq -r '(.[0] | @tsv), (.[1:][] | @tsv)' \ +| column -t -s $'\t' +``` + +When you reinstall or start a new GPU-dependent pod, the previous PID (process) on the GPU may not have been flushed yet. + +1. Scale the previous GPU-dependent pod deployment down to 0, as the current `RollingUpdate` strategy for vLLM relies on back-up/secondary GPUs to be available for a graceful turnover +2. Use `nvidia-smi` to check if the process has been flushed upon Pod termination BEFORE you deploy a new GPU-dependent pod, and if not, use `kill -9 ` to manually flush the process diff --git a/website/content/en/docs/local-deploy-guide/dependencies.md b/website/content/en/docs/local-deploy-guide/dependencies.md index e6a050c3f..459506488 100644 --- a/website/content/en/docs/local-deploy-guide/dependencies.md +++ b/website/content/en/docs/local-deploy-guide/dependencies.md @@ -68,6 +68,8 @@ wget -O uds https://github.com/defenseunicorns/uds-cli/releases/download/$UDS_VE LeapfrogAI exclusively supports NVIDIA GPUs at this point in time. The following instructions are tailored for users utilizing an NVIDIA GPU. +If you are experiencing issues even after carefully following the instructions below, please refer to the [Developer Documentation](https://github.com/defenseunicorns/leapfrogai/tree/main/docs/DEVELOPMENT.md) troubleshooting section in the GitHub repository. + ### NVIDIA Drivers - Ensure that the proper [NVIDIA drivers](https://www.nvidia.com/download/index.aspx) are installed (>=525.60). From 54bcca5382ee6ad320dcbfd726ae7c14992d473d Mon Sep 17 00:00:00 2001 From: Justin Law Date: Wed, 14 Aug 2024 18:34:14 -0400 Subject: [PATCH 05/30] revert bundle move, add new pre-commits --- .pre-commit-config.yaml | 67 +++++++++++++++---- .../dev/cpu/uds-bundle.yaml | 0 .../dev/cpu/uds-config.yaml | 0 .../dev/gpu/uds-bundle.yaml | 0 .../dev/gpu/uds-config.yaml | 0 .../latest/cpu/uds-bundle.yaml | 0 .../latest/cpu/uds-config.yaml | 0 .../latest/gpu/uds-bundle.yaml | 0 .../latest/gpu/uds-config.yaml | 0 9 files changed, 55 insertions(+), 12 deletions(-) rename {bundles => uds-bundles}/dev/cpu/uds-bundle.yaml (100%) rename {bundles => uds-bundles}/dev/cpu/uds-config.yaml (100%) rename {bundles => uds-bundles}/dev/gpu/uds-bundle.yaml (100%) rename {bundles => uds-bundles}/dev/gpu/uds-config.yaml (100%) rename {bundles => uds-bundles}/latest/cpu/uds-bundle.yaml (100%) rename {bundles => uds-bundles}/latest/cpu/uds-config.yaml (100%) rename {bundles => uds-bundles}/latest/gpu/uds-bundle.yaml (100%) rename {bundles => uds-bundles}/latest/gpu/uds-config.yaml (100%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3e6df886e..4a094ae16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,32 +1,75 @@ repos: - # Generic pre-commit checks + ################ + # GENERAL CHECKS + ################ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: - id: check-added-large-files + name: Large Files Check args: ["--maxkb=1024"] - - id: check-merge-conflict + - id: detect-aws-credentials + name: Check AWS Credentials args: - "--allow-missing-credentials" + - id: detect-private-key + name: Check Private Keys + + - id: check-merge-conflict + name: Merge Conflict Resolution Check + - id: end-of-file-fixer + name: Newline EOF Checker + - id: fix-byte-order-marker + name: Fix UTF-8 byte order marker + - id: trailing-whitespace + name: Whitespace Cleaning Check args: [--markdown-linebreak-ext=md] - # Python linting and formatting + - repo: https://github.com/scop/pre-commit-shfmt + rev: v3.8.0-1 + hooks: + - id: shfmt + name: Shell Script Format + + - repo: https://github.com/gitleaks/gitleaks + rev: v8.18.0 + hooks: + - id: gitleaks + name: GitLeaks Checks + + - repo: https://github.com/sirosen/fix-smartquotes + rev: 0.2.0 + hooks: + - id: fix-smartquotes + name: Fix Quotes + + ############ + # CODE LINT + ############ + + - repo: https://github.com/DavidAnson/markdownlint-cli2 + rev: v0.12.1 + hooks: + - id: markdownlint-cli2 + name: Markdown Linti + + - repo: local + hooks: + - id: eslint + name: ESLint + language: system + entry: sh -c 'npm --prefix src/leapfrogai_ui/ run lint' + files: \.(js|jsx|ts|tsx|svelte|cjs|mjs)$ # *.js, *.jsx, *.ts, *.tsx, *.svelte, *.cjs, *.mjs + - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.3.4 hooks: - id: ruff # Run the linter. + name: Ruff Lint - id: ruff-format # Run the formatter. - - # Local Eslint w/ Prettier - - repo: local - hooks: - - id: eslint - name: eslint - language: system - entry: sh -c 'npm --prefix src/leapfrogai_ui/ run lint' - files: \.(js|jsx|ts|tsx|svelte|cjs|mjs)$ # *.js, *.jsx, *.ts, *.tsx, *.svelte, *.cjs, *.mjs + name: Ruff Format diff --git a/bundles/dev/cpu/uds-bundle.yaml b/uds-bundles/dev/cpu/uds-bundle.yaml similarity index 100% rename from bundles/dev/cpu/uds-bundle.yaml rename to uds-bundles/dev/cpu/uds-bundle.yaml diff --git a/bundles/dev/cpu/uds-config.yaml b/uds-bundles/dev/cpu/uds-config.yaml similarity index 100% rename from bundles/dev/cpu/uds-config.yaml rename to uds-bundles/dev/cpu/uds-config.yaml diff --git a/bundles/dev/gpu/uds-bundle.yaml b/uds-bundles/dev/gpu/uds-bundle.yaml similarity index 100% rename from bundles/dev/gpu/uds-bundle.yaml rename to uds-bundles/dev/gpu/uds-bundle.yaml diff --git a/bundles/dev/gpu/uds-config.yaml b/uds-bundles/dev/gpu/uds-config.yaml similarity index 100% rename from bundles/dev/gpu/uds-config.yaml rename to uds-bundles/dev/gpu/uds-config.yaml diff --git a/bundles/latest/cpu/uds-bundle.yaml b/uds-bundles/latest/cpu/uds-bundle.yaml similarity index 100% rename from bundles/latest/cpu/uds-bundle.yaml rename to uds-bundles/latest/cpu/uds-bundle.yaml diff --git a/bundles/latest/cpu/uds-config.yaml b/uds-bundles/latest/cpu/uds-config.yaml similarity index 100% rename from bundles/latest/cpu/uds-config.yaml rename to uds-bundles/latest/cpu/uds-config.yaml diff --git a/bundles/latest/gpu/uds-bundle.yaml b/uds-bundles/latest/gpu/uds-bundle.yaml similarity index 100% rename from bundles/latest/gpu/uds-bundle.yaml rename to uds-bundles/latest/gpu/uds-bundle.yaml diff --git a/bundles/latest/gpu/uds-config.yaml b/uds-bundles/latest/gpu/uds-config.yaml similarity index 100% rename from bundles/latest/gpu/uds-config.yaml rename to uds-bundles/latest/gpu/uds-config.yaml From 8af05d941c2437f043a6a462871179a9d0514def Mon Sep 17 00:00:00 2001 From: Justin Law Date: Wed, 14 Aug 2024 18:42:40 -0400 Subject: [PATCH 06/30] initial package level markdown organization --- packages/api/README.md | 43 ++++++++++++++++++- packages/k3d-gpu/README.md | 6 ++- packages/llama-cpp-python/README.md | 14 +++---- packages/repeater/README.md | 13 +++--- packages/supabase/README.md | 37 +++++++++++------ packages/text-embeddings/README.md | 12 +++--- packages/vllm/README.md | 10 ++--- packages/whisper/README.md | 11 +++-- src/leapfrogai_api/README.md | 46 +++++++++++++-------- src/leapfrogai_ui/README.md | 64 ++++++++++++++--------------- 10 files changed, 161 insertions(+), 95 deletions(-) diff --git a/packages/api/README.md b/packages/api/README.md index ea0b6eeb6..02e1f6b30 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -4,4 +4,45 @@ A Python API that exposes LLM backends, via FastAPI and gRPC, in the [OpenAI API ## Usage -:construction_worker: This documentation is still under construction. :construction_worker: \ No newline at end of file +See [instructions](#instructions) to get the backend up and running. + +### Instructions + +The instructions in this section assume the following: + +1. Properly installed and configured Python 3.11.x, to include its development tools + +### Zarf Package Deployment + +To build and deploy just the llama-cpp-python Zarf package (from the root of the repository): + +> Deploy a [UDS cluster](/README.md#uds) if one isn't deployed already + +```bash +make build-api LOCAL_VERSION=dev +uds zarf package deploy packages/api/zarf-package-leapfrogai-api-*-dev.tar.zst --confirm +``` + +### Local Development + +To run the API locally (starting from the root directory of the repository): + +From this directory: + +```bash +# Setup Virtual Environment +python -m venv .venv +source .venv/bin/activate +``` + +```bash +# Install dependencies +python -m pip install src/leapfrogai_sdk +cd packages/api +python -m pip install ".[dev]" +``` + +```bash +# Run the API application +cd packages/api && make dev +``` diff --git a/packages/k3d-gpu/README.md b/packages/k3d-gpu/README.md index dbe1e534a..857409d3a 100644 --- a/packages/k3d-gpu/README.md +++ b/packages/k3d-gpu/README.md @@ -2,6 +2,8 @@ Prepares `k3s` + `nvidia/cuda` base image that enables a K3D cluster to have access to your host machine's NVIDIA, CUDA-capable GPU(s). +This is for development and demonstration purposes, and should not be used to deploy LeapfrogAI in a production environment. + ## Pre-Requisites * Docker: https://www.docker.com/ @@ -13,9 +15,9 @@ Prepares `k3s` + `nvidia/cuda` base image that enables a K3D cluster to have acc Check out the Make targets for the various options. -### Local +### Local Development -```shell +```bash make build-k3d-gpu # build the image make create-uds-gpu-cluster # create a uds cluster equipped with the k3d-gpu image diff --git a/packages/llama-cpp-python/README.md b/packages/llama-cpp-python/README.md index 9aed7f7c7..35e192a4b 100644 --- a/packages/llama-cpp-python/README.md +++ b/packages/llama-cpp-python/README.md @@ -1,12 +1,12 @@ -# LeapfrogAI llama-cpp-python Backend +# LeapfrogAI LLaMA C++ Python Backend A LeapfrogAI API-compatible [llama-cpp-python](https://github.com/abetlen/llama-cpp-python) w wrapper for quantized and un-quantized model inferencing across CPU infrastructures. - +## Usage See [instructions](#instructions) to get the backend up and running. Then, use the [LeapfrogAI API server](https://github.com/defenseunicorns/leapfrogai-api) to interact with the backend. -## Instructions +### Instructions The instructions in this section assume the following: @@ -30,24 +30,24 @@ FILENAME # eg: "synthia-7b-v2.0.Q4_K_M.gguf" REVISION # eg: "3f65d882253d1f15a113dabf473a7c02a004d2b5" ``` -## Zarf Package Deployment +### Zarf Package Deployment To build and deploy just the llama-cpp-python Zarf package (from the root of the repository): > Deploy a [UDS cluster](/README.md#uds) if one isn't deployed already -```shell +```bash pip install 'huggingface_hub[cli,hf_transfer]' # Used to download the model weights from huggingface make build-llama-cpp-python LOCAL_VERSION=dev uds zarf package deploy packages/llama-cpp-python/zarf-package-llama-cpp-python-*-dev.tar.zst --confirm ``` -## Run Locally - +### Local Development To run the llama-cpp-python backend locally (starting from the root directory of the repository): From this directory: + ```bash # Setup Virtual Environment python -m venv .venv diff --git a/packages/repeater/README.md b/packages/repeater/README.md index 86f362ea5..8ad130b49 100644 --- a/packages/repeater/README.md +++ b/packages/repeater/README.md @@ -2,33 +2,34 @@ A LeapfrogAI API-compatible repeater model that simply parrots the input it is provided back to the user. This is primarily used for quick-testing the API. - -# Usage +## Usage The repeater model is used to verify that the API is able to both load configs for and send inputs to a very simple model. The repeater model fulfills this role by returning the input it recieves as output. -## Zarf Package Deployment +### Zarf Package Deployment To build and deploy just the repeater Zarf package (from the root of the repository): > Deploy a [UDS cluster](/README.md#uds) if one isn't deployed already -```shell +```bash make build-repeater LOCAL_VERSION=dev uds zarf package deploy packages/repeater/zarf-package-repeater-*-dev.tar.zst --confirm ``` -## Local Usage +### Local Development Here is how to run the repeater model locally to test the API: It's easiest to set up a virtual environment to keep things clean: + ```bash python -m venv .venv source .venv/bin/activate ``` First install the lfai-repeater project and dependencies. From the root of the project repository: + ```bash pip install src/leapfrogai_sdk cd packages/repeater @@ -36,11 +37,13 @@ pip install . ``` Next, launch the repeater model: + ```bash python repeater.py ``` Now the basic API tests can be run in full. In a new terminal, starting from the root of the project repository: + ```bash export LFAI_RUN_REPEATER_TESTS=true # this is needed to run the tests that require the repeater model, otherwise they get skipped pytest tests/pytest/test_api_auth.py diff --git a/packages/supabase/README.md b/packages/supabase/README.md index 35cdb276f..8c2e7c91f 100644 --- a/packages/supabase/README.md +++ b/packages/supabase/README.md @@ -1,23 +1,26 @@ -# Setting up Supabase locally +# Supabase -## Step 1: Create a Zarf package +## Usage + +### Step 1: Create a Zarf package From `leapfrogai/packages/supabase` run `zarf package create` -## Step 2: Create the uds bundle +### Step 2: Create the UDS bundle From `leapfrogai/uds-bundles/dev//` run `uds create` - -## Step 3: Deploy the UDS bundle or deploy the Zarf package +### Step 3: Deploy the UDS bundle or deploy the Zarf package To deploy only Supabase for UDS bundle run the following from `leapfrogai/uds-bundles/dev//`: + * `uds deploy -p supabase uds-bundle-leapfrogai-*.tar.zst` To deploy the Zarf package run the following from `leapfrogai/packages/supabase`: + * `uds zarf package deploy zarf-package-supabase-*.tar.zst` -## Step 4: Accessing Supabase +### Step 4: Accessing Supabase Go to `https://supabase-kong.uds.dev`. The login is `supabase-admin` the password is randomly generated in a cluster secret named `supabase-dashboard-secret` @@ -29,22 +32,30 @@ Go to `https://supabase-kong.uds.dev`. The login is `supabase-admin` the passwor * If logging in to the UI through keycloak returns a `500`, check and see if the `sql` migrations have been run in Supabase. * You can find those in `leapfrogai/src/leapfrogai_ui/supabase/migrations`. They can be run in the studios SQL Editor. * To obtain a jwt token for testing, create a test user and run the following: -``` -curl -X POST 'https://supabase-kong.uds.dev/auth/v1/token?grant_type=password' \-H "apikey: " \-H "Content-Type: application/json" \-d '{ "email": "", "password": ""}' + +```bash +# Grab the Supabase Key from the JWT Secret +export ANON_KEY=$(uds zarf tools kubectl get secret -n leapfrogai supabase-bootstrap-jwt -o json | uds zarf tools yq '.data.anon-key' | base64 -d) + +# Replace and / with your desired credentials +curl -X POST 'https://supabase-kong.uds.dev/auth/v1/signup' \ + -H "apikey: " \ + -H "Content-Type: application/json" \ + -H "Authorization": f"Bearer $ANON_KEY" \ + -d '{ "email": "", "password": "", "confirmPassword": ""}' ``` By following these steps, you'll have successfully set up Keycloak for your application, allowing secure authentication and authorization for your users. +## Supabase Migrations -# Supabase Migrations - -## Motivation +### Motivation A database migration is the process of modifying a database's schema in a controlled and versioned way. Migrations are used to modify the functionality of a database as its supported applications evolves over time. As time goes on, an application may require new tables, or tables may need new columns/indexes. Migrations allow for smooth changes to be applied to deployed databases, regardless of the current version the application is on. Migrations catalog a history of the database and provide an inherit form of database documentation, as each migration is stored in the Git repository chronologically (and by release). Migrations are automated on new deployments of LeapfrogAI such that all of the migrations (i.e database changes) are applied in order to ensure that the database has the most up to date schema. Migrations can also be run anytime a new version of LeapfrogAI is released, regardless of which version of LeapfrogAI is being updated from. -## Approach +### Approach Migrations are handled using the [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started?queryGroups=platform&platform=linux). The Supabase CLI automatically handles new migrations and keeps track of which migrations have already been run, regardless whether the database instance is brand new or pre-existing. @@ -54,7 +65,7 @@ In order to submit migrations at deploy time, [K8s jobs](https://kubernetes.io/d The K8s jobs themselves simply pull any existing migrations from the remote database within the same cluster, then push up the local migrations. Due to the [schema migrations table](https://supabase.com/docs/reference/cli/usage#supabase-db-push), any migrations that have already been run on the remote database will be skipped, ensuring migrations are not repeated. Since each package's migrations should be separate, a different template is used for each job. -## Managing Migrations +### Managing Migrations Keep the following in mind when adding new migrations: diff --git a/packages/text-embeddings/README.md b/packages/text-embeddings/README.md index 09bef42b4..60660e499 100644 --- a/packages/text-embeddings/README.md +++ b/packages/text-embeddings/README.md @@ -1,29 +1,27 @@ - # LeapfrogAI llama-cpp-python Backend A LeapfrogAI API-compatible [instructor-xl](https://huggingface.co/hkunlp/instructor-xl) model for creating embeddings across CPU and GPU infrastructures. +## Usage -# Usage - -## Zarf Package Deployment +### Zarf Package Deployment To build and deploy just the text-embeddings Zarf package (from the root of the repository): > Deploy a [UDS cluster](/README.md#uds) if one isn't deployed already -```shell +```bash pip install 'huggingface_hub[cli,hf_transfer]' # Used to download the model weights from huggingface make build-text-embeddings LOCAL_VERSION=dev uds zarf package deploy packages/text-embeddings/zarf-package-text-embeddings-*-dev.tar.zst --confirm ``` -## Local Development +### Local Development To run the text-embeddings backend locally (starting from the root directory of the repository): -```shell +```bash # Setup Virtual Environment if you haven't done so already python -m venv .venv source .venv/bin/activate diff --git a/packages/vllm/README.md b/packages/vllm/README.md index eac93f3ef..8a835f659 100644 --- a/packages/vllm/README.md +++ b/packages/vllm/README.md @@ -2,12 +2,11 @@ A LeapfrogAI API-compatible [vLLM](https://github.com/vllm-project/vllm) wrapper for quantized and un-quantized model inferencing across GPU infrastructures. - ## Usage See [instructions](#instructions) to get the backend up and running. Then, use the [LeapfrogAI API server](https://github.com/defenseunicorns/leapfrogai-api) to interact with the backend. -## Instructions +### Instructions The instructions in this section assume the following: @@ -31,21 +30,22 @@ You can optionally specify different models or quantization types using the foll - `--build-arg QUANTIZATION="gptq"`: Quantization type (e.g., gptq, awq, or empty for un-quantized) - `--build-arg TENSOR_PARALLEL_SIZE="1"`: The number of gpus to spread the tensor processing across -## Zarf Package Deployment +### Zarf Package Deployment To build and deploy just the VLLM Zarf package (from the root of the repository): > Deploy a [UDS cluster](/README.md#uds) if one isn't deployed already -```shell +```bash pip install 'huggingface_hub[cli,hf_transfer]' # Used to download the model weights from huggingface make build-vllm LOCAL_VERSION=dev uds zarf package deploy packages/vllm/zarf-package-vllm-*-dev.tar.zst --confirm ``` -## Run Locally +### Local Development To run the vllm backend locally (starting from the root directory of the repository): + ```bash # Setup Virtual Environment if you haven't done so already python -m venv .venv diff --git a/packages/whisper/README.md b/packages/whisper/README.md index ddf0b1008..31804784f 100644 --- a/packages/whisper/README.md +++ b/packages/whisper/README.md @@ -2,27 +2,26 @@ A LeapfrogAI API-compatible [whisper](https://huggingface.co/openai/whisper-base) wrapper for audio transcription inferencing across CPU & GPU infrastructures. +## Usage -# Usage - -## Zarf Package Deployment +### Zarf Package Deployment To build and deploy just the whisper Zarf package (from the root of the repository): > Deploy a [UDS cluster](/README.md#uds) if one isn't deployed already -```shell +```bash pip install 'ctranslate2' # Used to download and convert the model weights pip install 'transformers[torch]' # Used to download and convert the model weights make build-whisper LOCAL_VERSION=dev uds zarf package deploy packages/whisper/zarf-package-whisper-*-dev.tar.zst --confirm ``` -## Local Development +### Local Development To run the vllm backend locally without K8s (starting from the root directory of the repository): -```shell +```bash python -m pip install src/leapfrogai_sdk cd packages/whisper python -m pip install ".[dev]" diff --git a/src/leapfrogai_api/README.md b/src/leapfrogai_api/README.md index 383f271cb..5304fcc17 100644 --- a/src/leapfrogai_api/README.md +++ b/src/leapfrogai_api/README.md @@ -6,9 +6,9 @@ A mostly OpenAI compliant API surface. To build and deploy just the API Zarf package (from the root of the repository): -> Deploy a [UDS cluster](/README.md#uds) if one isn't deployed already +> Deploy a [UDS cluster](../../README.md#uds) if one isn't deployed already -```shell +```bash make build-api LOCAL_VERSION=dev uds zarf package deploy packages/api/zarf-package-leapfrogai-api-*-dev.tar.zst --confirm ``` @@ -16,61 +16,73 @@ uds zarf package deploy packages/api/zarf-package-leapfrogai-api-*-dev.tar.zst - ## Local Development Setup 1. Install dependencies + ```bash make install ``` -2. Create a local Supabase instance (requires [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started)): +2. Create a config.yaml using the config.example.yaml as a template. + +3. Run the FastAPI application + + ``` bash + make dev-run-api + ``` + +4. Create a local Supabase instance (requires [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started)): + ```bash - brew install supabase/tap/supabases + brew install supabase/tap/supabase supabase start # from this directory supabase stop --project-id leapfrogai # stop api containers - supabase db reset # clears all data and reinitializes migrations + supabase db reset # clears all data and re-initializes migrations supabase status # to check status and see your keys ``` -### Session Authentication +5. Create a local API user -3. Create a local api user ```bash make user ``` -4. Create a JWT token +6. Create a JWT token + ```bash make jwt source .env ``` - This will copy the JWT token to your clipboard. + This will copy the JWT token to your clipboard. -5. Make calls to the api swagger endpoint at `http://localhost:8080/docs` using your JWT token as the `HTTPBearer` token. +7. Make calls to the api swagger endpoint at `http://localhost:8080/docs` using your JWT token as the `HTTPBearer` token. * Hit `Authorize` on the swagger page to enter your JWT token ## Integration Tests The integration tests serve to identify any mismatches between components: -- Check all API routes -- Validate Request/Response types -- DB CRUD operations -- Schema mismatches +* Check all API routes +* Validate Request/Response types +* DB CRUD operations +* Schema mismatches ### Prerequisites -Integration tests require a Supabase instance and environment variables configured (see [Local Development](#local-development)). +Integration tests require a Supabase instance and environment variables configured (see [Local Development](#local-development-setup)). ### Authentication -Tests require a JWT token environment variable `SUPABASE_USER_JWT`. See [Session Authentication](#session-authentication) to set this up. +Tests require a JWT token environment variable `SUPABASE_USER_JWT`. See [Local Development](#local-development-setup) steps 3-5 to set this up. ### Running the tests + After obtaining the JWT token, run the following: -``` + +```bash make test-integration ``` diff --git a/src/leapfrogai_ui/README.md b/src/leapfrogai_ui/README.md index 7b38cdb86..b4c561af4 100644 --- a/src/leapfrogai_ui/README.md +++ b/src/leapfrogai_ui/README.md @@ -2,7 +2,7 @@ ## Getting Started -### Requirements: +### Requirements This application requires Supabase, and either Leapfrog API or OpenAI to function. Additionally, it can optionally use Keycloak for authentication. There are several different ways to run it, so please see the "Configuration Options" section @@ -20,24 +20,26 @@ below for more information. It is recommended to run LeapfrogAI with UDS, but if you want to run the UI locally (on localhost, e.g. for local development), you can either: -1. Connect to a UDS deployed version of the Leapfrog API and Supabase - or +1. Connect to a UDS deployed version of the Leapfrog API and Supabase + + **OR** + 2. Connect to OpenAI and UDS deployed Supabase or locally running Supabase. -_Note - most data CRUD operations utilize Leapfrog API or OpenAI, but some functionality still depends on a direct connection with Supabase._ +**NOTE:** most data CRUD operations utilize Leapfrog API or OpenAI, but some functionality still depends on a direct connection with Supabase. -#### Running everything with UDS +#### UDS Bundle Deployment -This is the easiest way to use the UI. Follow the documentation for running the entire [LeapfrogAI stack](https://github.com/defenseunicorns/leapfrogai) +This is the easiest way to use the UI. Follow the README and documentation website for running the entire [LeapfrogAI stack](https://github.com/defenseunicorns/leapfrogai). -#### Running UI Locally with LeapfrogAI +#### Local Development -If running the UI locally and utilizing LeapfrogAPI, **you must use the same Supabase instance that the Leapfrog API is utilizing**. +If running the UI locally and utilizing LeapfrogAPI, **you must use the same Supabase instance that the Leapfrog API is utilizing**. 1. Connect the UI to a UDS deployed version of Supabase and Leapfrog API. Ensure these env variables are set appropriately in your .env file: -``` +```bash PUBLIC_SUPABASE_URL=https://supabase-kong.uds.dev PUBLIC_SUPABASE_ANON_KEY= ... @@ -64,7 +66,7 @@ run the UI outside of UDS on localhost (e.g. for development work), there are so Add these values to the "GOTRUE_URI_ALLOW_LIST" (no spaces!). This variable may not exist and you will need to add it. Restart the supabase-auth pod after updating the config: `http://localhost:5173/auth/callback,http://localhost:4173/auth/callback` - Note - Port 4173 is utilized by Playwright for E2E tests. You do not need this if you are not concerned about Playwright. + **NOTE:** Port 4173 is utilized by Playwright for E2E tests. You do not need this if you are not concerned about Playwright. ###### With Keycloak authentication @@ -84,17 +86,17 @@ run the UI outside of UDS on localhost (e.g. for development work), there are so Set the following .env variables: -``` +```bash DEFAULT_MODEL=gpt-3.5-turbo LEAPFROGAI_API_BASE_URL=https://api.openai.com -#If specified, app will use OpenAI instead of Leapfrog +# If specified, app will use OpenAI instead of Leapfrog OPENAI_API_KEY= ``` You still need Supabase, so you can connect to UDS deployed Supabase, or run Supabase locally. To connect to UDS deployed Supabase, set these .env variables: -``` +```bash PUBLIC_SUPABASE_URL=https://supabase-kong.uds.dev PUBLIC_SUPABASE_ANON_KEY= ``` @@ -106,7 +108,7 @@ Running Supabase locally: The configuration files at src/leapfrogai_ui/supabase will ensure your Supabase is configured to work with Keycloak if you set these .env variables: -``` +```bash SUPABASE_AUTH_KEYCLOAK_CLIENT_ID=uds-supabase SUPABASE_AUTH_KEYCLOAK_SECRET= #this is the client secret for the client in Keycloak SUPABASE_AUTH_EXTERNAL_KEYCLOAK_URL=https://sso.uds.dev/realms/uds @@ -124,16 +126,13 @@ Stop Supabase: `npm run supabase:stop` -_Warning - if switching the application from utilizing Leapfrog API to OpenAI or vice versa, -and you encounter this error:_ -`Server responded with status code 431. See https://vitejs.dev/guide/troubleshooting.html#_431-request-header-fields-too-large.` -_you need to clear your browser cookies_ +**WARNING:** if switching the application from utilizing Leapfrog API to OpenAI or vice versa, and you encounter this error: `Server responded with status code 431. See https://vitejs.dev/guide/troubleshooting.html#_431-request-header-fields-too-large.`, then you need to clear your browser cookies. ### Building To create a production version of the app: -``` +```bash npm run build ``` @@ -170,14 +169,14 @@ Notes: The Supabase docs are inadequate for properly integrating with Keycloak. Additionally, they only support integration with the Supabase Cloud SAAS offering. Before reading the section below, first reference the [Supabase docs](https://supabase.com/docs/guides/auth/social-login/auth-keycloak). -### The following steps are required to integrate Supabase with Keycloak for local development: +**The following steps are required to integrate Supabase with Keycloak for local development** The supabase/config.toml file contains configuration options for Supabase when running it locally. When running locally, the Supabase UI dashboard does not offer all the same configuration options that the cloud version does, so you have to specify some options in this file instead. The variables that had to be overridden were: -``` +```toml [auth] site_url = "http://localhost:5173" @@ -201,8 +200,8 @@ Under a realm in Keycloak that is not the master realm (if using UDS, its "uds") 4. Copy the Client Secret under the Clients -> Credentials tab and use in the env variables below 5. You can create users under the "Users" tab and either have them verify their email (if you setup SMTP), or manually mark them as verified. -``` -#.env +```bash +# .env SUPABASE_AUTH_KEYCLOAK_CLIENT_ID= SUPABASE_AUTH_KEYCLOAK_SECRET= SUPABASE_AUTH_EXTERNAL_KEYCLOAK_URL= @@ -225,26 +224,27 @@ If you need to use a different Keycloak server for local development, you will n If your Keycloak server is not at a hosted domain, you will also need to modify the /etc/hosts on your machine: -``` -Example: -sudo nano /etc/hosts -*add this line (edit as required)* +```bash +vim /etc/hosts + +# add the following line to the opened `/etc/hosts` file +# replace beginning with the correct IP address xxx.xxx.xx.xx keycloak.admin.uds.dev ``` Ensure the -``` +```bash PUBLIC_SUPABASE_URL= PUBLIC_SUPABASE_ANON_KEY= ``` -variables in your .env file are pointing to the correct Supabase instance. +variables in your `.env` file are pointing to the correct Supabase instance. -Note - if connecting to a hosted Supabase instance, or in a cluster with networking, you will not need to override /etc/host files. +**NOTE:** if connecting to a hosted Supabase instance, or in a cluster with networking, you will not need to override /etc/host files. The: -``` +```bash SUPABASE_AUTH_KEYCLOAK_CLIENT_ID= SUPABASE_AUTH_KEYCLOAK_SECRET= SUPABASE_AUTH_EXTERNAL_KEYCLOAK_URL= @@ -265,7 +265,7 @@ When scanning the QR code, use an app that lets you see the url of the QR code. Login flow was adapted from [this reference](https://supabase.com/docs/guides/getting-started/tutorials/with-sveltekit?database-method=sql) -### Chat Data Flow +## Chat Data Flow The logic for handling regular chat messages and assistant chat messages, along with persisting that data to the database is complex and deserves a detailed explanation. From 162e6bf96c5640bb8bcd5edc09939743ca9d769a Mon Sep 17 00:00:00 2001 From: Justin Law Date: Wed, 14 Aug 2024 18:44:40 -0400 Subject: [PATCH 07/30] fix minor typo --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a094ae16..e435de041 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,7 +56,7 @@ repos: rev: v0.12.1 hooks: - id: markdownlint-cli2 - name: Markdown Linti + name: Markdown Lint - repo: local hooks: From 06e08c269e6da36d7c77e31105192e321a390357 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Thu, 15 Aug 2024 10:18:35 -0400 Subject: [PATCH 08/30] add workflow for markdown linting --- .github/workflows/markdown-lint.yaml | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/markdown-lint.yaml diff --git a/.github/workflows/markdown-lint.yaml b/.github/workflows/markdown-lint.yaml new file mode 100644 index 000000000..867a6ea2d --- /dev/null +++ b/.github/workflows/markdown-lint.yaml @@ -0,0 +1,43 @@ +name: Markdown Lint + +on: + push: + branches: + - "main" + paths: + - README.md + - .github/*.md + - docs/**/*.md + - ".github/workflows/markdown-lint.yaml" + pull_request: + branches: + - "main" + paths: + - README.md + - .github/*.md + - docs/**/*.md + - ".github/workflows/markdown-lint.yaml" + +concurrency: + group: markdown-lint-${{ github.ref }} + cancel-in-progress: true + +jobs: + markdown-lint: + runs-on: ubuntu-latest + name: Lint Markdown Files + + permissions: + contents: read + + steps: + - name: Checkout Repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - uses: DavidAnson/markdownlint-cli2-action@b4c9feab76d8025d1e83c653fa3990936df0e6c8 #v16.0.0 + with: + config: "./.markdownlint.json" + globs: | + README.md + .github/*.md + docs/**/*.md From de28cf3e91d1892bfde897f552b2d76f57537579 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Thu, 15 Aug 2024 12:43:45 -0400 Subject: [PATCH 09/30] completed docs, consistent LeapfrogAI nomenclature --- README.md | 12 +- docs/DEVELOPMENT.md | 12 +- packages/api/README.md | 42 ++---- packages/k3d-gpu/README.md | 31 ++-- packages/llama-cpp-python/README.md | 46 +++--- packages/repeater/Makefile | 7 + packages/repeater/README.md | 43 +++--- packages/supabase/README.md | 64 ++++---- packages/supabase/bitnami-values.yaml | 2 +- .../supabase/chart/templates/uds-package.yaml | 2 +- packages/text-embeddings/Dockerfile | 2 +- packages/text-embeddings/Makefile | 7 + packages/text-embeddings/README.md | 43 ++++-- packages/ui/README.md | 33 ++++ packages/ui/zarf.yaml | 2 +- packages/vllm/README.md | 55 +++---- packages/whisper/Makefile | 7 + packages/whisper/README.md | 35 ++++- src/leapfrogai_api/Makefile | 1 - src/leapfrogai_api/README.md | 63 +++----- src/leapfrogai_sdk/README.md | 17 +++ src/leapfrogai_sdk/__init__.py | 2 +- src/leapfrogai_sdk/cli.py | 2 +- src/leapfrogai_ui/.env.example | 2 +- src/leapfrogai_ui/README.md | 141 +++++++++--------- src/leapfrogai_ui/playwright.config.ts | 2 +- .../src/lib/components/Message.svelte | 2 +- src/leapfrogai_ui/tests/chat.test.ts | 6 +- .../tests/helpers/fileHelpers.ts | 4 +- .../en/docs/local-deploy-guide/components.md | 2 +- .../docs/local-deploy-guide/dependencies.md | 2 +- .../en/docs/local-deploy-guide/quick_start.md | 5 +- .../docs/local-deploy-guide/requirements.md | 2 +- 33 files changed, 378 insertions(+), 320 deletions(-) create mode 100644 packages/repeater/Makefile create mode 100644 packages/text-embeddings/Makefile create mode 100644 packages/ui/README.md create mode 100644 packages/whisper/Makefile create mode 100644 src/leapfrogai_sdk/README.md diff --git a/README.md b/README.md index dfe513569..94c2925e7 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Please refer to the [Quick Start](https://docs.leapfrog.ai/docs/local-deploy-gui ### API -LeapfrogAI provides an API that closely matches that of OpenAI's. This feature allows tools that have been built with OpenAI/ChatGPT to function seamlessly with a LeapfrogAI backend. +LeapfrogAI provides an [API](src/leapfrogai_api/) that closely matches that of OpenAI's. This feature allows tools that have been built with OpenAI/ChatGPT to function seamlessly with a LeapfrogAI backend. ### SDK @@ -113,9 +113,9 @@ Each of the LeapfrogAI components can also be run individually outside of a Kube **_First_** refer to the [DEVELOPMENT.md](docs/DEVELOPMENT.md) document for general development details, **_then_** refer to the linked READMEs for each individual packages local development instructions: -- [API](src/leapfrogai_api/README.md)[^2] -- [SDK](src/leapfrogai_sdk/README.md)[^3] -- [UI](src/leapfrogai_ui/README.md)[^2] +- [SDK](src/leapfrogai_sdk/README.md)[^2] +- [API](packages/api/README.md)[^3] +- [UI](packages/ui/README.md)[^3] - [LLaMA C++ Python](packages/llama-cpp-python/README.md) - [vLLM](packages/vllm/README.md) - [Supabase](packages/supabase/README.md) @@ -123,9 +123,9 @@ Each of the LeapfrogAI components can also be run individually outside of a Kube - [Faster Whisper](packages/whisper/README.md) - [Repeater](packages/repeater/README.md) -[^2]: Please be aware that the API and UI have artifacts under 2 sub-directories. The sub-directories related to `packages/` are focused on the Zarf packaging and Helm charts, whereas the sub-directories related to `src/` contains the actual source code and development instructions. +[^2]: The SDK is not a functionally independent unit, and only becomes a functional unit when combined and packaged with the API and Backends as a dependency. -[^3]: The SDK is not a functionally independent unit, and only becomes a functional unit when combined and packaged with the API and Backends as a dependency. +[^3]: Please be aware that the API and UI have artifacts under 2 sub-directories. The sub-directories related to `packages/` are focused on the Zarf packaging and Helm charts, whereas the sub-directories related to `src/` contains the actual source code and development instructions. ## Contributing diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 064021cb2..b32a60e5e 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -7,7 +7,7 @@ The purpose of this document is to describe how to run a development loop on the ## Local Development -Please first see the pre-requisites listed on the LeapfrogAI documentation website's [Requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) and [Dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/) +Please first see the pre-requisites listed on the LeapfrogAI documentation website's [Requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) and [Dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/), before going to each component's subdirectory README. ## UDS CLI Aliasing @@ -31,11 +31,17 @@ echo -e '#!/bin/bash\nuds zarf tools kubectl "$@"' > /usr/local/bin/kubectl chmod +x /usr/local/bin/kubectl ``` +## Makefile + +Many of the directories and sub-directories within this project contain Make targets that can be executed to simplify repetitive command-line tasks. + +Please refer to each Makefile for more arguments and details on what each target does and is dependent on. + ## Package Development -If you don't want to build an entire bundle, or you want to dev-loop on a single package in an existing, Zarf-init'd cluster, you can do so by performing a `uds zarf package remove [PACKAGE_NAME]` and re-deploying the package into the cluster. +If you don't want to build an entire bundle, or you want to dev-loop on a single package in an existing [UDS Kubernetes cluster](../packages/k3d-gpu/README.md) you can do so by performing the following. -For example, this is how you build and deploy a local DEV version of a package: +For example, this is how you build and (re)deploy a local DEV version of a package: ```bash # if package is already in the cluster, and you are deploying a new one diff --git a/packages/api/README.md b/packages/api/README.md index 02e1f6b30..3d451decb 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -1,22 +1,26 @@ # LeapfrogAI Python API -A Python API that exposes LLM backends, via FastAPI and gRPC, in the [OpenAI API specification](https://platform.openai.com/docs/api-reference). +A Python API that exposes AI backends, via FastAPI and gRPC, in the [OpenAI API specification](https://platform.openai.com/docs/api-reference). ## Usage -See [instructions](#instructions) to get the backend up and running. +### Pre-Requisites -### Instructions +See the LeapfrogAI documentation website for [system requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) and [dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/). -The instructions in this section assume the following: +#### Dependent Components -1. Properly installed and configured Python 3.11.x, to include its development tools +- [UDS Kubernetes cluster bootstrapped with UDS Core Slim Dev](../k3d-gpu/README.md) for local KeyCloak authentication, Istio Service Mesh, and MetalLB advertisement +- [Supabase](../supabase/README.md) for a vector database to store resulting embeddings in, and user management and authentication +- [Text Embeddings](../text-embeddings/README.md) for RAG +- [LLaMA C++ Python](../llama-cpp-python/README.md) or [vLLM](../vllm/README.md) for completions and chat completions -### Zarf Package Deployment +### Deployment -To build and deploy just the llama-cpp-python Zarf package (from the root of the repository): +To build and deploy the API Zarf package into an existing [UDS Kubernetes cluster](../k3d-gpu/README.md): -> Deploy a [UDS cluster](/README.md#uds) if one isn't deployed already +> [!IMPORTANT] +> Execute the following commands from the root of the LeapfrogAI repository ```bash make build-api LOCAL_VERSION=dev @@ -25,24 +29,4 @@ uds zarf package deploy packages/api/zarf-package-leapfrogai-api-*-dev.tar.zst - ### Local Development -To run the API locally (starting from the root directory of the repository): - -From this directory: - -```bash -# Setup Virtual Environment -python -m venv .venv -source .venv/bin/activate -``` - -```bash -# Install dependencies -python -m pip install src/leapfrogai_sdk -cd packages/api -python -m pip install ".[dev]" -``` - -```bash -# Run the API application -cd packages/api && make dev -``` +See the [source code documentation](../../src/leapfrogai_api/README.md) for running the API from the source code for local Python environment development. diff --git a/packages/k3d-gpu/README.md b/packages/k3d-gpu/README.md index 857409d3a..9a405a0f1 100644 --- a/packages/k3d-gpu/README.md +++ b/packages/k3d-gpu/README.md @@ -1,30 +1,39 @@ # K3D GPU -Prepares `k3s` + `nvidia/cuda` base image that enables a K3D cluster to have access to your host machine's NVIDIA, CUDA-capable GPU(s). +Prepares a `k3s` + `nvidia/cuda` base image that enables a K3D cluster to utilize your host machine's NVIDIA, CUDA-capable GPU(s). This is for development and demonstration purposes, and should not be used to deploy LeapfrogAI in a production environment. -## Pre-Requisites +## Usage -* Docker: https://www.docker.com/ -* K3D: https://k3d.io/ -* UDS-CLI: https://github.com/defenseunicorns/uds-cli -* Modern NVIDIA GPU with CUDA cores and drivers must be present. Additionally, the CUDA toolkit and NVIDIA container toolkit must be installed. +### Pre-Requisites -## Usage +All system requirements and pre-requisites from the [LeapfrogAI documentation website](https://docs.leapfrog.ai/docs/local-deploy-guide/quick_start/). -Check out the Make targets for the various options. +### Deployment -### Local Development +> [!NOTE] +> The following Make targets from the root of the LeapfrogAI repository or within this sub-directory. -```bash -make build-k3d-gpu # build the image +To deploy a new K3d cluster with [UDS Core Slim Dev](https://github.com/defenseunicorns/uds-core#uds-package-development), use one of the following Make targets. +```bash make create-uds-gpu-cluster # create a uds cluster equipped with the k3d-gpu image make test-uds-gpu-cluster # deploy a test gpu pod to see if everything is working ``` +### Local Development + +> [!NOTE] +> The following Make targets can be executed from the root of the LeapfrogAI repository or within this sub-directory + +To build **just** the K3s CUDA image for container debugging, use the following Make target. + +```bash +make build-k3d-gpu # build the image +``` + ## References * https://k3d.io/v5.7.2/usage/advanced/cuda/ diff --git a/packages/llama-cpp-python/README.md b/packages/llama-cpp-python/README.md index 35e192a4b..7ddd3dfe5 100644 --- a/packages/llama-cpp-python/README.md +++ b/packages/llama-cpp-python/README.md @@ -1,26 +1,20 @@ # LeapfrogAI LLaMA C++ Python Backend -A LeapfrogAI API-compatible [llama-cpp-python](https://github.com/abetlen/llama-cpp-python) w wrapper for quantized and un-quantized model inferencing across CPU infrastructures. +A LeapfrogAI API-compatible [llama-cpp-python](https://github.com/abetlen/llama-cpp-python) wrapper for quantized and un-quantized model inferencing across CPU infrastructures. ## Usage -See [instructions](#instructions) to get the backend up and running. Then, use the [LeapfrogAI API server](https://github.com/defenseunicorns/leapfrogai-api) to interact with the backend. +### Pre-Requisites -### Instructions +See the LeapfrogAI documentation website for [system requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) and [dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/). -The instructions in this section assume the following: +#### Dependent Components -1. Properly installed and configured Python 3.11.x, to include its development tools -2. The LeapfrogAI API server is deployed and running - -The following are additional assumptions for GPU inferencing: - -3. You have properly installed one or more NVIDIA GPUs and GPU drivers -4. You have properly installed and configured the [cuda-toolkit](https://developer.nvidia.com/cuda-toolkit) and [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/index.html) +- [LeapfrogAI API](../api/README.md) for a fully RESTful application ### Model Selection -The default model that comes with this backend in this repository's officially released images is a [4-bit quantization of the Synthia-7b model](https://huggingface.co/TheBloke/SynthIA-7B-v2.0-GPTQ). +The default model that comes with this backend in this repository's officially released images is a [quantization of the Synthia-7b model](https://huggingface.co/TheBloke/SynthIA-7B-v2.0-GPTQ). Models are pulled from [HuggingFace Hub](https://huggingface.co/models) via the [model_download.py](/packages/llama-cpp-python/scripts/model_download.py) script. To change what model comes with the llama-cpp-python backend, set the following environment variables: @@ -30,11 +24,14 @@ FILENAME # eg: "synthia-7b-v2.0.Q4_K_M.gguf" REVISION # eg: "3f65d882253d1f15a113dabf473a7c02a004d2b5" ``` -### Zarf Package Deployment +If you choose a different model, make sure to modify the default [config.yaml](./config.yaml) using the Hugging Face model repository's model files and model card. + +### Deployment -To build and deploy just the llama-cpp-python Zarf package (from the root of the repository): +To build and deploy the llama-cpp-python backend Zarf package into an existing [UDS Kubernetes cluster](../k3d-gpu/README.md): -> Deploy a [UDS cluster](/README.md#uds) if one isn't deployed already +> [!IMPORTANT] +> Execute the following commands from the root of the LeapfrogAI repository ```bash pip install 'huggingface_hub[cli,hf_transfer]' # Used to download the model weights from huggingface @@ -44,30 +41,23 @@ uds zarf package deploy packages/llama-cpp-python/zarf-package-llama-cpp-python- ### Local Development -To run the llama-cpp-python backend locally (starting from the root directory of the repository): +To run the llama-cpp-python backend locally: -From this directory: +> [!IMPORTANT] +> Execute the following commands from this sub-directory ```bash # Setup Virtual Environment python -m venv .venv source .venv/bin/activate -``` -```bash -# Install dependencies -python -m pip install src/leapfrogai_sdk -cd packages/llama-cpp-python -python -m pip install ".[dev]" -``` +pip install 'huggingface_hub[cli,hf_transfer]' # Used to download the model weights from huggingface -```bash # Clone Model # Supply a REPO_ID, FILENAME and REVISION if a different model is desired python scripts/model_download.py - mv .model/*.gguf .model/model.gguf -# Start Model Backend -lfai-cli --app-dir=. main:Model +# Install dependencies and start the model backend +make dev ``` diff --git a/packages/repeater/Makefile b/packages/repeater/Makefile new file mode 100644 index 000000000..780f8c36a --- /dev/null +++ b/packages/repeater/Makefile @@ -0,0 +1,7 @@ +install: + python -m pip install ../../src/leapfrogai_sdk + python -m pip install -e . + +dev: + make install + python -m leapfrogai_sdk.cli --app-dir=. main:Model diff --git a/packages/repeater/README.md b/packages/repeater/README.md index 8ad130b49..187265add 100644 --- a/packages/repeater/README.md +++ b/packages/repeater/README.md @@ -1,16 +1,25 @@ # LeapfrogAI Repeater Backend -A LeapfrogAI API-compatible repeater model that simply parrots the input it is provided back to the user. This is primarily used for quick-testing the API. +A LeapfrogAI API-compatible repeater backend that simply parrots the input it is provided back to the user. This is primarily used for quick-testing the API. + +The repeater backend is used to verify that the API is able to both load configs for and send inputs to a very simple backend. The repeater backend fulfills this role by returning the input it recieves as output. ## Usage -The repeater model is used to verify that the API is able to both load configs for and send inputs to a very simple model. The repeater model fulfills this role by returning the input it recieves as output. +### Pre-Requisites + +See the LeapfrogAI documentation website for [system requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) and [dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/). + +#### Dependent Components -### Zarf Package Deployment +- Have the LeapfrogAI API deployed, running, and accessible in order to provide a fully RESTful application -To build and deploy just the repeater Zarf package (from the root of the repository): +### Deployment -> Deploy a [UDS cluster](/README.md#uds) if one isn't deployed already +To build and deploy the repeater backend Zarf package into an existing [UDS Kubernetes cluster](../k3d-gpu/README.md): + +> [!IMPORTANT] +> Execute the following Make targets from the root of the LeapfrogAI repository ```bash make build-repeater LOCAL_VERSION=dev @@ -19,30 +28,24 @@ uds zarf package deploy packages/repeater/zarf-package-repeater-*-dev.tar.zst -- ### Local Development -Here is how to run the repeater model locally to test the API: +To run the repeater backend locally: -It's easiest to set up a virtual environment to keep things clean: +> [!IMPORTANT] +> Execute the following commands from this sub-directory ```bash +# Setup Virtual Environment python -m venv .venv source .venv/bin/activate -``` -First install the lfai-repeater project and dependencies. From the root of the project repository: - -```bash -pip install src/leapfrogai_sdk -cd packages/repeater -pip install . +# Install dependencies and start the model backend +make dev ``` -Next, launch the repeater model: - -```bash -python repeater.py -``` +Now the basic API tests can be run in full with the following commands. -Now the basic API tests can be run in full. In a new terminal, starting from the root of the project repository: +> [!IMPORTANT] +> Execute the following commands from from the root of the LeapfrogAI repository ```bash export LFAI_RUN_REPEATER_TESTS=true # this is needed to run the tests that require the repeater model, otherwise they get skipped diff --git a/packages/supabase/README.md b/packages/supabase/README.md index 8c2e7c91f..9630d1b36 100644 --- a/packages/supabase/README.md +++ b/packages/supabase/README.md @@ -1,51 +1,59 @@ # Supabase -## Usage +A comprehensive relational and vector database operator and multi-functional API layer. See the [Supabase documentation](https://supabase.com/docs) and the [Bitnami package](https://bitnami.com/stack/supabase) for more details. -### Step 1: Create a Zarf package +## Usage -From `leapfrogai/packages/supabase` run `zarf package create` +### Pre-Requisites -### Step 2: Create the UDS bundle +See the LeapfrogAI documentation website for [system requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) and [dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/). -From `leapfrogai/uds-bundles/dev//` run `uds create` +#### Dependent Components -### Step 3: Deploy the UDS bundle or deploy the Zarf package +- [UDS Kubernetes cluster bootstrapped with UDS Core Slim Dev](../k3d-gpu/README.md) for local KeyCloak authentication, Istio Service Mesh, and MetalLB advertisement +- [LeapfrogAI API](../api/README.md) for RESTful interaction +- [Text Embeddings](../text-embeddings/README.md) for vector generation +- [LeapfrogAI UI](../ui/README.md) for a Supabase and API compatible frontend -To deploy only Supabase for UDS bundle run the following from `leapfrogai/uds-bundles/dev//`: +### Deployment -* `uds deploy -p supabase uds-bundle-leapfrogai-*.tar.zst` +To build and deploy the Supabase Zarf package into an existing [UDS Kubernetes cluster](../k3d-gpu/README.md): -To deploy the Zarf package run the following from `leapfrogai/packages/supabase`: +> [!IMPORTANT] +> Execute the following commands from the root of the LeapfrogAI repository -* `uds zarf package deploy zarf-package-supabase-*.tar.zst` +```bash +make build-supabase LOCAL_VERSION=dev +uds zarf package deploy packages/supabase/zarf-package-supabase-*-dev.tar.zst --confirm +``` -### Step 4: Accessing Supabase +### Accessing Supabase Go to `https://supabase-kong.uds.dev`. The login is `supabase-admin` the password is randomly generated in a cluster secret named `supabase-dashboard-secret` **NOTE:** The `uds.dev` domain is only used for locally deployed LeapfrogAI packages, so this domain will be unreachable without first manually deploying the UDS bundle. -## Local Supabase Troubleshooting +## Troubleshooting -* If you cannot reach `https://supabase-kong.uds.dev`, check if the `Packages` CRDs and `VirtualServices` contain `supabase-kong.uds.dev`. If they do not, try restarting the `pepr-uds-core-watcher` pod. -* If logging in to the UI through keycloak returns a `500`, check and see if the `sql` migrations have been run in Supabase. - * You can find those in `leapfrogai/src/leapfrogai_ui/supabase/migrations`. They can be run in the studios SQL Editor. -* To obtain a jwt token for testing, create a test user and run the following: +- If you cannot reach `https://supabase-kong.uds.dev`, check if the `Packages` CRDs and `VirtualServices` contain `supabase-kong.uds.dev`. If they do not, try restarting the `pepr-uds-core-watcher` pod. +- If logging in to the UI through keycloak returns a `500`, check and see if the `sql` migrations have been run in Supabase. + - You can find those in `leapfrogai/src/leapfrogai_ui/supabase/migrations` - Migrations can be run in the Supabase studio SQL editor +- To obtain an API (JWT) token for testing, create a test user and run the following: -```bash -# Grab the Supabase Key from the JWT Secret -export ANON_KEY=$(uds zarf tools kubectl get secret -n leapfrogai supabase-bootstrap-jwt -o json | uds zarf tools yq '.data.anon-key' | base64 -d) - -# Replace and / with your desired credentials -curl -X POST 'https://supabase-kong.uds.dev/auth/v1/signup' \ - -H "apikey: " \ - -H "Content-Type: application/json" \ - -H "Authorization": f"Bearer $ANON_KEY" \ - -d '{ "email": "", "password": "", "confirmPassword": ""}' -``` + ```bash + # Grab the Supabase Anon Key from the JWT Secret in the UDS Kubernetes cluster + export ANON_KEY=$(uds zarf tools kubectl get secret -n leapfrogai supabase-bootstrap-jwt -o json | uds zarf tools yq '.data.anon-key' | base64 -d) + + # Replace and / with your desired credentials + # only lasts 1 hour from creation + curl -X POST 'https://supabase-kong.uds.dev/auth/v1/signup' \ + -H "apikey: " \ + -H "Content-Type: application/json" \ + -H "Authorization": f"Bearer $ANON_KEY" \ + -d '{ "email": "", "password": "", "confirmPassword": ""}' + ``` -By following these steps, you'll have successfully set up Keycloak for your application, allowing secure authentication and authorization for your users. +- Longer term API tokens (30, 60, or 90 days) can be created from the API key workflow within the LeapfrogAI UI ## Supabase Migrations diff --git a/packages/supabase/bitnami-values.yaml b/packages/supabase/bitnami-values.yaml index af26cb2eb..100a485ab 100644 --- a/packages/supabase/bitnami-values.yaml +++ b/packages/supabase/bitnami-values.yaml @@ -1,4 +1,4 @@ -## @section Leapfrog parameters +## @section LeapfrogAI parameters ## Parameters not defined in the upstream chart that are related to LeapfrogAI's specific configuration leapfrogai: package: diff --git a/packages/supabase/chart/templates/uds-package.yaml b/packages/supabase/chart/templates/uds-package.yaml index 8c80894d0..932586690 100644 --- a/packages/supabase/chart/templates/uds-package.yaml +++ b/packages/supabase/chart/templates/uds-package.yaml @@ -4,7 +4,7 @@ metadata: name: {{ .Values.leapfrogai.package.name }} spec: sso: - - name: Leapfrog AI + - name: LeapfrogAI description: Client for logging into Supabase clientId: {{ .Values.leapfrogai.sso.clientId }} redirectUris: diff --git a/packages/text-embeddings/Dockerfile b/packages/text-embeddings/Dockerfile index 9af7c5537..96becc0de 100644 --- a/packages/text-embeddings/Dockerfile +++ b/packages/text-embeddings/Dockerfile @@ -12,7 +12,7 @@ RUN python3.11 -m venv .venv ENV PATH="/leapfrogai/.venv/bin:$PATH" # copy and install all python dependencies -# NOTE: We are copying the leapfrog whl to this filename because installing 'optional extras' from +# NOTE: We are copying the leapfrogai whl to this filename because installing 'optional extras' from # a wheel requires the absolute path to the wheel file (instead of a wildcard whl) COPY --from=sdk /leapfrogai/${SDK_DEST} ${SDK_DEST} COPY packages/text-embeddings packages/text-embeddings diff --git a/packages/text-embeddings/Makefile b/packages/text-embeddings/Makefile new file mode 100644 index 000000000..780f8c36a --- /dev/null +++ b/packages/text-embeddings/Makefile @@ -0,0 +1,7 @@ +install: + python -m pip install ../../src/leapfrogai_sdk + python -m pip install -e . + +dev: + make install + python -m leapfrogai_sdk.cli --app-dir=. main:Model diff --git a/packages/text-embeddings/README.md b/packages/text-embeddings/README.md index 60660e499..98db16469 100644 --- a/packages/text-embeddings/README.md +++ b/packages/text-embeddings/README.md @@ -1,15 +1,28 @@ +# LeapfrogAI Text Embeddings Backend -# LeapfrogAI llama-cpp-python Backend - -A LeapfrogAI API-compatible [instructor-xl](https://huggingface.co/hkunlp/instructor-xl) model for creating embeddings across CPU and GPU infrastructures. +A LeapfrogAI API-compatible text embeddings wrapper for producing embeddings from text content. ## Usage -### Zarf Package Deployment +### Pre-Requisites + +See the LeapfrogAI documentation website for [system requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) and [dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/). + +#### Dependent Components + +- [LeapfrogAI API](../api/README.md) for a fully RESTful application +- [Supabase](../supabase/README.md) for a vector database to store resulting embeddings in + +### Model Selection -To build and deploy just the text-embeddings Zarf package (from the root of the repository): +The default model that comes with this backend in this repository's officially released images is [instructor-xl](https://huggingface.co/hkunlp/instructor-xl). -> Deploy a [UDS cluster](/README.md#uds) if one isn't deployed already +### Deployment + +To build and deploy the text-embeddings backend Zarf package into an existing [UDS Kubernetes cluster](../k3d-gpu/README.md): + +> [!IMPORTANT] +> Execute the following commands from the root of the LeapfrogAI repository ```bash pip install 'huggingface_hub[cli,hf_transfer]' # Used to download the model weights from huggingface @@ -19,21 +32,19 @@ uds zarf package deploy packages/text-embeddings/zarf-package-text-embeddings-*- ### Local Development -To run the text-embeddings backend locally (starting from the root directory of the repository): +To run the text-embeddings backend locally: + +> [!IMPORTANT] +> Execute the following commands from this sub-directory ```bash -# Setup Virtual Environment if you haven't done so already +# Setup Virtual Environment python -m venv .venv source .venv/bin/activate -# install dependencies -python -m pip install src/leapfrogai_sdk -cd packages/text-embeddings -python -m pip install ".[dev]" - -# download the model +# Clone Model python scripts/model_download.py -# start the model backend -python -u main.py +# Install dependencies and start the model backend +make dev ``` diff --git a/packages/ui/README.md b/packages/ui/README.md new file mode 100644 index 000000000..6f9ab9f01 --- /dev/null +++ b/packages/ui/README.md @@ -0,0 +1,33 @@ +# LeapfrogAI UI + +A Svelte UI that provides an easy-to-use frontend for interacting with all components of the LeapfrogAI tech stack. + +## Usage + +### Pre-Requisites + +See the LeapfrogAI documentation website for [system requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) and [dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/). + +#### Dependent Components + +- [UDS Kubernetes cluster bootstrapped with UDS Core Slim Dev](../k3d-gpu/README.md) for local KeyCloak authentication, Istio Service Mesh, and MetalLB advertisement +- [LeapfrogAI API](../api/README.md) for OpenAI API-like AI model backend interaction +- [Supabase](../supabase/README.md) for a vector database to store resulting embeddings in, and user management and authentication +- [Text Embeddings](../text-embeddings/README.md) for RAG +- [LLaMA C++ Python](../llama-cpp-python/README.md) or [vLLM](../vllm/README.md) for completions and chat completions + +### Deployment + +To build and deploy the UI Zarf package into an existing [UDS Kubernetes cluster](../k3d-gpu/README.md): + +> [!IMPORTANT] +> Execute the following commands from the root of the LeapfrogAI repository + +```bash +make build-ui LOCAL_VERSION=dev +uds zarf package deploy packages/ui/zarf-package-leapfrogai-ui-*-dev.tar.zst --confirm +``` + +### Local Development + +See the [source code documentation](../../src/leapfrogai_ui/README.md) for running the UI from the source code for local Node environment development. diff --git a/packages/ui/zarf.yaml b/packages/ui/zarf.yaml index 83a233f1f..b0556d332 100644 --- a/packages/ui/zarf.yaml +++ b/packages/ui/zarf.yaml @@ -16,7 +16,7 @@ variables: prompt: true sensitive: true - name: OPENAI_API_KEY - description: OpenAI API Key. If specified, app will use OpenAI instead of Leapfrog + description: OpenAI API Key. If specified, app will use OpenAI instead of LeapfrogAI prompt: true default: "" sensitive: true diff --git a/packages/vllm/README.md b/packages/vllm/README.md index 8a835f659..b4cc7417b 100644 --- a/packages/vllm/README.md +++ b/packages/vllm/README.md @@ -1,22 +1,16 @@ # LeapfrogAI vLLM Backend -A LeapfrogAI API-compatible [vLLM](https://github.com/vllm-project/vllm) wrapper for quantized and un-quantized model inferencing across GPU infrastructures. +A LeapfrogAI API-compatible [vllm](https://github.com/vllm-project/vllm) wrapper for quantized and un-quantized model inferencing across GPU infrastructures. ## Usage -See [instructions](#instructions) to get the backend up and running. Then, use the [LeapfrogAI API server](https://github.com/defenseunicorns/leapfrogai-api) to interact with the backend. +### Pre-Requisites -### Instructions +See the LeapfrogAI documentation website for [system requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) and [dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/). -The instructions in this section assume the following: +#### Dependent Components -1. Properly installed and configured Python 3.11.x, to include its development tools -2. The LeapfrogAI API server is deployed and running - -The following are additional assumptions for GPU inferencing: - -3. You have properly installed one or more NVIDIA GPUs and GPU drivers -4. You have properly installed and configured the [cuda-toolkit](https://developer.nvidia.com/cuda-toolkit) and [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/index.html) +- [LeapfrogAI API](../api/README.md) for a fully RESTful application ### Model Selection @@ -30,11 +24,12 @@ You can optionally specify different models or quantization types using the foll - `--build-arg QUANTIZATION="gptq"`: Quantization type (e.g., gptq, awq, or empty for un-quantized) - `--build-arg TENSOR_PARALLEL_SIZE="1"`: The number of gpus to spread the tensor processing across -### Zarf Package Deployment +### Deployment -To build and deploy just the VLLM Zarf package (from the root of the repository): +To build and deploy the vllm backend Zarf package into an existing [UDS Kubernetes cluster](../k3d-gpu/README.md): -> Deploy a [UDS cluster](/README.md#uds) if one isn't deployed already +> [!IMPORTANT] +> Execute the following commands from the root of the LeapfrogAI repository ```bash pip install 'huggingface_hub[cli,hf_transfer]' # Used to download the model weights from huggingface @@ -44,35 +39,21 @@ uds zarf package deploy packages/vllm/zarf-package-vllm-*-dev.tar.zst --confirm ### Local Development -To run the vllm backend locally (starting from the root directory of the repository): +To run the vllm backend locally: + +> [!IMPORTANT] +> Execute the following commands from this sub-directory ```bash -# Setup Virtual Environment if you haven't done so already +# Setup Virtual Environment python -m venv .venv source .venv/bin/activate -``` -```bash -# Install dependencies -python -m pip install src/leapfrogai_sdk -cd packages/vllm -# To support Huggingface Hub model downloads -python -m pip install ".[dev]" -``` - -```bash -# Copy the environment variable file, change this if different params are needed -cp .env.example .env - -# Make sure environment variables are set -source .env +pip install 'huggingface_hub[cli,hf_transfer]' # Used to download the model weights from huggingface # Clone Model -# Supply a REPO_ID, FILENAME and REVISION if a different model is desired -python src/model_download.py - -mv .model/*.gguf .model/model.gguf +python scripts/model_download.py -# Start Model Backend -lfai-cli --app-dir=src/ main:Model +# Install dependencies and start the model backend +make dev ``` diff --git a/packages/whisper/Makefile b/packages/whisper/Makefile new file mode 100644 index 000000000..0434d4101 --- /dev/null +++ b/packages/whisper/Makefile @@ -0,0 +1,7 @@ +install: + python -m pip install ../../src/leapfrogai_sdk + python -m pip install -e . + +dev: + make install + python -m leapfrogai_sdk.cli --app-dir=src/ main:Model diff --git a/packages/whisper/README.md b/packages/whisper/README.md index 31804784f..0dff5a900 100644 --- a/packages/whisper/README.md +++ b/packages/whisper/README.md @@ -1,14 +1,27 @@ # LeapfrogAI Whisper Backend -A LeapfrogAI API-compatible [whisper](https://huggingface.co/openai/whisper-base) wrapper for audio transcription inferencing across CPU & GPU infrastructures. +A LeapfrogAI API-compatible [faster-whisper](https://github.com/SYSTRAN/faster-whisper) wrapper for audio transcription inferencing across CPU & GPU infrastructures. ## Usage -### Zarf Package Deployment +### Pre-Requisites -To build and deploy just the whisper Zarf package (from the root of the repository): +See the LeapfrogAI documentation website for [system requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) and [dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/). -> Deploy a [UDS cluster](/README.md#uds) if one isn't deployed already +#### Dependent Components + +- [LeapfrogAI API](../api/README.md) for a fully RESTful application + +### Model Selection + +See the [Deployment section](#deployment) for the CTranslate2 command for pulling and converting a model for inferencing. + +### Deployment + +To build and deploy the whisper backend Zarf package into an existing [UDS Kubernetes cluster](../k3d-gpu/README.md): + +> [!IMPORTANT] +> Execute the following commands from the root of the LeapfrogAI repository ```bash pip install 'ctranslate2' # Used to download and convert the model weights @@ -22,9 +35,15 @@ uds zarf package deploy packages/whisper/zarf-package-whisper-*-dev.tar.zst --co To run the vllm backend locally without K8s (starting from the root directory of the repository): ```bash -python -m pip install src/leapfrogai_sdk -cd packages/whisper -python -m pip install ".[dev]" +# Setup Virtual Environment +python -m venv .venv +source .venv/bin/activate + +pip install 'ctranslate2' # Used to download and convert the model weights +pip install 'transformers[torch]' # Used to download and convert the model weights + ct2-transformers-converter --model openai/whisper-base --output_dir .model --copy_files tokenizer.json --quantization float32 -python -u main.py + +# Install dependencies and start the model backend +make dev ``` diff --git a/src/leapfrogai_api/Makefile b/src/leapfrogai_api/Makefile index adc2a2b20..3ae197d42 100644 --- a/src/leapfrogai_api/Makefile +++ b/src/leapfrogai_api/Makefile @@ -1,7 +1,6 @@ MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) SHELL := /bin/bash - export SUPABASE_URL=$(shell supabase status | grep -oP '(?<=API URL: ).*') export SUPABASE_ANON_KEY=$(shell supabase status | grep -oP '(?<=anon key: ).*') diff --git a/src/leapfrogai_api/README.md b/src/leapfrogai_api/README.md index 5304fcc17..b6179457e 100644 --- a/src/leapfrogai_api/README.md +++ b/src/leapfrogai_api/README.md @@ -1,55 +1,38 @@ # LeapfrogAI API -A mostly OpenAI compliant API surface. +> [!IMPORTANT] +> See the [API package documentation](../../packages/api/README.md) for general pre-requisites, dependent components, and package deployment instructions -## Zarf Package Deployment +This document is only applicable for spinning up the API in a local Python development environment. -To build and deploy just the API Zarf package (from the root of the repository): +## Local Development Setup -> Deploy a [UDS cluster](../../README.md#uds) if one isn't deployed already +> [!IMPORTANT] +> Execute the following commands from this sub-directory -```bash -make build-api LOCAL_VERSION=dev -uds zarf package deploy packages/api/zarf-package-leapfrogai-api-*-dev.tar.zst --confirm -``` - -## Local Development Setup +### Running 1. Install dependencies ```bash - make install + make install-api ``` 2. Create a config.yaml using the config.example.yaml as a template. 3. Run the FastAPI application - ``` bash - make dev-run-api - ``` - -4. Create a local Supabase instance (requires [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started)): - ```bash - brew install supabase/tap/supabase - - supabase start # from this directory - - supabase stop --project-id leapfrogai # stop api containers - - supabase db reset # clears all data and re-initializes migrations - - supabase status # to check status and see your keys + make dev-run-api ``` -5. Create a local API user +4. Create a local Supabase user ```bash make user ``` -6. Create a JWT token +5. Create an API (JWT) token ```bash make jwt @@ -58,34 +41,22 @@ uds zarf package deploy packages/api/zarf-package-leapfrogai-api-*-dev.tar.zst - This will copy the JWT token to your clipboard. -7. Make calls to the api swagger endpoint at `http://localhost:8080/docs` using your JWT token as the `HTTPBearer` token. +6. Make calls to the api swagger endpoint at `http://localhost:8080/docs` using your JWT token as the `HTTPBearer` token. * Hit `Authorize` on the swagger page to enter your JWT token -## Integration Tests +### Integration Tests -The integration tests serve to identify any mismatches between components: +The integration tests serve to verify API functionality and compatibility with other existing components: * Check all API routes -* Validate Request/Response types -* DB CRUD operations +* Validate Request/Response objects +* Database CRUD operations * Schema mismatches -### Prerequisites - -Integration tests require a Supabase instance and environment variables configured (see [Local Development](#local-development-setup)). - -### Authentication - -Tests require a JWT token environment variable `SUPABASE_USER_JWT`. See [Local Development](#local-development-setup) steps 3-5 to set this up. - -### Running the tests +#### Running the tests After obtaining the JWT token, run the following: ```bash make test-integration ``` - -## Notes - -* All API calls must be authenticated via a Supabase JWT token in the message's `Authorization` header, including swagger docs. diff --git a/src/leapfrogai_sdk/README.md b/src/leapfrogai_sdk/README.md new file mode 100644 index 000000000..b38917190 --- /dev/null +++ b/src/leapfrogai_sdk/README.md @@ -0,0 +1,17 @@ +# LeapfrogAI SDK + +> [!IMPORTANT] +> The SDK is not a functionally independent component! Please see the root README for more context. + +This document is only applicable for integrating the SDK into an API and model backend in a local Python development environment. + +## Local Development Setup + +1. Make changes to the SDK code +2. Re-install dependencies and spin-up a model backend (e.g., [vLLM](../../packages/vllm/README.md)) +3. Re-install dependencies and spin-up the [LeapfrogAI API](../leapfrogai_api/README.md) +4. Test changes as required + +## Integration Tests + +See the [API documentation](../leapfrogai_api/README.md) for instructions on running tests. diff --git a/src/leapfrogai_sdk/__init__.py b/src/leapfrogai_sdk/__init__.py index bea8fb196..946c37b42 100644 --- a/src/leapfrogai_sdk/__init__.py +++ b/src/leapfrogai_sdk/__init__.py @@ -58,4 +58,4 @@ ) from leapfrogai_sdk.serve import serve -print("Initializing Leapfrog") +print("Initializing LeapfrogAI") diff --git a/src/leapfrogai_sdk/cli.py b/src/leapfrogai_sdk/cli.py index d2c8912de..48322adad 100644 --- a/src/leapfrogai_sdk/cli.py +++ b/src/leapfrogai_sdk/cli.py @@ -32,7 +32,7 @@ @click.command() def cli(app: str, host: str, port: str, app_dir: str): sys.path.insert(0, app_dir) - """Leapfrog AI CLI""" + """LeapfrogAI CLI""" app = import_app(app) asyncio.run(serve(app(), host, port)) diff --git a/src/leapfrogai_ui/.env.example b/src/leapfrogai_ui/.env.example index 27463adad..442ed5377 100644 --- a/src/leapfrogai_ui/.env.example +++ b/src/leapfrogai_ui/.env.example @@ -15,7 +15,7 @@ SUPABASE_AUTH_KEYCLOAK_CLIENT_ID=uds-supabase SUPABASE_AUTH_KEYCLOAK_SECRET= ORIGIN=http://localhost:5137 -#If specified, app will use OpenAI instead of Leapfrog +#If specified, app will use OpenAI instead of LeapfrogAI OPENAI_API_KEY= # PLAYWRIGHT diff --git a/src/leapfrogai_ui/README.md b/src/leapfrogai_ui/README.md index b4c561af4..a04057950 100644 --- a/src/leapfrogai_ui/README.md +++ b/src/leapfrogai_ui/README.md @@ -1,42 +1,61 @@ # LeapfrogAI UI -## Getting Started +> [!IMPORTANT] +> See the [UI package documentation](../../packages/UI/README.md) for general pre-requisites, dependent components, and package deployment instructions -### Requirements +This document is only applicable for spinning up the UI in a local Node development environment. -This application requires Supabase, and either Leapfrog API or OpenAI to function. Additionally, it can optionally use -Keycloak for authentication. There are several different ways to run it, so please see the "Configuration Options" section -below for more information. +## Local Development Setup -### Running the UI +> [!IMPORTANT] +> Execute the following commands from this sub-directory -1. Change directories: `cd src/leapfrogai_ui` -2. Create a `.env` file at the root of the UI project (src/leapfrogai_ui), reference the `.env.example` file for values to put in the .env file -3. Install dependencies: `npm install` -4. Run `npm run dev -- --open` +### Running + +1. Install dependencies + + ```bash + npm install + ``` + +2. Create a .env using the .env.example as a template. + +3. Run the Node application and open in your default browser + + ```bash + npm run dev -- --open + ``` + +### Building + +To create a production version of the app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. ### Configuration Options +#### API + It is recommended to run LeapfrogAI with UDS, but if you want to run the UI locally (on localhost, e.g. for local development), you can either: -1. Connect to a UDS deployed version of the Leapfrog API and Supabase +1. Connect to a UDS deployed version of the LeapfrogAI API and Supabase **OR** 2. Connect to OpenAI and UDS deployed Supabase or locally running Supabase. -**NOTE:** most data CRUD operations utilize Leapfrog API or OpenAI, but some functionality still depends on a direct connection with Supabase. - -#### UDS Bundle Deployment - -This is the easiest way to use the UI. Follow the README and documentation website for running the entire [LeapfrogAI stack](https://github.com/defenseunicorns/leapfrogai). +**NOTE:** most data CRUD operations utilize LeapfrogAI API or OpenAI, but some functionality still depends on a direct connection with Supabase. -#### Local Development +If running the UI locally and utilizing LeapfrogAI API, **you must use the same Supabase instance that the LeapfrogAI API is utilizing**. -If running the UI locally and utilizing LeapfrogAPI, **you must use the same Supabase instance that the Leapfrog API is utilizing**. +#### Cluster -1. Connect the UI to a UDS deployed version of Supabase and Leapfrog API. +1. Connect the UI to a UDS deployed version of Supabase and LeapfrogAI API. Ensure these env variables are set appropriately in your .env file: ```bash @@ -54,7 +73,34 @@ database properly for you. Further instructions will be coming soon in a future release. -##### Authentication +#### Standalone Supabase + +1. Install [Supabase](https://supabase.com/docs/guides/cli/getting-started?platform=macos) +2. Run: `supabase start` + The configuration files at src/leapfrogai_ui/supabase will ensure your Supabase is configured to work with Keycloak if + you set these .env variables: + +```bash +SUPABASE_AUTH_KEYCLOAK_CLIENT_ID=uds-supabase +SUPABASE_AUTH_KEYCLOAK_SECRET= #this is the client secret for the client in Keycloak +SUPABASE_AUTH_EXTERNAL_KEYCLOAK_URL=https://sso.uds.dev/realms/uds +``` + +After it starts, the Supabase API URL and Anon key are printed to the console. These are used in the .env file to connect to Supabase. + +After starting supabase for the first time, you need to initialize the database with migrations and seed data: + +`supabase db reset` + +After this initial reset, if you start Supabase again it will already have the data and you don't need to run this command unless you want to restore it to the default. + +Stop Supabase: + +`npm run supabase:stop` + +**WARNING:** if switching the application from utilizing LeapfrogAI API to OpenAI or vice versa, and you encounter this error: `Server responded with status code 431. See https://vitejs.dev/guide/troubleshooting.html#_431-request-header-fields-too-large.`, then you need to clear your browser cookies. + +#### Authentication You can choose to use Keycloak (with UDS) or turn Keycloak off and just use Supabase. @@ -68,7 +114,7 @@ run the UI outside of UDS on localhost (e.g. for development work), there are so `http://localhost:5173/auth/callback,http://localhost:4173/auth/callback` **NOTE:** Port 4173 is utilized by Playwright for E2E tests. You do not need this if you are not concerned about Playwright. -###### With Keycloak authentication +##### With KeyCloak 1. If Supabase was deployed with UDS, it will automatically configure a Keycloak Client for you. We need to modify this client to allow localhost URIs. @@ -78,18 +124,18 @@ run the UI outside of UDS on localhost (e.g. for development work), there are so http://localhost:4173/auth/callback (for Playwright tests) 2. If you want to connect Keycloak to a locally running Supabase instance (non UDS deployed), see the "Running Supabase locally" section below. -###### Without Keycloak authentication +##### Without Keycloak 1. To turn off Keycloak, set this .env variable: `PUBLIC_DISABLE_KEYCLOAK=false` -##### Running UI Locally with OpenAI +#### OpenAI Set the following .env variables: ```bash DEFAULT_MODEL=gpt-3.5-turbo LEAPFROGAI_API_BASE_URL=https://api.openai.com -# If specified, app will use OpenAI instead of Leapfrog +# If specified, app will use OpenAI instead of LeapfrogAI OPENAI_API_KEY= ``` @@ -101,52 +147,13 @@ PUBLIC_SUPABASE_URL=https://supabase-kong.uds.dev PUBLIC_SUPABASE_ANON_KEY= ``` -Running Supabase locally: - -1. Install [Supabase](https://supabase.com/docs/guides/cli/getting-started?platform=macos) -2. Run: `supabase start` - The configuration files at src/leapfrogai_ui/supabase will ensure your Supabase is configured to work with Keycloak if - you set these .env variables: - -```bash -SUPABASE_AUTH_KEYCLOAK_CLIENT_ID=uds-supabase -SUPABASE_AUTH_KEYCLOAK_SECRET= #this is the client secret for the client in Keycloak -SUPABASE_AUTH_EXTERNAL_KEYCLOAK_URL=https://sso.uds.dev/realms/uds -``` - -After it starts, the Supabase API URL and Anon key are printed to the console. These are used in the .env file to connect to Supabase. - -After starting supabase for the first time, you need to initialize the database with migrations and seed data: - -`supabase db reset` - -After this initial reset, if you start Supabase again it will already have the data and you don't need to run this command unless you want to restore it to the default. - -Stop Supabase: - -`npm run supabase:stop` - -**WARNING:** if switching the application from utilizing Leapfrog API to OpenAI or vice versa, and you encounter this error: `Server responded with status code 431. See https://vitejs.dev/guide/troubleshooting.html#_431-request-header-fields-too-large.`, then you need to clear your browser cookies. - -### Building - -To create a production version of the app: - -```bash -npm run build -``` - -You can preview the production build with `npm run preview`. - -## Developer Notes - -### Tooling +## Notes and Troubleshooting ### Supabase We use Supabase for authentication and a database. Application specific data (ex. user profile images, application settings like feature flags, etc..) should be stored directly in Supabase and -would not normally utilize the Leapfrog API for CRUD operations. +would not normally utilize the LeapfrogAI API for CRUD operations. ### Playwright End-to-End Tests @@ -164,7 +171,7 @@ Notes: .env file. See the "Configuration Options" section above to configure which database Playwright is using. 2. If you run the tests in headless mode (`npm run test:integration`) you do not need the app running, it will build the app and run on port 4173. -# Supabase and Keycloak Integration +### Supabase and Keycloak Integration The Supabase docs are inadequate for properly integrating with Keycloak. Additionally, they only support integration with the Supabase Cloud SAAS offering. Before reading the section below, first reference the [Supabase docs](https://supabase.com/docs/guides/auth/social-login/auth-keycloak). @@ -265,7 +272,7 @@ When scanning the QR code, use an app that lets you see the url of the QR code. Login flow was adapted from [this reference](https://supabase.com/docs/guides/getting-started/tutorials/with-sveltekit?database-method=sql) -## Chat Data Flow +### Chat Data Flow The logic for handling regular chat messages and assistant chat messages, along with persisting that data to the database is complex and deserves a detailed explanation. diff --git a/src/leapfrogai_ui/playwright.config.ts b/src/leapfrogai_ui/playwright.config.ts index 27f1dff05..f544586ba 100644 --- a/src/leapfrogai_ui/playwright.config.ts +++ b/src/leapfrogai_ui/playwright.config.ts @@ -45,7 +45,7 @@ const chromeConfig = { const defaultConfig: PlaywrightTestConfig = { // running more than 1 worker can cause flakiness due to test files being run at the same time in different browsers // (e.x. navigation history is incorrect) - // Additionally, Leapfrog API is slow when attaching files to assistants, resulting in flaky tests + // Additionally, LeapfrogAI API is slow when attaching files to assistants, resulting in flaky tests // We can attempt in increase number of browser and workers in the pipeline when the API is faster workers: 1, projects: [ diff --git a/src/leapfrogai_ui/src/lib/components/Message.svelte b/src/leapfrogai_ui/src/lib/components/Message.svelte index 79fe0c88e..bf2c939f1 100644 --- a/src/leapfrogai_ui/src/lib/components/Message.svelte +++ b/src/leapfrogai_ui/src/lib/components/Message.svelte @@ -130,7 +130,7 @@ {:else} - LeapfrogAI + LeapfrogAI {/if}
diff --git a/src/leapfrogai_ui/tests/chat.test.ts b/src/leapfrogai_ui/tests/chat.test.ts index bb096963d..bae117f05 100644 --- a/src/leapfrogai_ui/tests/chat.test.ts +++ b/src/leapfrogai_ui/tests/chat.test.ts @@ -99,7 +99,7 @@ test('it cancels responses when clicking enter instead of pause button and does await deleteActiveThread(page, openAIClient); }); -// TODO - Leapfrog API is currently too slow when sending assistant responses so when this test +// TODO - LeapfrogAI API is currently too slow when sending assistant responses so when this test // runs with multiple browsers in parallel, it times out. It should usually work for individual // browsers unless the API is receiving additional run requests simultaneously test('it can switch between normal chat and chat with an assistant', async ({ @@ -130,7 +130,7 @@ test('it can switch between normal chat and chat with an assistant', async ({ await expect(messages).toHaveCount(4); await expect(page.getByTestId('user-icon')).toHaveCount(2); - await expect(page.getByTestId('leapfrog-icon')).toHaveCount(1); + await expect(page.getByTestId('leapfrogai-icon')).toHaveCount(1); await expect(page.getByTestId('assistant-icon')).toHaveCount(1); // Test selected assistant has a checkmark and clicking it again de-selects the assistant @@ -145,7 +145,7 @@ test('it can switch between normal chat and chat with an assistant', async ({ await expect(messages).toHaveCount(6); await expect(page.getByTestId('user-icon')).toHaveCount(3); - await expect(page.getByTestId('leapfrog-icon')).toHaveCount(2); + await expect(page.getByTestId('leapfrogai-icon')).toHaveCount(2); await expect(page.getByTestId('assistant-icon')).toHaveCount(1); // Cleanup diff --git a/src/leapfrogai_ui/tests/helpers/fileHelpers.ts b/src/leapfrogai_ui/tests/helpers/fileHelpers.ts index 14df9ea01..4afa1be4e 100644 --- a/src/leapfrogai_ui/tests/helpers/fileHelpers.ts +++ b/src/leapfrogai_ui/tests/helpers/fileHelpers.ts @@ -17,7 +17,7 @@ export const uploadFileWithApi = async (filename = 'test.pdf', openAIClient: Ope type: 'application/pdf' }); - // This can also be done IAW the OpenAI API documentation with fs.createReadStream, but Leapfrog API does not currently + // This can also be done IAW the OpenAI API documentation with fs.createReadStream, but LeapfrogAI API does not currently // support a ReadStream. Open Issue: https://github.com/defenseunicorns/leapfrogai/issues/710 return openAIClient.files.create({ @@ -78,7 +78,7 @@ export const createExcelFile = (options: CreateFileOptions = {}) => { const filenameWithExtension = `${filename}${extension}`; const workbook = XLSX.utils.book_new(); - const worksheet = XLSX.utils.json_to_sheet([{ Name: 'Leapfrog', Age: 1, Type: 'AI' }]); + const worksheet = XLSX.utils.json_to_sheet([{ Name: 'LeapfrogAI', Age: 1, Type: 'AI' }]); XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1'); XLSX.writeFile(workbook, `./tests/fixtures/${filenameWithExtension}`); diff --git a/website/content/en/docs/local-deploy-guide/components.md b/website/content/en/docs/local-deploy-guide/components.md index 4cd5c9153..e161af589 100644 --- a/website/content/en/docs/local-deploy-guide/components.md +++ b/website/content/en/docs/local-deploy-guide/components.md @@ -1,7 +1,7 @@ --- title: Components type: docs -weight: 3 +weight: 4 --- ## Components diff --git a/website/content/en/docs/local-deploy-guide/dependencies.md b/website/content/en/docs/local-deploy-guide/dependencies.md index 459506488..03c4636ca 100644 --- a/website/content/en/docs/local-deploy-guide/dependencies.md +++ b/website/content/en/docs/local-deploy-guide/dependencies.md @@ -1,7 +1,7 @@ --- title: Dependencies type: docs -weight: 5 +weight: 2 --- This documentation addresses the local deployment dependencies of LeapfrogAI, a self-hosted generative AI platform. LeapfrogAI extends the diverse capabilities and modalities of AI models to various environments, ranging from cloud-based deployments to servers with ingress and egress limitations. With LeapfrogAI, teams can deploy APIs aligned with OpenAI's API specifications, empowering teams to create and utilize tools compatible with nearly any model and code library available. Importantly, all operations take place locally, ensuring users can maintain the security of their information and sensitive data within their own environments diff --git a/website/content/en/docs/local-deploy-guide/quick_start.md b/website/content/en/docs/local-deploy-guide/quick_start.md index bfb692c97..83313961f 100644 --- a/website/content/en/docs/local-deploy-guide/quick_start.md +++ b/website/content/en/docs/local-deploy-guide/quick_start.md @@ -1,7 +1,7 @@ --- title: Quick Start type: docs -weight: 2 +weight: 3 --- # LeapfrogAI UDS Deployment @@ -32,8 +32,7 @@ Examples of other models to put into vLLM or LLaMA C++ Python that are not spons - [justinthelaw/Phi-3-mini-128k-instruct-4bit-128g](https://huggingface.co/justinthelaw/Phi-3-mini-128k-instruct-4bit-128g) - [NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO-GGUF](https://huggingface.co/NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO-GGUF) -> [!CAUTION] -> The default configuration when deploying with GPU support assumes a single GPU. `vllm` is assigned the GPU resource. GPU workloads **_WILL NOT_** run if GPU resources are unavailable to the pod(s). You must provide sufficient NVIDIA GPU scheduling or else the pod(s) will go into a crash loop. See the [Dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/) and [Requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) pages for more details. +The default configuration when deploying with GPU support assumes a single GPU. `vllm` is assigned the GPU resource. GPU workloads **_WILL NOT_** run if GPU resources are unavailable to the pod(s). You must provide sufficient NVIDIA GPU scheduling or else the pod(s) will go into a crash loop. See the [Dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/) and [Requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) pages for more details. ## Building the UDS Bundle diff --git a/website/content/en/docs/local-deploy-guide/requirements.md b/website/content/en/docs/local-deploy-guide/requirements.md index 1df1fb0ef..74181dce3 100644 --- a/website/content/en/docs/local-deploy-guide/requirements.md +++ b/website/content/en/docs/local-deploy-guide/requirements.md @@ -1,7 +1,7 @@ --- title: Requirements type: docs -weight: 4 +weight: 1 --- Prior to deploying LeapfrogAI, ensure that the following tools, packages, and requirements are met and present in your environment. See the [Dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/) page fro more details. From 89ed08d88b7590329e82abaf2179988b407a52de Mon Sep 17 00:00:00 2001 From: Justin Law Date: Thu, 15 Aug 2024 12:57:40 -0400 Subject: [PATCH 10/30] explicit main readme instructs for local dev --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 94c2925e7..af3cc5f1a 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ leapfrogai/ │ ├── ui/ # deployment infrastructure for the UI │ ├── vllm/ # source code & deployment infrastructure for the vllm backend │ └── whisper/ # source code & deployment infrastructure for the whisper backend -├── bundles/ +├── uds-bundles/ │ ├── dev/ # uds bundles for local uds dev deployments │ └── latest/ # uds bundles for the most current uds deployments ├── Makefile @@ -68,7 +68,7 @@ leapfrogai/ The preferred method for running LeapfrogAI is a local [Kubernetes](https://kubernetes.io/) deployment using [UDS](https://github.com/defenseunicorns/uds-core). -Please refer to the [Quick Start](https://docs.leapfrog.ai/docs/local-deploy-guide/quick_start/) section of the LeapfrogAI documentation site for system requirements and instructions. +Please refer to the [Quick Start](https://docs.leapfrog.ai/docs/local-deploy-guide/quick_start/) section of the LeapfrogAI documentation website for system requirements and instructions. ## Components @@ -109,9 +109,14 @@ For contributing and local deployment and development for each component in a lo ## Local Development +> [!NOTE] +> Please start with the [LeapfrogAI documentation website](https://docs.leapfrog.ai/docs/local-deploy-guide/) prior to attempting local development + Each of the LeapfrogAI components can also be run individually outside of a Kubernetes or Containerized environment. This is useful when testing changes to a specific component, but will not assist in a full deployment of LeapfrogAI. Please refer to the [above section](#usage) for deployment instructions. Please refer to the [next section](#contributing) for rules on contributing to LeapfrogAI. -**_First_** refer to the [DEVELOPMENT.md](docs/DEVELOPMENT.md) document for general development details, **_then_** refer to the linked READMEs for each individual packages local development instructions: +**_First_** refer to the [DEVELOPMENT.md](docs/DEVELOPMENT.md) document for general development details. + +**_Then_** refer to the linked READMEs for each individual packages local development instructions. - [SDK](src/leapfrogai_sdk/README.md)[^2] - [API](packages/api/README.md)[^3] From 1412ed14bc4111d0f4ef6fe4e930321ebcc1e0c5 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Thu, 15 Aug 2024 13:12:44 -0400 Subject: [PATCH 11/30] minor typo, add preferred dev method --- README.md | 2 +- docs/DEVELOPMENT.md | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index af3cc5f1a..e6474f9d0 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Each of the LeapfrogAI components can also be run individually outside of a Kube **_First_** refer to the [DEVELOPMENT.md](docs/DEVELOPMENT.md) document for general development details. -**_Then_** refer to the linked READMEs for each individual packages local development instructions. +**_Then_** refer to the linked READMEs for each individual package's local development instructions. - [SDK](src/leapfrogai_sdk/README.md)[^2] - [API](packages/api/README.md)[^3] diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index b32a60e5e..f7b15bb94 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -61,6 +61,14 @@ uds zarf package pull oci://ghcr.io/defenseunicorns/leapfrogai/leapfrogai-api:la uds zarf package deploy zarf-package-*.tar.zst --confirm ``` +## In-Cluster Components + +All in-cluster components can be accessed without port-forwarding if [UDS Core Slim Dev](../packages/k3d-gpu/README.md) is installed with LeapfrogAI packages. + +For example, when developing the API and you need access to Supabase, you can point your locally running API to the in-cluster Supabase by setting the Supabase base URL to the in-cluster domain (https://supabase-kong.uds.dev). + +The preferred method of testing changes is to fully deploy something to a cluster and run local smoke tests as needed. The GitHub workflows will run all integration and E2E test suites. + ## Troubleshooting Occasionally, a package you are trying to re-deploy, or a namespace you are trying to delete, may hang. To workaround this, be sure to check the events and logs of all resources, to include pods, deployments, daemonsets, clusterpolicies, etc. There may be finalizers, Pepr hooks, and etc. causing the re-deployment or deletion to fail. Use the [`k9s`](https://k9scli.io/topics/commands/) and `kubectl` tools that are vendored with UDS CLI, like in the examples below: From a6bf913f93f54c98aa8ee4765dd555dcd50850fb Mon Sep 17 00:00:00 2001 From: Justin Law Date: Thu, 15 Aug 2024 18:31:26 -0400 Subject: [PATCH 12/30] many fixes --- Makefile | 2 +- docs/DEVELOPMENT.md | 82 ++++++++++++++++++- packages/llama-cpp-python/Makefile | 3 +- packages/llama-cpp-python/README.md | 11 +-- packages/llama-cpp-python/pyproject.toml | 2 +- packages/repeater/README.md | 4 - packages/supabase/README.md | 15 ++-- packages/text-embeddings/Makefile | 3 +- packages/text-embeddings/README.md | 7 +- packages/vllm/Makefile | 3 +- packages/vllm/README.md | 9 +- packages/vllm/pyproject.toml | 2 +- packages/whisper/Makefile | 2 +- packages/whisper/README.md | 12 ++- src/leapfrogai_api/Makefile | 75 ++++++++--------- src/leapfrogai_api/README.md | 40 ++++----- src/leapfrogai_api/pyproject.toml | 9 ++ src/leapfrogai_ui/README.md | 20 +++-- tests/{make-tests.mk => Makefile} | 9 +- tests/README.md | 51 ++++++++++++ tests/e2e/README.md | 41 ---------- tests/load/README.md | 21 ++--- .../docs/local-deploy-guide/dependencies.md | 28 ++++--- 23 files changed, 270 insertions(+), 181 deletions(-) rename tests/{make-tests.mk => Makefile} (83%) create mode 100644 tests/README.md delete mode 100644 tests/e2e/README.md diff --git a/Makefile b/Makefile index bf5442755..3515ff30a 100644 --- a/Makefile +++ b/Makefile @@ -164,7 +164,7 @@ build-gpu: build-supabase build-api build-ui build-vllm build-text-embeddings bu build-all: build-cpu build-gpu ## Build all of the LFAI packages -include tests/make-tests.mk +include tests/Makefile include packages/k3d-gpu/Makefile diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index f7b15bb94..3241cdf3b 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -7,7 +7,32 @@ The purpose of this document is to describe how to run a development loop on the ## Local Development -Please first see the pre-requisites listed on the LeapfrogAI documentation website's [Requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) and [Dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/), before going to each component's subdirectory README. +Please first see the pre-requisites listed on the LeapfrogAI documentation website's [Requirements](https://docs.leapfrog.ai/docs/local-deploy-guide/requirements/) and [Dependencies](https://docs.leapfrog.ai/docs/local-deploy-guide/dependencies/), before going to each component's subdirectory README + +## PyEnv + +It is **_HIGHLY RECOMMENDED_** that PyEnv be installed on your machine, and a new virtual environment is created for every new development branch. + +Follow the installation instructions outlined in the [pyenv](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) repository to install Python 3.11.6: + + ```bash + # install the correct python version + pyenv install 3.11.6 + + # create a new virtual environment named "leapfrogai" + pyenv virtualenv 3.11.6 leapfrogai + + # activate the virtual environment + pyenv activate leapfrogai + ``` + +If your installation process completes successfully but indicates missing packages such as `sqlite3`, execute the following command to install the required packages then proceed with the reinstallation of Python 3.11.6: + + ```bash + sudo apt-get install build-essential zlib1g-dev libffi-dev \ + libssl-dev libbz2-dev libreadline-dev libsqlite3-dev \ + liblzma-dev libncurses-dev + ``` ## UDS CLI Aliasing @@ -31,12 +56,18 @@ echo -e '#!/bin/bash\nuds zarf tools kubectl "$@"' > /usr/local/bin/kubectl chmod +x /usr/local/bin/kubectl ``` -## Makefile +## Makefiles Many of the directories and sub-directories within this project contain Make targets that can be executed to simplify repetitive command-line tasks. Please refer to each Makefile for more arguments and details on what each target does and is dependent on. +## Environment Variables + +Be wary of `*config*.yaml` or `.env*` files that are in individual components of the stack. The component's README will usually tell the developer when to fill them out or supply environment variables to a script. + +For example, the LeapfrogAI API requires a `config.yaml` be supplied when spun up locally. Use the `config.example.yaml` as an example, and make sure the [ports chosen for applicable backends do not conflict on localhost](#port-conflicts). + ## Package Development If you don't want to build an entire bundle, or you want to dev-loop on a single package in an existing [UDS Kubernetes cluster](../packages/k3d-gpu/README.md) you can do so by performing the following. @@ -61,14 +92,57 @@ uds zarf package pull oci://ghcr.io/defenseunicorns/leapfrogai/leapfrogai-api:la uds zarf package deploy zarf-package-*.tar.zst --confirm ``` -## In-Cluster Components +## Access -All in-cluster components can be accessed without port-forwarding if [UDS Core Slim Dev](../packages/k3d-gpu/README.md) is installed with LeapfrogAI packages. +All LeapfrogAI components exposed as `VirtualService` resources can be accessed without port-forwarding if [UDS Core Slim Dev](../packages/k3d-gpu/README.md) is installed with LeapfrogAI packages. For example, when developing the API and you need access to Supabase, you can point your locally running API to the in-cluster Supabase by setting the Supabase base URL to the in-cluster domain (https://supabase-kong.uds.dev). The preferred method of testing changes is to fully deploy something to a cluster and run local smoke tests as needed. The GitHub workflows will run all integration and E2E test suites. +### Backends + +#### Cluster + +The model backends are the only components within the LeapfrogAI stack that are not readily accessible via a `VirtualService`. These must be port-forwarded if a user wants to test a local deployment of the API against an in-cluster backend. + +For example, the following bash script can be used to setup CPU RAG: + +```bash +#!/bin/bash + +# Function to kill all background processes when the script exits or is interrupted +cleanup() { + echo "Cleaning up..." + kill $PID1 $PID2 +} + +# Set environment variables +export SUPABASE_URL="https://supabase-kong.uds.dev" +export SUPABASE_ANON_KEY=$(kubectl get secret supabase-bootstrap-jwt -n leapfrogai -o jsonpath='{.data.anon-key}' | base64 --decode) + +# Trap SIGINT (Ctrl-C) and SIGTERM (termination signal) to call the cleanup function +trap cleanup SIGINT SIGTERM + +# Start kubectl and uvicorn services in the background and save their PIDs +# Expose the backends at different ports to prevent localhost conflict +kubectl port-forward svc/text-embeddings-model -n leapfrogai 50052:50051 & +PID1=$! +kubectl port-forward svc/llama-cpp-python-model -n leapfrogai 50051:50051 & +PID2=$! + +# Wait for all background processes to finish +wait $PID1 $PID2 +``` + +#### Locally + +Backends can also be run locally as Python applications. See each model backend's README in the `packages/` directory for more details on running each in development mode. + +#### Port Conflicts + +In all cases, port conflicts may arise when outside of a cluster service mesh. As seen in the [Cluster sub-section](#cluster), backends all try to emit at port `50051`; however, on a host machine's localhost, there can only be one on 50051. Using the [Leapfrogai API](../src/leapfrogai_api/config.example.yaml), define the ports at which you plan on making a backend accessible. + ## Troubleshooting Occasionally, a package you are trying to re-deploy, or a namespace you are trying to delete, may hang. To workaround this, be sure to check the events and logs of all resources, to include pods, deployments, daemonsets, clusterpolicies, etc. There may be finalizers, Pepr hooks, and etc. causing the re-deployment or deletion to fail. Use the [`k9s`](https://k9scli.io/topics/commands/) and `kubectl` tools that are vendored with UDS CLI, like in the examples below: diff --git a/packages/llama-cpp-python/Makefile b/packages/llama-cpp-python/Makefile index 780f8c36a..b7ab569eb 100644 --- a/packages/llama-cpp-python/Makefile +++ b/packages/llama-cpp-python/Makefile @@ -1,7 +1,6 @@ install: python -m pip install ../../src/leapfrogai_sdk - python -m pip install -e . + python -m pip install -e ".[dev]" dev: - make install python -m leapfrogai_sdk.cli --app-dir=. main:Model diff --git a/packages/llama-cpp-python/README.md b/packages/llama-cpp-python/README.md index 7ddd3dfe5..24917fbcb 100644 --- a/packages/llama-cpp-python/README.md +++ b/packages/llama-cpp-python/README.md @@ -47,17 +47,14 @@ To run the llama-cpp-python backend locally: > Execute the following commands from this sub-directory ```bash -# Setup Virtual Environment -python -m venv .venv -source .venv/bin/activate - -pip install 'huggingface_hub[cli,hf_transfer]' # Used to download the model weights from huggingface +# Install dev and runtime dependencies +make install # Clone Model -# Supply a REPO_ID, FILENAME and REVISION if a different model is desired +# Supply a REPO_ID, FILENAME and REVISION, as seen in the "Model Selection" section python scripts/model_download.py mv .model/*.gguf .model/model.gguf -# Install dependencies and start the model backend +# Start the model backend make dev ``` diff --git a/packages/llama-cpp-python/pyproject.toml b/packages/llama-cpp-python/pyproject.toml index c47c875f4..58852aa65 100644 --- a/packages/llama-cpp-python/pyproject.toml +++ b/packages/llama-cpp-python/pyproject.toml @@ -15,7 +15,7 @@ readme = "README.md" [project.optional-dependencies] dev = [ - "huggingface_hub", + "huggingface_hub[cli,hf_transfer]" ] [tool.pip-tools] diff --git a/packages/repeater/README.md b/packages/repeater/README.md index 187265add..b11de60d3 100644 --- a/packages/repeater/README.md +++ b/packages/repeater/README.md @@ -34,10 +34,6 @@ To run the repeater backend locally: > Execute the following commands from this sub-directory ```bash -# Setup Virtual Environment -python -m venv .venv -source .venv/bin/activate - # Install dependencies and start the model backend make dev ``` diff --git a/packages/supabase/README.md b/packages/supabase/README.md index 9630d1b36..7e5da3da5 100644 --- a/packages/supabase/README.md +++ b/packages/supabase/README.md @@ -38,18 +38,15 @@ Go to `https://supabase-kong.uds.dev`. The login is `supabase-admin` the passwor - If you cannot reach `https://supabase-kong.uds.dev`, check if the `Packages` CRDs and `VirtualServices` contain `supabase-kong.uds.dev`. If they do not, try restarting the `pepr-uds-core-watcher` pod. - If logging in to the UI through keycloak returns a `500`, check and see if the `sql` migrations have been run in Supabase. - You can find those in `leapfrogai/src/leapfrogai_ui/supabase/migrations` - Migrations can be run in the Supabase studio SQL editor -- To obtain an API (JWT) token for testing, create a test user and run the following: +- To obtain a 1-hour JWT for testing run the following: ```bash - # Grab the Supabase Anon Key from the JWT Secret in the UDS Kubernetes cluster - export ANON_KEY=$(uds zarf tools kubectl get secret -n leapfrogai supabase-bootstrap-jwt -o json | uds zarf tools yq '.data.anon-key' | base64 -d) - - # Replace and / with your desired credentials - # only lasts 1 hour from creation - curl -X POST 'https://supabase-kong.uds.dev/auth/v1/signup' \ - -H "apikey: " \ + # Replace , , and with your desired credentials + # Grab the Supabase Anon Key from the JWT Secret in the UDS Kubernetes cluster and use it with xargs + uds zarf tools kubectl get secret -n leapfrogai supabase-bootstrap-jwt -o json | uds zarf tools yq '.data.anon-key' | base64 -d | xargs -I {} curl -X POST 'https://supabase-kong.uds.dev/auth/v1/signup' \ + -H "apikey: {}" \ -H "Content-Type: application/json" \ - -H "Authorization": f"Bearer $ANON_KEY" \ + -H "Authorization: Bearer {}" \ -d '{ "email": "", "password": "", "confirmPassword": ""}' ``` diff --git a/packages/text-embeddings/Makefile b/packages/text-embeddings/Makefile index 780f8c36a..19600e1d3 100644 --- a/packages/text-embeddings/Makefile +++ b/packages/text-embeddings/Makefile @@ -1,6 +1,7 @@ install: python -m pip install ../../src/leapfrogai_sdk - python -m pip install -e . + python -m pip install -e ".[dev]" + dev: make install diff --git a/packages/text-embeddings/README.md b/packages/text-embeddings/README.md index 98db16469..e71256a76 100644 --- a/packages/text-embeddings/README.md +++ b/packages/text-embeddings/README.md @@ -38,13 +38,12 @@ To run the text-embeddings backend locally: > Execute the following commands from this sub-directory ```bash -# Setup Virtual Environment -python -m venv .venv -source .venv/bin/activate +# Install dev and runtime dependencies +make install # Clone Model python scripts/model_download.py -# Install dependencies and start the model backend +# Start the model backend make dev ``` diff --git a/packages/vllm/Makefile b/packages/vllm/Makefile index 0434d4101..98e8b29db 100644 --- a/packages/vllm/Makefile +++ b/packages/vllm/Makefile @@ -1,7 +1,6 @@ install: python -m pip install ../../src/leapfrogai_sdk - python -m pip install -e . + python -m pip install -e ".[dev]" dev: - make install python -m leapfrogai_sdk.cli --app-dir=src/ main:Model diff --git a/packages/vllm/README.md b/packages/vllm/README.md index b4cc7417b..673d7bc01 100644 --- a/packages/vllm/README.md +++ b/packages/vllm/README.md @@ -45,15 +45,12 @@ To run the vllm backend locally: > Execute the following commands from this sub-directory ```bash -# Setup Virtual Environment -python -m venv .venv -source .venv/bin/activate - -pip install 'huggingface_hub[cli,hf_transfer]' # Used to download the model weights from huggingface +# Install dev and runtime dependencies +make install # Clone Model python scripts/model_download.py -# Install dependencies and start the model backend +# Start the model backend make dev ``` diff --git a/packages/vllm/pyproject.toml b/packages/vllm/pyproject.toml index f0b347a7a..a74ec3c24 100644 --- a/packages/vllm/pyproject.toml +++ b/packages/vllm/pyproject.toml @@ -19,7 +19,7 @@ readme = "README.md" [project.optional-dependencies] dev = [ - "huggingface_hub", + "huggingface_hub[cli,hf_transfer]" ] [tool.pip-tools] diff --git a/packages/whisper/Makefile b/packages/whisper/Makefile index 0434d4101..730458a21 100644 --- a/packages/whisper/Makefile +++ b/packages/whisper/Makefile @@ -1,6 +1,6 @@ install: python -m pip install ../../src/leapfrogai_sdk - python -m pip install -e . + python -m pip install -e ".[dev]" dev: make install diff --git a/packages/whisper/README.md b/packages/whisper/README.md index 0dff5a900..bdb9ca960 100644 --- a/packages/whisper/README.md +++ b/packages/whisper/README.md @@ -35,15 +35,13 @@ uds zarf package deploy packages/whisper/zarf-package-whisper-*-dev.tar.zst --co To run the vllm backend locally without K8s (starting from the root directory of the repository): ```bash -# Setup Virtual Environment -python -m venv .venv -source .venv/bin/activate - -pip install 'ctranslate2' # Used to download and convert the model weights -pip install 'transformers[torch]' # Used to download and convert the model weights +# Install dev and runtime dependencies +make install +# Download and convert model +# Change the value for --model to change the whisper base ct2-transformers-converter --model openai/whisper-base --output_dir .model --copy_files tokenizer.json --quantization float32 -# Install dependencies and start the model backend +# Start the model backend make dev ``` diff --git a/src/leapfrogai_api/Makefile b/src/leapfrogai_api/Makefile index 3ae197d42..afddd176e 100644 --- a/src/leapfrogai_api/Makefile +++ b/src/leapfrogai_api/Makefile @@ -1,43 +1,44 @@ -MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) -SHELL := /bin/bash +API_PORT := 3000 +API_BASE_URL := https://leapfrogai-api.uds.dev +SUPABASE_BASE_URL := https://supabase-kong.uds.dev +EXPIRATION_TIME := $(shell date -d "+30 days" +%s) +SUPABASE_ANON_KEY := $(shell uds zarf tools kubectl get secret -n leapfrogai supabase-bootstrap-jwt -o json | uds zarf tools yq '.data.anon-key' | base64 -d) -export SUPABASE_URL=$(shell supabase status | grep -oP '(?<=API URL: ).*') -export SUPABASE_ANON_KEY=$(shell supabase status | grep -oP '(?<=anon key: ).*') - -install-api: - @cd ${MAKEFILE_DIR} && \ +install: set-env python -m pip install ../../src/leapfrogai_sdk - @cd ${MAKEFILE_DIR} && \ - python -m pip install -e . - python -m pip install "../../.[dev]" - -dev-run-api: - @cd ${MAKEFILE_DIR} && \ - python -m uvicorn main:app --port 3000 --reload --log-level info + python -m pip install -e ".[dev]" -define get_jwt_token - echo "Getting JWT token from ${SUPABASE_URL}..."; \ - TOKEN_RESPONSE=$$(curl -s -X POST $(1) \ - -H "apikey: ${SUPABASE_ANON_KEY}" \ - -H "Content-Type: application/json" \ - -d '{ "email": "admin@localhost", "password": "$$SUPABASE_PASS"}'); \ - echo "Extracting token from $(TOKEN_RESPONSE)"; \ - JWT=$$(echo $$TOKEN_RESPONSE | grep -oP '(?<="access_token":")[^"]*'); \ - echo -n "$$JWT" | xclip -selection clipboard; \ - echo "SUPABASE_USER_JWT=$$JWT" > .env; \ - echo "SUPABASE_URL=$$SUPABASE_URL" >> .env; \ - echo "SUPABASE_ANON_KEY=$$SUPABASE_ANON_KEY" >> .env; \ - echo "DONE - JWT token copied to clipboard" -endef +set-env: + echo "SUPABASE_URL=${SUPABASE_BASE_URL}" > .env + echo "SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}" >> .env -user: - @read -s -p "Enter a new DEV API password: " SUPABASE_PASS; echo; \ - echo "Creating new supabase user..."; \ - $(call get_jwt_token,"${SUPABASE_URL}/auth/v1/signup") +dev: set-env + . ./.env && python -m uvicorn main:app --port ${API_PORT} --reload --log-level info -env: - @read -s -p "Enter your DEV API password: " SUPABASE_PASS; echo; \ - $(call get_jwt_token,"${SUPABASE_URL}/auth/v1/token?grant_type=password") +api-key: + curl -s -X POST '${SUPABASE_BASE_URL}/auth/v1/signup' \ + -H "apikey: ${SUPABASE_ANON_KEY}" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SUPABASE_ANON_KEY}" \ + -d '{ "email": "leapfrogai@defenseunicorns.com", "password": "password", "confirmPassword": "password"}' | \ + uds zarf tools yq '.access_token' | \ + xargs -I {} curl -s --insecure -X POST '${API_BASE_URL}/leapfrogai/v1/auth/api-keys' \ + -H "apikey: {}" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {}" \ + -d '{ "name": "api-key", "expires_at": "${EXPIRATION_TIME}" }' | \ + uds zarf tools yq '.api_key' -test-integration: - @cd ${MAKEFILE_DIR} && python -m pytest ../../tests/integration/api/ -vv -s +new-api-key: + curl -s -X POST '${SUPABASE_BASE_URL}/auth/v1/token?grant_type=password' \ + -H "apikey: ${SUPABASE_ANON_KEY}" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SUPABASE_ANON_KEY}" \ + -d '{ "email": "leapfrogai@defenseunicorns.com", "password": "password"}' | \ + uds zarf tools yq '.access_token' | \ + xargs -I {} curl -s --insecure -X POST '${API_BASE_URL}/leapfrogai/v1/auth/api-keys' \ + -H "apikey: {}" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {}" \ + -d '{ "name": "api-key", "expires_at": "${EXPIRATION_TIME}" }' | \ + uds zarf tools yq '.api_key' diff --git a/src/leapfrogai_api/README.md b/src/leapfrogai_api/README.md index b6179457e..87b3f019d 100644 --- a/src/leapfrogai_api/README.md +++ b/src/leapfrogai_api/README.md @@ -12,10 +12,13 @@ This document is only applicable for spinning up the API in a local Python devel ### Running +> [!IMPORTANT] +> The following steps assume that you already have a deployed and accessible UDS Kubernetes cluster and LeapfrogAI. Please follow the steps within the LeapfrogAI documentation website for details. + 1. Install dependencies ```bash - make install-api + make install ``` 2. Create a config.yaml using the config.example.yaml as a template. @@ -23,40 +26,33 @@ This document is only applicable for spinning up the API in a local Python devel 3. Run the FastAPI application ```bash - make dev-run-api + make dev API_PORT=3000 ``` -4. Create a local Supabase user +4. Create an API key with test user "leapfrogai@defenseunicorns.com" and test password "password", lasting 30 days from creation time ```bash - make user + # If the in-cluster API is up, and not testing the API workflow + make api-key API_BASE_URL=https://leapfrogai-api.uds.dev ``` -5. Create an API (JWT) token + To create a new 30-day API key, use the following: ```bash - make jwt - source .env + # If the in-cluster API is up, and not testing the API workflow + make new-api-key API_BASE_URL=https://leapfrogai-api.uds.dev ``` - This will copy the JWT token to your clipboard. - -6. Make calls to the api swagger endpoint at `http://localhost:8080/docs` using your JWT token as the `HTTPBearer` token. - * Hit `Authorize` on the swagger page to enter your JWT token + The newest API key will be printed to a `.env` file located within this directory. -### Integration Tests +5. Make calls to the API Swagger endpoint at `http://localhost:8080/docs` using your API token as the `HTTPBearer` token. -The integration tests serve to verify API functionality and compatibility with other existing components: + - Hit `Authorize` on the Swagger page to enter your API key -* Check all API routes -* Validate Request/Response objects -* Database CRUD operations -* Schema mismatches +### Access -#### Running the tests +See the Access section of the [DEVELOPMENT.md](../../docs/DEVELOPMENT.md) for different ways to connect the API to a model backend or Supabase. -After obtaining the JWT token, run the following: +### Tests -```bash -make test-integration -``` +See the [tests directory documentation](../../tests/README.md) for more details. diff --git a/src/leapfrogai_api/pyproject.toml b/src/leapfrogai_api/pyproject.toml index d031f87d1..0c32738cf 100644 --- a/src/leapfrogai_api/pyproject.toml +++ b/src/leapfrogai_api/pyproject.toml @@ -29,6 +29,15 @@ dependencies = [ ] requires-python = "~=3.11" +[project.optional-dependencies] +dev = [ + "locust", + "pytest-asyncio", + "requests", + "requests-toolbelt", + "pytest" +] + [tool.pip-tools] generate-hashes = true diff --git a/src/leapfrogai_ui/README.md b/src/leapfrogai_ui/README.md index a04057950..11a9ac1cf 100644 --- a/src/leapfrogai_ui/README.md +++ b/src/leapfrogai_ui/README.md @@ -157,13 +157,21 @@ would not normally utilize the LeapfrogAI API for CRUD operations. ### Playwright End-to-End Tests -First install Playwright: `npm init playwright@latest` +1. Install Playwright -To run the E2E tests: -`npm run test:integration:ui` -Click the play button in the Playwright UI. -Playwright will run it's own production build and server the app at `http://localhost:4173`. If you make server side changes, -restart playwright for them to take effect. + ```bash + npm init playwright@latest + ``` + +2. Run the E2E tests: + + ```bash + npm run test:integration:ui + ``` + + Click the play button in the Playwright UI. + Playwright will run it's own production build and server the app at `http://localhost:4173`. If you make server side changes, + restart playwright for them to take effect. Notes: diff --git a/tests/make-tests.mk b/tests/Makefile similarity index 83% rename from tests/make-tests.mk rename to tests/Makefile index 00b24dc1a..b56a13761 100644 --- a/tests/make-tests.mk +++ b/tests/Makefile @@ -1,7 +1,6 @@ - set-supabase: - SUPABASE_URL := $(shell cd src/leapfrogai_api; supabase status | awk '/API URL:/ {print $$3}') - SUPABASE_ANON_KEY := $(shell cd src/leapfrogai_api; supabase status | awk '/anon key:/ {print $$3}') + SUPABASE_URL := $(shell cd src/leapfrogai_api; supabase status | awk '/LEAPFROGAI API URL:/ {print $$3}') + SUPABASE_ANON_KEY := $(shell uds zarf tools kubectl get secret -n leapfrogai supabase-bootstrap-jwt -o json | uds zarf tools yq '.data.anon-key' | base64 -d) define get_jwt_token echo "Getting JWT token from ${SUPABASE_URL}..."; \ @@ -26,10 +25,10 @@ test-env: set-supabase @read -s -p "Enter your DEV API password: " SUPABASE_PASS; echo; \ $(call get_jwt_token,"${SUPABASE_URL}/auth/v1/token?grant_type=password") -test-int-api: set-supabase +test-api-intergation: set-supabase source .env; PYTHONPATH=$$(pwd) pytest -vv -s tests/integration/api -test-unit: set-supabase +test-api-unit: set-supabase PYTHONPATH=$$(pwd) pytest -vv -s tests/unit test-load: diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..69c2dd69d --- /dev/null +++ b/tests/README.md @@ -0,0 +1,51 @@ +# Testing + +This document outlines tests related to the LeapfrogAI API and backends. + +Please see the [documentation in the LeapfrogAI UI sub-directory](../src/leapfrogai_ui/README.md) for Svelte UI Playwright tests. + +## API Tests + +Please see the [Makefile](./Makefile) for commands related to testing the various LeapfrogAI Python services. + +For the Python tests within this directory, the following components must be running and accessible: + +- [LeapfrogAI API](../src/leapfrogai_api/README.md) +- [Repeater](../packages/repeater/README.md) + +Review the different test code and test fixtures for more details. + +## Load Tests + +Please see the [Load Test documentation](./load/README.md) and directory for more details. + +## End-To-End Tests + +End-to-End (E2E) tests are located in the `e2e/` sub-directory. Each E2E test runs independently based on the model backend that we are trying to test. + +### Running Tests + +Run the tests on an existing [UDS Kubernetes cluster](../k3d-gpu/README.md) with the applicable backend deployed to the cluster. + +For example, the following sequence of commands runs test on the llama-cpp-python backend: + +```bash +# Build and Deploy the LFAI API +make build-api +uds zarf package deploy packages/api/zarf-package-leapfrogai-api-*.tar.zst + +# Build and Deploy the model backend you want to test. +# NOTE: In this case we are showing llama-cpp-python +make build-llama-cpp-python +uds zarf package deploy packages/llama-cpp-python/zarf-package-llama-cpp-python-*.tar.zst + +# Install the python dependencies +python -m pip install ".[dev]" + +# Run the tests! +# NOTE: Each model backend has its own e2e test files +python -m pytest tests/e2e/test_llama.py -v + +# Cleanup after yourself +k3d cluster delete uds +``` diff --git a/tests/e2e/README.md b/tests/e2e/README.md deleted file mode 100644 index b566ffc04..000000000 --- a/tests/e2e/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# LeapfrogAI End-To-End Tests - -This directory holds our e2e tests that we use to verify LFAI-API + various model backend functionality in an environment that replicates a live setting. The tests in this directory are automatically run against a [UDS Core](https://github.com/defenseunicorns/uds-core) cluster whenever a PR is opened or updated. - -## Running Tests Locally - -The tests in this directory are also able to be run locally! We are currently opinionated towards running on a cluster that is configured with UDS, as we mature out tests & documentations we'll potentially lose some of that opinionation. - -### Dependencies - -1. Python >= 3.11.6 -2. k3d >= v5.6.0 -3. uds >= v0.7.0 - -### Actually Running The Test - -There are several ways you can setup and run these tests. Here is one such way: - -> Deploy the [UDS cluster](/README.md#uds) \ -> NOTE: This stands up a k3d cluster and installs istio & pepr - -```bash -# Build and Deploy the LFAI API -make build-api -uds zarf package deploy zarf-package-leapfrogai-api-*.tar.zst - -# Build and Deploy the model backend you want to test. -# NOTE: In this case we are showing llama-cpp-python -make build-llama-cpp-python -uds zarf package deploy zarf-package-llama-cpp-python-*.tar.zst - -# Install the python dependencies -python -m pip install ".[dev]" - -# Run the tests! -# NOTE: Each model backend has its own e2e test files -python -m pytest tests/e2e/test_llama.py -v - -# Cleanup after yourself -k3d cluster delete -``` diff --git a/tests/load/README.md b/tests/load/README.md index 7985a6463..86d186e0e 100644 --- a/tests/load/README.md +++ b/tests/load/README.md @@ -1,29 +1,28 @@ # LeapfrogAI Load Tests -## Overview - These tests check the API's ability to handle different amounts of load. The tests simulate a specified number of users hitting the endpoints with some number of requests per second. -# Requirements - -### Environment Setup +## Pre-Requisites -Before running the tests, ensure that your API URL and bearer token are properly configured in your environment variables. Follow these steps: +Before running the tests, ensure that your API URL and API key are properly configured in your environment variables. Follow these steps: 1. Set the API URL: + ```bash export API_URL="https://leapfrogai-api.uds.dev" ``` 2. Set the API token: + ```bash - export BEARER_TOKEN="" + export BEARER_TOKEN="" ``` - **Note:** The bearer token should be your Supabase user JWT. For information on generating a JWT, please refer to the [Supabase README.md](../../packages/supabase/README.md). While an API key generated from the LeapfrogAI API endpoint can be used, it will cause the token generation load tests to fail. + **Note:** See the [API documentation](../../src/leapfrogai_api/README.md) to create an API key. 3. (Optional) - Set the model backend, this will default to `vllm` if unset: - ```bash + + ```bash export DEFAULT_MODEL="llama-cpp-python" ``` @@ -32,6 +31,7 @@ Before running the tests, ensure that your API URL and bearer token are properly To start the Locust web interface and run the tests: 1. Install dependencies from the project root. + ```bash pip install ".[dev]" ``` @@ -39,6 +39,7 @@ To start the Locust web interface and run the tests: 2. Navigate to the directory containing `loadtest.py`. 3. Execute the following command: + ```bash locust -f loadtest.py --web-port 8089 ``` @@ -49,4 +50,4 @@ To start the Locust web interface and run the tests: - Set the number of users to simulate - Set the spawn rate (users per second) - Choose the host to test against (should match your `API_URL`) - - Start the test and monitor results in real-time \ No newline at end of file + - Start the test and monitor results in real-time diff --git a/website/content/en/docs/local-deploy-guide/dependencies.md b/website/content/en/docs/local-deploy-guide/dependencies.md index 03c4636ca..a306979c7 100644 --- a/website/content/en/docs/local-deploy-guide/dependencies.md +++ b/website/content/en/docs/local-deploy-guide/dependencies.md @@ -12,25 +12,33 @@ Follow the outlined steps to ensure that your device is configured to execute Le Ensure that the following tools and packages are present in your environment: -- [build-essential](https://packages.ubuntu.com/focal/build-essential) -- [iptables](https://help.ubuntu.com/community/IptablesHowTo?action=show&redirect=Iptables) - [Git](https://git-scm.com/) -- [procps](https://gitlab.com/procps-ng/procps) -- [Python 3.11](https://www.python.org/downloads/release/python-3116/) - [Docker](https://docs.docker.com/engine/install/) - [K3D](https://k3d.io/) - [UDS CLI](https://github.com/defenseunicorns/uds-cli) ### Install PyEnv -- Follow the installation instructions outlined in the [pyenv](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) repository to install Python 3.11.6. +- Follow the installation instructions outlined in the [pyenv](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) repository to install Python 3.11.6: + + ```bash + # install the correct python version + pyenv install 3.11.6 + + # create a new virtual environment + pyenv virtualenv 3.11.6 leapfrogai + + # activate the virtual environment + pyenv activate leapfrogai + ``` + - If your installation process completes successfully but indicates missing packages such as `sqlite3`, execute the following command to install the required packages then proceed with the reinstallation of Python 3.11.6: -```bash -sudo apt-get install build-essential zlib1g-dev libffi-dev \ - libssl-dev libbz2-dev libreadline-dev libsqlite3-dev \ - liblzma-dev libncurses-dev -``` + ```bash + sudo apt-get install build-essential zlib1g-dev libffi-dev \ + libssl-dev libbz2-dev libreadline-dev libsqlite3-dev \ + liblzma-dev libncurses-dev + ``` ### Install Homebrew From dd25ec32b8f9bfb830a2561a13cbfd1e895f67ad Mon Sep 17 00:00:00 2001 From: Justin Law Date: Thu, 15 Aug 2024 18:45:35 -0400 Subject: [PATCH 13/30] moved dev bundle to developer docs only --- docs/DEVELOPMENT.md | 68 ++++++++++++++++- .../docs/local-deploy-guide/dependencies.md | 33 ++------ .../en/docs/local-deploy-guide/quick_start.md | 76 +------------------ .../docs/local-deploy-guide/requirements.md | 2 +- 4 files changed, 77 insertions(+), 102 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 3241cdf3b..f7eab7aaa 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -70,7 +70,7 @@ For example, the LeapfrogAI API requires a `config.yaml` be supplied when spun u ## Package Development -If you don't want to build an entire bundle, or you want to dev-loop on a single package in an existing [UDS Kubernetes cluster](../packages/k3d-gpu/README.md) you can do so by performing the following. +If you don't want to [build an entire bundle](#bundle-development), or you want to "dev-loop" on a single package in an existing [UDS Kubernetes cluster](../packages/k3d-gpu/README.md) you can do so by performing the following. For example, this is how you build and (re)deploy a local DEV version of a package: @@ -92,6 +92,72 @@ uds zarf package pull oci://ghcr.io/defenseunicorns/leapfrogai/leapfrogai-api:la uds zarf package deploy zarf-package-*.tar.zst --confirm ``` +## Bundle Development + +1. Install all the necessary package creation dependencies: + + ```bash + python -m pip install "hugging_face[cli,hf_transfer]" "transformers[torch]" ctranslate2 + ``` + +2. Build all of the packages you need at once with **ONE** of the following Make targets: + + ```bash + LOCAL_VERSION=dev ARCH=amd64 make build-cpu # ui, api, llama-cpp-python, text-embeddings, whisper, supabase + # OR + LOCAL_VERSION=dev ARCH=amd64 make build-gpu # ui, api, vllm, text-embeddings, whisper, supabase + # OR + LOCAL_VERSION=dev ARCH=amd64 make build-all # all of the components + ``` + + **OR** + + You can build components individually using the following Make targets: + + ```bash + LOCAL_VERSION=dev ARCH=amd64 make build-ui + LOCAL_VERSION=dev ARCH=amd64 make build-api + LOCAL_VERSION=dev ARCH=amd64 make build-supabase + LOCAL_VERSION=dev ARCH=amd64 make build-vllm # if you have GPUs (macOS not supported) + LOCAL_VERSION=dev ARCH=amd64 make build-llama-cpp-python # if you have CPU only + LOCAL_VERSION=dev ARCH=amd64 make build-text-embeddings + LOCAL_VERSION=dev ARCH=amd64 make build-whisper + ``` + +3. Create the UDS bundle, modifying the `uds-config.yaml` as required: + + ```bash + cd uds-bundles/dev/ + uds create . --confirm + ``` + +4. Deploy the UDS bundle to an existing [UDS Kubernetes cluster](../packages/k3d-gpu/README.md): + + ```bash + cd uds-bundles/dev/ + uds deploy --confirm + ``` + +### MacOS Specifics + +To run the same commands in MacOS, you will need to prepend your command with a couple of env vars like so: + +**All Macs:** `REG_PORT=5001` + +**Apple Silicon (M1/M2/M3/M4 series) Macs:** `ARCH=arm64` + +To demonstrate what this would look like for an Apple Silicon Mac: + +``` shell +REG_PORT=5001 ARCH=arm64 LOCAL_VERSION=dev make build-cpu +``` + +To demonstrate what this would look like for an older Intel Mac: + +``` shell +REG_PORT=5001 ARCH=arm64 LOCAL_VERSION=dev make build-cpu +``` + ## Access All LeapfrogAI components exposed as `VirtualService` resources can be accessed without port-forwarding if [UDS Core Slim Dev](../packages/k3d-gpu/README.md) is installed with LeapfrogAI packages. diff --git a/website/content/en/docs/local-deploy-guide/dependencies.md b/website/content/en/docs/local-deploy-guide/dependencies.md index a306979c7..97b503587 100644 --- a/website/content/en/docs/local-deploy-guide/dependencies.md +++ b/website/content/en/docs/local-deploy-guide/dependencies.md @@ -10,39 +10,16 @@ Follow the outlined steps to ensure that your device is configured to execute Le ### Host Dependencies -Ensure that the following tools and packages are present in your environment: +Ensure that the following tools and packages are installed in your environment according to the instructions below: - [Git](https://git-scm.com/) - [Docker](https://docs.docker.com/engine/install/) - [K3D](https://k3d.io/) - [UDS CLI](https://github.com/defenseunicorns/uds-cli) -### Install PyEnv +### Install Git -- Follow the installation instructions outlined in the [pyenv](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) repository to install Python 3.11.6: - - ```bash - # install the correct python version - pyenv install 3.11.6 - - # create a new virtual environment - pyenv virtualenv 3.11.6 leapfrogai - - # activate the virtual environment - pyenv activate leapfrogai - ``` - -- If your installation process completes successfully but indicates missing packages such as `sqlite3`, execute the following command to install the required packages then proceed with the reinstallation of Python 3.11.6: - - ```bash - sudo apt-get install build-essential zlib1g-dev libffi-dev \ - libssl-dev libbz2-dev libreadline-dev libsqlite3-dev \ - liblzma-dev libncurses-dev - ``` - -### Install Homebrew - -- Follow the [instructions](https://brew.sh/) to install the Homebrew package manager onto your system. +- Download [Git](https://git-scm.com/downloads) and follow the instructions on the [Git documentation website](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) ### Install Docker @@ -68,8 +45,8 @@ Ensure that the following tools and packages are present in your environment: ```bash # where $UDS_VERSION is the latest UDS CLI release wget -O uds https://github.com/defenseunicorns/uds-cli/releases/download/$UDS_VERSION/uds-cli_$UDS_VERSION_Linux_amd64 && \ - sudo chmod +x uds && \ - sudo mv uds /usr/local/bin/ + sudo chmod +x uds && \ + sudo mv uds /usr/local/bin/ ``` ## GPU Specific Instructions diff --git a/website/content/en/docs/local-deploy-guide/quick_start.md b/website/content/en/docs/local-deploy-guide/quick_start.md index 83313961f..85204299b 100644 --- a/website/content/en/docs/local-deploy-guide/quick_start.md +++ b/website/content/en/docs/local-deploy-guide/quick_start.md @@ -36,17 +36,8 @@ The default configuration when deploying with GPU support assumes a single GPU. ## Building the UDS Bundle -The following instructions are split into two sections: - -1. [LeapfrogAI Latest](#leapfrogai-latest): for hassle-free deployment of the latest stable version of LeapfrogAI -2. [LeapfrogAI Development](#leapfrogai-development): for deployment of a unreleased branch, a fork or `main` - If you already have a pre-built UDS bundle, please skip to [Deploying the UDS Bundle](#deploying-the-uds-bundle) -If you are using MacOS, please skip to [MacOS Specific Instructions](#macos-specifics) - -### LeapfrogAI Latest - 1. Start by cloning the [LeapfrogAI Repository](https://github.com/defenseunicorns/leapfrogai): ``` bash @@ -56,76 +47,17 @@ If you are using MacOS, please skip to [MacOS Specific Instructions](#macos-spec 2. From within the cloned repository create the LeapfrogAI bundle using **ONE** of the following: ```bash + # For CPU-only cd bundles/latest/cpu/ uds create . - uds deploy uds-bundle-leapfrogai-*.tar.zst --confirm + UDS_ARCH=amd64 uds deploy uds-bundle-leapfrogai-*.tar.zst --confirm + # For compatible AMD64, NVIDIA CUDA-capable GPU machines cd bundles/latest/gpu/ uds create . - uds deploy uds-bundle-leapfrogai-*.tar.zst --confirm - ``` - -3. Move on to [Deploying the UDS Bundle](#deploying-the-uds-bundle) - -### LeapfrogAI Development - -1. For ease, it's best to create a virtual environment for installing, managing and isolating package creation dependencies: - - ```bash - python -m venv .venv - source .venv/bin/activate - ``` - -2. Install all the necessary package creation dependencies: - - ```bash - python -m pip install "hugging_face[cli,hf_transfer]" "transformers[torch]" ctranslate2 + UDS_ARCH=amd64 uds deploy uds-bundle-leapfrogai-*.tar.zst --confirm ``` -3. Build all of the packages you need at once with **ONE** of the following `Make` targets: - - ```bash - LOCAL_VERSION=dev make build-cpu # ui, api, llama-cpp-python, text-embeddings, whisper, supabase - # OR - LOCAL_VERSION=dev make build-gpu # ui, api, vllm, text-embeddings, whisper, supabase - # OR - LOCAL_VERSION=dev make build-all # all of the components - ``` - - **OR** - - You can build components individually using the following `Make` targets: - - ```bash - LOCAL_VERSION=dev make build-ui - LOCAL_VERSION=dev make build-api - LOCAL_VERSION=dev make build-supabase - LOCAL_VERSION=dev make build-vllm # if you have GPUs (macOS not supported) - LOCAL_VERSION=dev make build-llama-cpp-python # if you have CPU only - LOCAL_VERSION=dev make build-text-embeddings - LOCAL_VERSION=dev make build-whisper - ``` - -## MacOS Specifics - -To run the same commands in MacOS, you will need to prepend your command with a couple of env vars like so: - -**All Macs:** `REG_PORT=5001` - -**Apple Silicon (M1/M2/M3/M4 series) Macs:** `ARCH=arm64` - -To demonstrate what this would look like for an Apple Silicon Mac: - -``` shell -REG_PORT=5001 ARCH=arm64 LOCAL_VERSION=dev make build-cpu -``` - -To demonstrate what this would look like for an older Intel Mac: - -``` shell -REG_PORT=5001 LOCAL_VERSION=dev make build-cpu -``` - ## Deploying the UDS bundle 1. Deploy a UDS Kubernetes cluster with **ONE** of the following: diff --git a/website/content/en/docs/local-deploy-guide/requirements.md b/website/content/en/docs/local-deploy-guide/requirements.md index 74181dce3..46760c2f4 100644 --- a/website/content/en/docs/local-deploy-guide/requirements.md +++ b/website/content/en/docs/local-deploy-guide/requirements.md @@ -19,7 +19,7 @@ Please review the following table to ensure your system meets the minimum requir ## Tested Environments -The following operating systems, hardware, architectures, and system specifications have been tested and validated for our deployment instructions: +The following is a non-exhaustive list of operating systems, hardware, architectures, and system specifications have been tested and validated for our deployment instructions: ### Operating Systems From 933ebe645c6e38c538e45ba2bb51b910083fc6c1 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Thu, 15 Aug 2024 18:49:09 -0400 Subject: [PATCH 14/30] minor vllm typo --- packages/vllm/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vllm/README.md b/packages/vllm/README.md index 673d7bc01..a55238cfd 100644 --- a/packages/vllm/README.md +++ b/packages/vllm/README.md @@ -49,7 +49,7 @@ To run the vllm backend locally: make install # Clone Model -python scripts/model_download.py +python src/model_download.py # Start the model backend make dev From 8973d866965a02578b012c5df33671a0e60cc928 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Thu, 15 Aug 2024 18:51:17 -0400 Subject: [PATCH 15/30] remove README from prettier lint --- src/leapfrogai_ui/.prettierignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/leapfrogai_ui/.prettierignore b/src/leapfrogai_ui/.prettierignore index 06c9c69b9..9b8360d5f 100644 --- a/src/leapfrogai_ui/.prettierignore +++ b/src/leapfrogai_ui/.prettierignore @@ -5,4 +5,6 @@ yarn.lock ../../packages/ui/chart/ -supabase \ No newline at end of file +supabase + +README.md From 61b15d58d510d613996a6822f207aba323f2d538 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Thu, 15 Aug 2024 18:56:42 -0400 Subject: [PATCH 16/30] docs website pointer back to GH --- website/content/en/docs/local-deploy-guide/components.md | 2 +- website/content/en/docs/local-deploy-guide/dependencies.md | 2 +- website/content/en/docs/local-deploy-guide/quick_start.md | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/website/content/en/docs/local-deploy-guide/components.md b/website/content/en/docs/local-deploy-guide/components.md index e161af589..7bf1b6550 100644 --- a/website/content/en/docs/local-deploy-guide/components.md +++ b/website/content/en/docs/local-deploy-guide/components.md @@ -58,4 +58,4 @@ The LeapfrogAI SDK offers a standardized collection of Protobuf and Python utili ### User Interface -LeapfrogAI offers user-friendly interfaces tailored for common use-cases, including chat, summarization, and transcription, providing accessible options for users to initiate these tasks. Please see the [LeapfrogAI UI](https://github.com/defenseunicorns/leapfrogai/tree/main/src/leapfrogai_ui)GitHub repository for additional information. +LeapfrogAI offers user-friendly interfaces tailored for common use-cases, including chat, summarization, and transcription, providing accessible options for users to initiate these tasks. Please see the [LeapfrogAI UI](https://github.com/defenseunicorns/leapfrogai/tree/main/src/leapfrogai_ui) GitHub repository for additional information. diff --git a/website/content/en/docs/local-deploy-guide/dependencies.md b/website/content/en/docs/local-deploy-guide/dependencies.md index 97b503587..3a1dc25a0 100644 --- a/website/content/en/docs/local-deploy-guide/dependencies.md +++ b/website/content/en/docs/local-deploy-guide/dependencies.md @@ -79,4 +79,4 @@ If you are experiencing issues even after carefully following the instructions b ### Deploy LeapfrogAI -- After ensuring that all system dependencies and requirements are fulfilled, refer to the LeapfrogAI deployment guide for comprehensive instructions on deploying LeapfrogAI within your local environment. +- After ensuring that all system dependencies and requirements are fulfilled, refer to the Quick Start guide for comprehensive instructions on deploying LeapfrogAI within your local environment. diff --git a/website/content/en/docs/local-deploy-guide/quick_start.md b/website/content/en/docs/local-deploy-guide/quick_start.md index 85204299b..cb65bd5ba 100644 --- a/website/content/en/docs/local-deploy-guide/quick_start.md +++ b/website/content/en/docs/local-deploy-guide/quick_start.md @@ -109,3 +109,7 @@ docker volume prune -f # removes all hanging container volumes - [UDS Core](https://github.com/defenseunicorns/uds-core) - [UDS CLI](https://github.com/defenseunicorns/uds-cli) + +## Further Tinkering + +For more LeapfrogAI customization options and developer-level documentation, please visit the [LeapfrogAI GitHub](https://github.com/defenseunicorns/leapfrogai) project for more details. From b1d9dbb76f6adc4b1451a8ec6eba394fde91ef32 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Fri, 16 Aug 2024 10:11:16 -0400 Subject: [PATCH 17/30] more clarifications for API + backend dev --- README.md | 3 ++- docs/DEVELOPMENT.md | 20 +++++++++++--------- src/leapfrogai_api/README.md | 4 ++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e6474f9d0..c383294b4 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Each of the LeapfrogAI components can also be run individually outside of a Kube **_First_** refer to the [DEVELOPMENT.md](docs/DEVELOPMENT.md) document for general development details. -**_Then_** refer to the linked READMEs for each individual package's local development instructions. +**_Then_** refer to the linked READMEs for each individual sub-directory's local development instructions. - [SDK](src/leapfrogai_sdk/README.md)[^2] - [API](packages/api/README.md)[^3] @@ -127,6 +127,7 @@ Each of the LeapfrogAI components can also be run individually outside of a Kube - [Text Embeddings](packages/text-embeddings/README.md) - [Faster Whisper](packages/whisper/README.md) - [Repeater](packages/repeater/README.md) +- [Tests](tests/README.md) [^2]: The SDK is not a functionally independent unit, and only becomes a functional unit when combined and packaged with the API and Backends as a dependency. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index f7eab7aaa..022113e84 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -160,7 +160,7 @@ REG_PORT=5001 ARCH=arm64 LOCAL_VERSION=dev make build-cpu ## Access -All LeapfrogAI components exposed as `VirtualService` resources can be accessed without port-forwarding if [UDS Core Slim Dev](../packages/k3d-gpu/README.md) is installed with LeapfrogAI packages. +All LeapfrogAI components exposed as `VirtualService`resources within a [UDS Kubernetes cluster](../packages/k3d-gpu/README.md) can be accessed without port-forwarding if [UDS Core Slim Dev](../packages/k3d-gpu/README.md) is installed with LeapfrogAI packages. For example, when developing the API and you need access to Supabase, you can point your locally running API to the in-cluster Supabase by setting the Supabase base URL to the in-cluster domain (https://supabase-kong.uds.dev). @@ -168,11 +168,17 @@ The preferred method of testing changes is to fully deploy something to a cluste ### Backends +The following sections discuss the nuances of developing on or with the LeapfrogAI model backends. + +#### Locally + +Backends can also be run locally as Python applications. See each model backend's README in the `packages/` directory for more details on running each in development mode. + #### Cluster The model backends are the only components within the LeapfrogAI stack that are not readily accessible via a `VirtualService`. These must be port-forwarded if a user wants to test a local deployment of the API against an in-cluster backend. -For example, the following bash script can be used to setup CPU RAG: +For example, the following bash script can be used to setup CPU RAG between a deployed UDS Kubernetes cluster and a locally running LeapfrogAI API: ```bash #!/bin/bash @@ -190,21 +196,17 @@ export SUPABASE_ANON_KEY=$(kubectl get secret supabase-bootstrap-jwt -n leapfrog # Trap SIGINT (Ctrl-C) and SIGTERM (termination signal) to call the cleanup function trap cleanup SIGINT SIGTERM -# Start kubectl and uvicorn services in the background and save their PIDs +# Start Kubectl port-forward services in the background and save their PIDs # Expose the backends at different ports to prevent localhost conflict -kubectl port-forward svc/text-embeddings-model -n leapfrogai 50052:50051 & +uds zarf tools kubectl port-forward svc/text-embeddings-model -n leapfrogai 50052:50051 & PID1=$! -kubectl port-forward svc/llama-cpp-python-model -n leapfrogai 50051:50051 & +uds zarf tools kubectl port-forward svc/llama-cpp-python-model -n leapfrogai 50051:50051 & PID2=$! # Wait for all background processes to finish wait $PID1 $PID2 ``` -#### Locally - -Backends can also be run locally as Python applications. See each model backend's README in the `packages/` directory for more details on running each in development mode. - #### Port Conflicts In all cases, port conflicts may arise when outside of a cluster service mesh. As seen in the [Cluster sub-section](#cluster), backends all try to emit at port `50051`; however, on a host machine's localhost, there can only be one on 50051. Using the [Leapfrogai API](../src/leapfrogai_api/config.example.yaml), define the ports at which you plan on making a backend accessible. diff --git a/src/leapfrogai_api/README.md b/src/leapfrogai_api/README.md index 87b3f019d..5977970c7 100644 --- a/src/leapfrogai_api/README.md +++ b/src/leapfrogai_api/README.md @@ -13,7 +13,7 @@ This document is only applicable for spinning up the API in a local Python devel ### Running > [!IMPORTANT] -> The following steps assume that you already have a deployed and accessible UDS Kubernetes cluster and LeapfrogAI. Please follow the steps within the LeapfrogAI documentation website for details. +> The following steps assume that you already have a deployed and accessible UDS Kubernetes cluster and LeapfrogAI. Please follow the steps within the [DEVELOPMENT.md](../../docs/DEVELOPMENT.md) for details. 1. Install dependencies @@ -51,7 +51,7 @@ This document is only applicable for spinning up the API in a local Python devel ### Access -See the Access section of the [DEVELOPMENT.md](../../docs/DEVELOPMENT.md) for different ways to connect the API to a model backend or Supabase. +See the ["Access" section of the DEVELOPMENT.md](../../docs/DEVELOPMENT.md#access) for different ways to connect the API to a model backend or Supabase. ### Tests From 6df414d08fce4a18d3eae6df029bc4d9b924b018 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Fri, 16 Aug 2024 10:16:38 -0400 Subject: [PATCH 18/30] adds back missing pre-commit --- .pre-commit-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e435de041..24785cab5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,6 +9,9 @@ repos: name: Large Files Check args: ["--maxkb=1024"] + - id: check-merge-conflict + name: Check for Upstream Merge Conflicts + - id: detect-aws-credentials name: Check AWS Credentials args: From d62de15e2c2775a683420b3292008fdc775ea7e7 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Fri, 16 Aug 2024 10:43:55 -0400 Subject: [PATCH 19/30] fix typos, remove locust from api dev --- README.md | 4 ++-- packages/k3d-gpu/README.md | 2 +- packages/supabase/README.md | 1 + src/leapfrogai_api/pyproject.toml | 1 - src/leapfrogai_ui/README.md | 7 ++----- tests/Makefile | 4 +++- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c383294b4..5c1764d8c 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Large Language Models (LLMs) are a powerful resource for AI-driven decision maki ## Structure -The LeapfrogAI repository follows a monorepo structure based around an [API](#api) with each of the [components](#components) included in a dedicated `packages` directory. Each of these package directories contains the source code for each component as well as the deployment infrastructure. The UDS bundles that handle the development and latest deployments of LeapfrogAI are in the `uds-bundles` directory. The structure looks as follows: +The LeapfrogAI repository follows a monorepo structure based around an [API](#api) with each of the [components](#components) included in a dedicated `packages` directory. The UDS bundles that handle the development and latest deployments of LeapfrogAI are in the `uds-bundles` directory. The structure looks as follows: ```bash leapfrogai/ @@ -99,7 +99,7 @@ LeapfrogAI provides several backends for a variety of use cases. Below is the ba #### Repeater -The [repeater](packages/repeater/) "model" is a basic "backend" that parrots all inputs it receives back to the user. It is built out the same way all the actual backends are and it primarily used for testing the API. +The [repeater](packages/repeater/) "model" is a basic "backend" that parrots all inputs it receives back to the user. It is built out the same way all the actual backends are and it is primarily used for testing the API. ## Usage diff --git a/packages/k3d-gpu/README.md b/packages/k3d-gpu/README.md index 9a405a0f1..1a67e353b 100644 --- a/packages/k3d-gpu/README.md +++ b/packages/k3d-gpu/README.md @@ -13,7 +13,7 @@ All system requirements and pre-requisites from the [LeapfrogAI documentation we ### Deployment > [!NOTE] -> The following Make targets from the root of the LeapfrogAI repository or within this sub-directory. +> The following Make targets can be executed from the root of the LeapfrogAI repository or within this sub-directory. To deploy a new K3d cluster with [UDS Core Slim Dev](https://github.com/defenseunicorns/uds-core#uds-package-development), use one of the following Make targets. diff --git a/packages/supabase/README.md b/packages/supabase/README.md index 7e5da3da5..920f349ce 100644 --- a/packages/supabase/README.md +++ b/packages/supabase/README.md @@ -51,6 +51,7 @@ Go to `https://supabase-kong.uds.dev`. The login is `supabase-admin` the passwor ``` - Longer term API tokens (30, 60, or 90 days) can be created from the API key workflow within the LeapfrogAI UI +- Longer term API tokens (30 days) can also be created using the [API documentation](../../src/leapfrogai_api/README.md) ## Supabase Migrations diff --git a/src/leapfrogai_api/pyproject.toml b/src/leapfrogai_api/pyproject.toml index 5f3f52479..fb3ce6cbe 100644 --- a/src/leapfrogai_api/pyproject.toml +++ b/src/leapfrogai_api/pyproject.toml @@ -30,7 +30,6 @@ requires-python = "~=3.11" [project.optional-dependencies] dev = [ - "locust", "pytest-asyncio", "requests", "requests-toolbelt", diff --git a/src/leapfrogai_ui/README.md b/src/leapfrogai_ui/README.md index 11a9ac1cf..2c296603a 100644 --- a/src/leapfrogai_ui/README.md +++ b/src/leapfrogai_ui/README.md @@ -66,12 +66,9 @@ LEAPFROGAI_API_BASE_URL=https://leapfrogai-api.uds.dev DEFAULT_MODEL=llama-cpp-python # or vllm ``` -2. Run the frontend migrations +2. Run the UI migrations -If you deploy the UI with UDS, the necessary database migrations will be applied. You can still run a local version of the UI, but the deployed version will have set up the -database properly for you. - -Further instructions will be coming soon in a future release. + If you deploy the UI with UDS, the necessary database migrations will be applied. You can still run a local version of the UI, but the deployed version will have set up the database properly for you. #### Standalone Supabase diff --git a/tests/Makefile b/tests/Makefile index b56a13761..29a28c327 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,5 +1,7 @@ +SUPABASE_URL := https://supabase-kong.uds.dev + set-supabase: - SUPABASE_URL := $(shell cd src/leapfrogai_api; supabase status | awk '/LEAPFROGAI API URL:/ {print $$3}') + SUPABASE_URL := ${SUPABASE_URL} SUPABASE_ANON_KEY := $(shell uds zarf tools kubectl get secret -n leapfrogai supabase-bootstrap-jwt -o json | uds zarf tools yq '.data.anon-key' | base64 -d) define get_jwt_token From d119fdfc8fd6e18c7b2dfe331e00841550a4a50a Mon Sep 17 00:00:00 2001 From: Justin Law Date: Fri, 16 Aug 2024 11:00:04 -0400 Subject: [PATCH 20/30] more typos, .env for local api --- docs/DEVELOPMENT.md | 2 +- src/leapfrogai_api/Makefile | 5 +++-- src/leapfrogai_api/README.md | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 022113e84..2e73a5b28 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -36,7 +36,7 @@ If your installation process completes successfully but indicates missing packag ## UDS CLI Aliasing -Below are instructions for adding UDS CLI aliases that are useful for deployments that occur in an air-gap where only the UDS CLI binary available to the engineer. +Below are instructions for adding UDS CLI aliases that are useful for deployments that occur in an air-gap where the UDS CLI binary is available to the engineer. For general CLI UX, put the following in your shell configuration (e.g., `/root/.bashrc`, `~/.zshrc`): diff --git a/src/leapfrogai_api/Makefile b/src/leapfrogai_api/Makefile index afddd176e..5e78b3f06 100644 --- a/src/leapfrogai_api/Makefile +++ b/src/leapfrogai_api/Makefile @@ -1,4 +1,5 @@ -API_PORT := 3000 +API_PORT := 8080 +# This can be pointed at the localhost:8080 instance of the API as well API_BASE_URL := https://leapfrogai-api.uds.dev SUPABASE_BASE_URL := https://supabase-kong.uds.dev EXPIRATION_TIME := $(shell date -d "+30 days" +%s) @@ -13,7 +14,7 @@ set-env: echo "SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}" >> .env dev: set-env - . ./.env && python -m uvicorn main:app --port ${API_PORT} --reload --log-level info + python -m uvicorn main:app --port ${API_PORT} --reload --log-level info --env-file .env api-key: curl -s -X POST '${SUPABASE_BASE_URL}/auth/v1/signup' \ diff --git a/src/leapfrogai_api/README.md b/src/leapfrogai_api/README.md index 5977970c7..eec4dd0c6 100644 --- a/src/leapfrogai_api/README.md +++ b/src/leapfrogai_api/README.md @@ -26,21 +26,21 @@ This document is only applicable for spinning up the API in a local Python devel 3. Run the FastAPI application ```bash - make dev API_PORT=3000 + make dev API_PORT=8080 ``` 4. Create an API key with test user "leapfrogai@defenseunicorns.com" and test password "password", lasting 30 days from creation time ```bash # If the in-cluster API is up, and not testing the API workflow - make api-key API_BASE_URL=https://leapfrogai-api.uds.dev + make api-key API_BASE_URL=http://localhost:8080 ``` To create a new 30-day API key, use the following: ```bash # If the in-cluster API is up, and not testing the API workflow - make new-api-key API_BASE_URL=https://leapfrogai-api.uds.dev + make new-api-key API_BASE_URL=http://localhost:8080 ``` The newest API key will be printed to a `.env` file located within this directory. From cb2b1fed01d395306a91480f41fad4841e636e27 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Fri, 16 Aug 2024 11:42:33 -0400 Subject: [PATCH 21/30] add supabase dashboard secret command --- packages/supabase/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/supabase/README.md b/packages/supabase/README.md index 920f349ce..db87fb7d6 100644 --- a/packages/supabase/README.md +++ b/packages/supabase/README.md @@ -29,7 +29,11 @@ uds zarf package deploy packages/supabase/zarf-package-supabase-*-dev.tar.zst -- ### Accessing Supabase -Go to `https://supabase-kong.uds.dev`. The login is `supabase-admin` the password is randomly generated in a cluster secret named `supabase-dashboard-secret` +Go to `https://supabase-kong.uds.dev`. The login username is `supabase-admin`, and the password is randomly generated in a cluster secret named `supabase-dashboard-secret`. Run the following to grab the password in a single-line command: + +```bash +uds zarf tools kubectl get secret -n leapfrogai supabase-dashboard-secret -o json | jq '.data | map_values(@base64d)' +``` **NOTE:** The `uds.dev` domain is only used for locally deployed LeapfrogAI packages, so this domain will be unreachable without first manually deploying the UDS bundle. From 53d7a4b72288a7b9451dcdeffa4959c74e0956bc Mon Sep 17 00:00:00 2001 From: Justin Law Date: Fri, 16 Aug 2024 11:46:20 -0400 Subject: [PATCH 22/30] use uds yq for supabase dashboard secret --- packages/supabase/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/supabase/README.md b/packages/supabase/README.md index db87fb7d6..94472542a 100644 --- a/packages/supabase/README.md +++ b/packages/supabase/README.md @@ -32,7 +32,7 @@ uds zarf package deploy packages/supabase/zarf-package-supabase-*-dev.tar.zst -- Go to `https://supabase-kong.uds.dev`. The login username is `supabase-admin`, and the password is randomly generated in a cluster secret named `supabase-dashboard-secret`. Run the following to grab the password in a single-line command: ```bash -uds zarf tools kubectl get secret -n leapfrogai supabase-dashboard-secret -o json | jq '.data | map_values(@base64d)' +uds zarf tools kubectl get secret -n leapfrogai supabase-dashboard-secret -o json | uds zarf tools yq '.data.password' | base64 -d ``` **NOTE:** The `uds.dev` domain is only used for locally deployed LeapfrogAI packages, so this domain will be unreachable without first manually deploying the UDS bundle. From 31f60f9a59156ba81193e62237177d6e7600f2d1 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Fri, 16 Aug 2024 12:59:51 -0400 Subject: [PATCH 23/30] temp Supabase version change --- src/leapfrogai_api/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/leapfrogai_api/pyproject.toml b/src/leapfrogai_api/pyproject.toml index fb3ce6cbe..b54827948 100644 --- a/src/leapfrogai_api/pyproject.toml +++ b/src/leapfrogai_api/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "python-multipart >= 0.0.7", #indirect dep of FastAPI to receive form data for file uploads "watchfiles >= 0.21.0", "leapfrogai_sdk", - "supabase >= 2.5.1", + "supabase == 2.5.1", "langchain >= 0.2.1", "langchain-community >= 0.2.1", "unstructured[md,xlsx,pptx] >= 0.15.3", # Only specify necessary filetypes to prevent package bloat (e.g. 130MB vs 6GB) From 87d19effb9533380aee048234c4ad683e03e8869 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Sat, 17 Aug 2024 13:04:37 -0400 Subject: [PATCH 24/30] gato improvements --- .github/release-please-config.json | 5 +++++ docs/DEVELOPMENT.md | 14 ++++++++++++- tests/Makefile | 2 +- tests/README.md | 21 +++++++++++++++---- .../docs/local-deploy-guide/dependencies.md | 17 ++++++++++----- website/hugo.toml | 4 +++- 6 files changed, 51 insertions(+), 12 deletions(-) diff --git a/.github/release-please-config.json b/.github/release-please-config.json index 2c61e1446..6cf564cdb 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -40,6 +40,11 @@ "type": "generic", "path": "**/zarf-config.yaml", "glob": true + }, + { + "type": "generic", + "path": "**/hugo.toml", + "glob": true } ] } diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 2e73a5b28..06fdd8255 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -118,7 +118,7 @@ uds zarf package deploy zarf-package-*.tar.zst --confirm LOCAL_VERSION=dev ARCH=amd64 make build-ui LOCAL_VERSION=dev ARCH=amd64 make build-api LOCAL_VERSION=dev ARCH=amd64 make build-supabase - LOCAL_VERSION=dev ARCH=amd64 make build-vllm # if you have GPUs (macOS not supported) + LOCAL_VERSION=dev ARCH=amd64 make build-vllm # if you have NVIDIA GPUs (AMR64 not supported) LOCAL_VERSION=dev ARCH=amd64 make build-llama-cpp-python # if you have CPU only LOCAL_VERSION=dev ARCH=amd64 make build-text-embeddings LOCAL_VERSION=dev ARCH=amd64 make build-whisper @@ -166,6 +166,17 @@ For example, when developing the API and you need access to Supabase, you can po The preferred method of testing changes is to fully deploy something to a cluster and run local smoke tests as needed. The GitHub workflows will run all integration and E2E test suites. +### Supabase + +Supabase is a special case when spun up inside of a UDS Kubernetes cluster. All of the bitnami Supabase components are served through the Kong service mesh, which is exposed as https://supabase-kong.uds.dev through our Istio tenant gateway. All of the Make commands, and our UI and API, correctly route to the right endpoint for interacting with each sub-component of Supabase. The UI and API use the `supabase` Typescript or Python package to interact with Supabase without issue. + +Although not recommended, below are example endpoints for direct interaction with Supabase sub-components is as follows: + +- https://supabase-kong.uds.dev/auth/v1/* -> to access auth endpoints +- https://supabase-kong.uds.dev/rest/v1/ -> for postgres + +We highly recommend using the published `supabase` packages, or interacting with Supabase via the LeapfrogAI API or UI. Go to https://leapfrogai-api.uds.dev/docs to see the exposed Supabase sub-component routes under the `leapfrogai` namespace / routes. + ### Backends The following sections discuss the nuances of developing on or with the LeapfrogAI model backends. @@ -198,6 +209,7 @@ trap cleanup SIGINT SIGTERM # Start Kubectl port-forward services in the background and save their PIDs # Expose the backends at different ports to prevent localhost conflict +# Make sure to change the config.yaml in the api source directory uds zarf tools kubectl port-forward svc/text-embeddings-model -n leapfrogai 50052:50051 & PID1=$! uds zarf tools kubectl port-forward svc/llama-cpp-python-model -n leapfrogai 50051:50051 & diff --git a/tests/Makefile b/tests/Makefile index 29a28c327..53441cd0f 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -27,7 +27,7 @@ test-env: set-supabase @read -s -p "Enter your DEV API password: " SUPABASE_PASS; echo; \ $(call get_jwt_token,"${SUPABASE_URL}/auth/v1/token?grant_type=password") -test-api-intergation: set-supabase +test-api-integration: set-supabase source .env; PYTHONPATH=$$(pwd) pytest -vv -s tests/integration/api test-api-unit: set-supabase diff --git a/tests/README.md b/tests/README.md index 69c2dd69d..46951f643 100644 --- a/tests/README.md +++ b/tests/README.md @@ -6,14 +6,27 @@ Please see the [documentation in the LeapfrogAI UI sub-directory](../src/leapfro ## API Tests -Please see the [Makefile](./Makefile) for commands related to testing the various LeapfrogAI Python services. - -For the Python tests within this directory, the following components must be running and accessible: +For the unit and integration tests within this directory, the following components must be running and accessible: - [LeapfrogAI API](../src/leapfrogai_api/README.md) - [Repeater](../packages/repeater/README.md) +- [Supabase](../packages/supabase/README.md) + +Please see the [Makefile](./Makefile) for more details. Below is a quick synopsis of the available Make targets: + +```bash +# create a test user for the tests +make test-user SUPABASE_URL=https://supabase-kong.uds.dev -Review the different test code and test fixtures for more details. +# setup the environment variables for the tests +make test-env SUPABASE_URL=https://supabase-kong.uds.dev + +# run the unit tests +make test-api-unit SUPABASE_URL=https://supabase-kong.uds.dev + +# run the integration tests +make test-api-integration SUPABASE_URL=https://supabase-kong.uds.dev +``` ## Load Tests diff --git a/website/content/en/docs/local-deploy-guide/dependencies.md b/website/content/en/docs/local-deploy-guide/dependencies.md index 3a1dc25a0..bf80c62db 100644 --- a/website/content/en/docs/local-deploy-guide/dependencies.md +++ b/website/content/en/docs/local-deploy-guide/dependencies.md @@ -67,15 +67,22 @@ If you are experiencing issues even after carefully following the instructions b ### NVIDIA Container Toolkit - Follow the [instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installing-with-apt) to download the NVIDIA container toolkit (>=1.14). -- After the successful installation off the toolkit, follow the [toolkit instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installing-with-apt) to verify that your default Docker runtime is configured for NVIDIA. -- Configure Docker to use the `nvidia` runtime by default by adding the `--set-as-default` flag during the container toolkit post-installation configuration step -- Verify that the default runtime is changed by running the following command: +- After the successful installation off the toolkit, follow the [toolkit instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#configuring-docker) to verify that your default Docker runtime is configured for NVIDIA. +- Verify that `nvidia` is now a runtime available to the Docker daemon to use: ```bash - docker info | grep "Default Runtime" + # the expected output should be similar to: `Runtimes: io.containerd.runc.v2 nvidia runc` + docker info | grep -i nvidia ``` -- The expected output should be similar to: `Default Runtime: nvidia`. +- [Try out a sample CUDA workload](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/sample-workload.html) to ensure your Docker containers have access to the GPUs after configuration. +- (OPTIONAL) You can configure Docker to use the `nvidia` runtime by default by adding the `--set-as-default` flag during the container toolkit post-installation configuration step +- (OPTIONAL) Verify that the default runtime is changed by running the following command: + + ```bash + # the expected output should be similar to: `Default Runtime: nvidia` + docker info | grep "Default Runtime" + ``` ### Deploy LeapfrogAI diff --git a/website/hugo.toml b/website/hugo.toml index 85ce051b5..8534a153c 100644 --- a/website/hugo.toml +++ b/website/hugo.toml @@ -51,7 +51,9 @@ proxy = "direct" copyright = "Defense Unicorns" github_project_repo = "https://github.com/defenseunicorns/leapfrogai" github_repo = "https://github.com/defenseunicorns/leapfrogai" - version = "v0.10.0" + # x-release-please-start-version + version = "v0.11.0" + # x-release-please-end # version_menu = "v1" # url_latest_version = "https://latest-version" From a69213d6a13113614e866bd9dae7e40827a4128f Mon Sep 17 00:00:00 2001 From: Kyle Palko <165685856+unicorn-kp@users.noreply.github.com> Date: Sat, 17 Aug 2024 17:49:43 -0400 Subject: [PATCH 25/30] Demo video for README.md (#925) --- README.md | 12 ++++++++++++ website/assets/img/walkthrough_thumbnail.jpg | Bin 0 -> 211347 bytes 2 files changed, 12 insertions(+) create mode 100644 website/assets/img/walkthrough_thumbnail.jpg diff --git a/README.md b/README.md index 5c1764d8c..a9cc9d3c2 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,18 @@ Large Language Models (LLMs) are a powerful resource for AI-driven decision maki - **Mission Integration**: By hosting your own LLM, you have the ability to customize the model's parameters, training data, and more, tailoring the AI to your specific needs. +## Short Minute and a Half Demo + + + 2 minute demo of features of LeapfrogAI + + +LeapfrogAI, built on top of [Unicorn Delivery Service (UDS)](https://github.com/defenseunicorns/uds-core) includes several features including: +- **Single Sign-On** +- **Non-proprietary API Compatible with OpenAI's API** +- **Retrieval Augmetented Generation (RAG)** +- **And More!** + ## Structure The LeapfrogAI repository follows a monorepo structure based around an [API](#api) with each of the [components](#components) included in a dedicated `packages` directory. The UDS bundles that handle the development and latest deployments of LeapfrogAI are in the `uds-bundles` directory. The structure looks as follows: diff --git a/website/assets/img/walkthrough_thumbnail.jpg b/website/assets/img/walkthrough_thumbnail.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e3ccdcb80ce456940f4d0df363b955c736805213 GIT binary patch literal 211347 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 Date: Mon, 19 Aug 2024 09:59:15 -0400 Subject: [PATCH 26/30] fix Kyle adds --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a9cc9d3c2..eb24dcde4 100644 --- a/README.md +++ b/README.md @@ -36,16 +36,17 @@ Large Language Models (LLMs) are a powerful resource for AI-driven decision maki - **Mission Integration**: By hosting your own LLM, you have the ability to customize the model's parameters, training data, and more, tailoring the AI to your specific needs. -## Short Minute and a Half Demo +## Demo Video 2 minute demo of features of LeapfrogAI -LeapfrogAI, built on top of [Unicorn Delivery Service (UDS)](https://github.com/defenseunicorns/uds-core) includes several features including: +LeapfrogAI, built on top of [Unicorn Delivery Service (UDS)](https://github.com/defenseunicorns/uds-core), which includes several features including: + - **Single Sign-On** - **Non-proprietary API Compatible with OpenAI's API** -- **Retrieval Augmetented Generation (RAG)** +- **Retrieval Augmented Generation (RAG)** - **And More!** ## Structure From 87a1798f935f441a2da9035ccdd8b4ce3f6ae17d Mon Sep 17 00:00:00 2001 From: Kyle Palko <165685856+unicorn-kp@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:25:47 -0400 Subject: [PATCH 27/30] Update requirements.md with minimum GPU requirements (#927) --- .../en/docs/local-deploy-guide/requirements.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/website/content/en/docs/local-deploy-guide/requirements.md b/website/content/en/docs/local-deploy-guide/requirements.md index 46760c2f4..5d599c0a2 100644 --- a/website/content/en/docs/local-deploy-guide/requirements.md +++ b/website/content/en/docs/local-deploy-guide/requirements.md @@ -10,12 +10,12 @@ Prior to deploying LeapfrogAI, ensure that the following tools, packages, and re Please review the following table to ensure your system meets the minimum requirements. GPU requirements only apply when your system is capable of deploying a GPU-accelerated version of the LeapfrogAI stack. -| | Minimum | Recommended (Performance) | -|------|--------------------|---------------------------| -| DISK | 256 GB | 1 TB | -| RAM | 32 GB | 128 GB | -| CPU | 8 Cores @ 3.0 GHz | 32 Cores @ 3.0 GHz | -| GPU | N/A | 2x NVIDIA RTX 4090 GPUs | +| | Minimum (CPU) | Minimum (GPU) | Recommended (Performance) | +|------|--------------------|----------------------------|---------------------------| +| DISK | 256 GB | 256 GB | 1 TB | +| RAM | 32 GB | 32 GB | 128 GB | +| CPU | 8 Cores @ 3.0 GHz | 8 Cores @ 3.0 GHz | 32 Cores @ 3.0 GHz | +| GPU | N/A | 1x NVIDIA GPU @ 12 GB VRAM | 2x NVIDIA RTX 4090 GPUs | ## Tested Environments From 083de0b940dc8f379403aa4444e4def5c351e3c2 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Mon, 19 Aug 2024 13:20:07 -0400 Subject: [PATCH 28/30] table of contents reorder --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eb24dcde4..e60eb4af3 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ - [Getting Started](#getting-started) - [Components](#components) - [API](#api) - - [Backends](#backends) - - [Repeater](#repeater) - [SDK](#sdk) - [UI](#ui) + - [Backends](#backends) + - [Repeater](#repeater) - [Usage](#usage) - [Local Development](#local-development) - [Contributing](#contributing) From d391d3b0d551b14e471e4a6d2fd58bd6f86e7373 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Mon, 19 Aug 2024 13:42:21 -0400 Subject: [PATCH 29/30] more clear nvidia-ctk instructions --- .../en/docs/local-deploy-guide/dependencies.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/website/content/en/docs/local-deploy-guide/dependencies.md b/website/content/en/docs/local-deploy-guide/dependencies.md index bf80c62db..891ce324b 100644 --- a/website/content/en/docs/local-deploy-guide/dependencies.md +++ b/website/content/en/docs/local-deploy-guide/dependencies.md @@ -66,8 +66,13 @@ If you are experiencing issues even after carefully following the instructions b ### NVIDIA Container Toolkit -- Follow the [instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installing-with-apt) to download the NVIDIA container toolkit (>=1.14). -- After the successful installation off the toolkit, follow the [toolkit instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#configuring-docker) to verify that your default Docker runtime is configured for NVIDIA. +- Read the pre-requisites for installation and follow the [instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installing-with-apt) to download and install the NVIDIA container toolkit (>=1.14). +- After the successful installation off the toolkit, follow the [toolkit instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#configuring-docker) to verify that your default Docker runtime is configured for NVIDIA: + + ```bash + nvidia-ctk runtime configure --runtime=docker --config=$HOME/.config/docker/daemon.json + ``` + - Verify that `nvidia` is now a runtime available to the Docker daemon to use: ```bash @@ -76,7 +81,12 @@ If you are experiencing issues even after carefully following the instructions b ``` - [Try out a sample CUDA workload](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/sample-workload.html) to ensure your Docker containers have access to the GPUs after configuration. -- (OPTIONAL) You can configure Docker to use the `nvidia` runtime by default by adding the `--set-as-default` flag during the container toolkit post-installation configuration step +- (OPTIONAL) You can configure Docker to use the `nvidia` runtime by default by adding the `--set-as-default` flag during the container toolkit post-installation configuration step by running the following command: + + ```bash + nvidia-ctk runtime configure --runtime=docker --config=$HOME/.config/docker/daemon.json --set-as-default + ``` + - (OPTIONAL) Verify that the default runtime is changed by running the following command: ```bash From 5fc2b38ce5bcdc7cc90561d2d589af218f373b93 Mon Sep 17 00:00:00 2001 From: Justin Law Date: Mon, 19 Aug 2024 13:44:48 -0400 Subject: [PATCH 30/30] more clear nvidia-ctk instructions --- website/content/en/docs/local-deploy-guide/dependencies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/en/docs/local-deploy-guide/dependencies.md b/website/content/en/docs/local-deploy-guide/dependencies.md index 891ce324b..8c6edf4a7 100644 --- a/website/content/en/docs/local-deploy-guide/dependencies.md +++ b/website/content/en/docs/local-deploy-guide/dependencies.md @@ -66,7 +66,7 @@ If you are experiencing issues even after carefully following the instructions b ### NVIDIA Container Toolkit -- Read the pre-requisites for installation and follow the [instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installing-with-apt) to download and install the NVIDIA container toolkit (>=1.14). +- [Read the pre-requisites for installation and follow the instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installing-with-apt) to download and install the NVIDIA container toolkit (>=1.14). - After the successful installation off the toolkit, follow the [toolkit instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#configuring-docker) to verify that your default Docker runtime is configured for NVIDIA: ```bash