From 6c5344f33a48ad6c64bc9e80c9f161de6f0bbf64 Mon Sep 17 00:00:00 2001 From: Abhishek Malvankar Date: Wed, 25 Jun 2025 21:02:54 -0400 Subject: [PATCH 01/39] changes to move to new design --- Dockerfile | 53 ++- Makefile | 225 +++++++++ PROJECT | 20 + README.md | 202 ++++---- api/v1alpha1/groupversion_info.go | 36 ++ api/v1alpha1/optimizer_types.go | 72 +++ api/v1alpha1/zz_generated.deepcopy.go | 143 ++++++ cmd/main.go | 244 ++++++++++ cmd/optimizer/main.go | 20 - .../crd/bases/llmd.llm-d.ai_optimizers.yaml | 148 ++++++ config/crd/kustomization.yaml | 16 + config/crd/kustomizeconfig.yaml | 19 + .../default/cert_metrics_manager_patch.yaml | 30 ++ config/default/kustomization.yaml | 234 ++++++++++ config/default/manager_metrics_patch.yaml | 4 + config/default/metrics_service.yaml | 18 + config/manager/kustomization.yaml | 2 + config/manager/manager.yaml | 98 ++++ .../network-policy/allow-metrics-traffic.yaml | 27 ++ config/network-policy/kustomization.yaml | 2 + config/prometheus/kustomization.yaml | 11 + config/prometheus/monitor.yaml | 27 ++ config/prometheus/monitor_tls_patch.yaml | 19 + config/rbac/kustomization.yaml | 28 ++ config/rbac/leader_election_role.yaml | 40 ++ config/rbac/leader_election_role_binding.yaml | 15 + config/rbac/metrics_auth_role.yaml | 17 + config/rbac/metrics_auth_role_binding.yaml | 12 + config/rbac/metrics_reader_role.yaml | 9 + config/rbac/optimizer_admin_role.yaml | 27 ++ config/rbac/optimizer_editor_role.yaml | 33 ++ config/rbac/optimizer_viewer_role.yaml | 29 ++ config/rbac/role.yaml | 32 ++ config/rbac/role_binding.yaml | 15 + config/rbac/service_account.yaml | 8 + config/samples/kustomization.yaml | 4 + config/samples/llmd_v1alpha1_optimizer.yaml | 9 + demos/generators/main.go | 153 ------ demos/main/main.go | 116 ----- demos/scale/main.go | 143 ------ demos/transition/main.go | 151 ------ docs/arch/high-level.png | Bin 44679 -> 0 bytes docs/arch/runtime.png | Bin 72423 -> 0 bytes docs/figs/Slide10.png | Bin 54354 -> 0 bytes docs/figs/Slide11.png | Bin 50274 -> 0 bytes docs/figs/Slide12.png | Bin 77875 -> 0 bytes docs/figs/Slide13.png | Bin 54857 -> 0 bytes docs/figs/Slide14.png | Bin 22087 -> 0 bytes docs/figs/Slide16.png | Bin 51571 -> 0 bytes docs/figs/Slide17.png | Bin 36642 -> 0 bytes docs/figs/Slide19.png | Bin 48281 -> 0 bytes docs/figs/Slide2.png | Bin 72423 -> 0 bytes docs/figs/Slide20.png | Bin 45389 -> 0 bytes docs/figs/Slide21.png | Bin 36444 -> 0 bytes docs/figs/Slide23.png | Bin 56661 -> 0 bytes docs/figs/Slide24.png | Bin 39458 -> 0 bytes docs/figs/Slide26.png | Bin 53246 -> 0 bytes docs/figs/Slide27.png | Bin 48919 -> 0 bytes docs/figs/Slide28.png | Bin 37757 -> 0 bytes docs/figs/Slide3.png | Bin 94107 -> 0 bytes docs/figs/Slide30.png | Bin 34609 -> 0 bytes docs/figs/Slide32.png | Bin 41068 -> 0 bytes docs/figs/Slide33.png | Bin 34893 -> 0 bytes docs/figs/Slide34.png | Bin 55996 -> 0 bytes docs/figs/Slide4.png | Bin 94115 -> 0 bytes docs/figs/Slide5.png | Bin 47446 -> 0 bytes docs/figs/Slide6.png | Bin 137637 -> 0 bytes docs/figs/Slide7.png | Bin 37286 -> 0 bytes docs/figs/Slide8.png | Bin 47765 -> 0 bytes docs/figs/Slide9.png | Bin 44689 -> 0 bytes docs/figs/slide22.png | Bin 22365 -> 0 bytes docs/slides/inferno-dynamic.pdf | Bin 1642452 -> 0 bytes docs/slides/summary.pdf | Bin 1623649 -> 0 bytes go.mod | 119 +++-- go.sum | 257 ++++++----- hack/boilerplate.go.txt | 15 + internal/controller/optimizer_controller.go | 59 +++ .../controller/optimizer_controller_test.go | 82 ++++ internal/controller/suite_test.go | 116 +++++ internal/interfaces/interfaces.go | 26 ++ internal/interfaces/types.go | 47 ++ manifests/yamls/deploy-optimizer.yaml | 51 -- pkg/config/defaults.go | 34 -- pkg/config/types.go | 137 ------ pkg/core/accelerator.go | 71 --- pkg/core/allocation.go | 436 ------------------ pkg/core/model.go | 75 --- pkg/core/server.go | 135 ------ pkg/core/serviceclass.go | 86 ---- pkg/core/system.go | 385 ---------------- pkg/manager/manager.go | 27 -- pkg/solver/milpsolver.go | 284 ------------ pkg/solver/optimizer.go | 48 -- pkg/solver/solver.go | 233 ---------- pkg/utils/helpers.go | 12 - rest-server/README.md | 294 ------------ rest-server/base.go | 38 -- rest-server/defaults.go | 16 - rest-server/handlers.go | 428 ----------------- rest-server/interfaces.go | 6 - rest-server/statefull.go | 57 --- rest-server/stateless.go | 36 -- sample-data | 1 - test/e2e/e2e_suite_test.go | 89 ++++ test/e2e/e2e_test.go | 329 +++++++++++++ test/utils/utils.go | 251 ++++++++++ 106 files changed, 3019 insertions(+), 3772 deletions(-) create mode 100644 Makefile create mode 100644 PROJECT create mode 100644 api/v1alpha1/groupversion_info.go create mode 100644 api/v1alpha1/optimizer_types.go create mode 100644 api/v1alpha1/zz_generated.deepcopy.go create mode 100644 cmd/main.go delete mode 100644 cmd/optimizer/main.go create mode 100644 config/crd/bases/llmd.llm-d.ai_optimizers.yaml create mode 100644 config/crd/kustomization.yaml create mode 100644 config/crd/kustomizeconfig.yaml create mode 100644 config/default/cert_metrics_manager_patch.yaml create mode 100644 config/default/kustomization.yaml create mode 100644 config/default/manager_metrics_patch.yaml create mode 100644 config/default/metrics_service.yaml create mode 100644 config/manager/kustomization.yaml create mode 100644 config/manager/manager.yaml create mode 100644 config/network-policy/allow-metrics-traffic.yaml create mode 100644 config/network-policy/kustomization.yaml create mode 100644 config/prometheus/kustomization.yaml create mode 100644 config/prometheus/monitor.yaml create mode 100644 config/prometheus/monitor_tls_patch.yaml create mode 100644 config/rbac/kustomization.yaml create mode 100644 config/rbac/leader_election_role.yaml create mode 100644 config/rbac/leader_election_role_binding.yaml create mode 100644 config/rbac/metrics_auth_role.yaml create mode 100644 config/rbac/metrics_auth_role_binding.yaml create mode 100644 config/rbac/metrics_reader_role.yaml create mode 100644 config/rbac/optimizer_admin_role.yaml create mode 100644 config/rbac/optimizer_editor_role.yaml create mode 100644 config/rbac/optimizer_viewer_role.yaml create mode 100644 config/rbac/role.yaml create mode 100644 config/rbac/role_binding.yaml create mode 100644 config/rbac/service_account.yaml create mode 100644 config/samples/kustomization.yaml create mode 100644 config/samples/llmd_v1alpha1_optimizer.yaml delete mode 100644 demos/generators/main.go delete mode 100644 demos/main/main.go delete mode 100644 demos/scale/main.go delete mode 100644 demos/transition/main.go delete mode 100644 docs/arch/high-level.png delete mode 100644 docs/arch/runtime.png delete mode 100644 docs/figs/Slide10.png delete mode 100644 docs/figs/Slide11.png delete mode 100644 docs/figs/Slide12.png delete mode 100644 docs/figs/Slide13.png delete mode 100644 docs/figs/Slide14.png delete mode 100644 docs/figs/Slide16.png delete mode 100644 docs/figs/Slide17.png delete mode 100644 docs/figs/Slide19.png delete mode 100644 docs/figs/Slide2.png delete mode 100644 docs/figs/Slide20.png delete mode 100644 docs/figs/Slide21.png delete mode 100644 docs/figs/Slide23.png delete mode 100644 docs/figs/Slide24.png delete mode 100644 docs/figs/Slide26.png delete mode 100644 docs/figs/Slide27.png delete mode 100644 docs/figs/Slide28.png delete mode 100644 docs/figs/Slide3.png delete mode 100644 docs/figs/Slide30.png delete mode 100644 docs/figs/Slide32.png delete mode 100644 docs/figs/Slide33.png delete mode 100644 docs/figs/Slide34.png delete mode 100644 docs/figs/Slide4.png delete mode 100644 docs/figs/Slide5.png delete mode 100644 docs/figs/Slide6.png delete mode 100644 docs/figs/Slide7.png delete mode 100644 docs/figs/Slide8.png delete mode 100644 docs/figs/Slide9.png delete mode 100644 docs/figs/slide22.png delete mode 100644 docs/slides/inferno-dynamic.pdf delete mode 100644 docs/slides/summary.pdf create mode 100644 hack/boilerplate.go.txt create mode 100644 internal/controller/optimizer_controller.go create mode 100644 internal/controller/optimizer_controller_test.go create mode 100644 internal/controller/suite_test.go create mode 100644 internal/interfaces/interfaces.go create mode 100644 internal/interfaces/types.go delete mode 100644 manifests/yamls/deploy-optimizer.yaml delete mode 100644 pkg/config/defaults.go delete mode 100644 pkg/config/types.go delete mode 100644 pkg/core/accelerator.go delete mode 100644 pkg/core/allocation.go delete mode 100644 pkg/core/model.go delete mode 100644 pkg/core/server.go delete mode 100644 pkg/core/serviceclass.go delete mode 100644 pkg/core/system.go delete mode 100644 pkg/manager/manager.go delete mode 100644 pkg/solver/milpsolver.go delete mode 100644 pkg/solver/optimizer.go delete mode 100644 pkg/solver/solver.go delete mode 100644 pkg/utils/helpers.go delete mode 100644 rest-server/README.md delete mode 100644 rest-server/base.go delete mode 100644 rest-server/defaults.go delete mode 100644 rest-server/handlers.go delete mode 100644 rest-server/interfaces.go delete mode 100644 rest-server/statefull.go delete mode 100644 rest-server/stateless.go delete mode 160000 sample-data create mode 100644 test/e2e/e2e_suite_test.go create mode 100644 test/e2e/e2e_test.go create mode 100644 test/utils/utils.go diff --git a/Dockerfile b/Dockerfile index d78a93dd4..348b8372c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,30 +1,33 @@ -# Use a multi-stage build -FROM golang:1.23-bookworm AS builder +# Build the manager binary +FROM docker.io/golang:1.23 AS builder +ARG TARGETOS +ARG TARGETARCH -# Install the lpsolve package -RUN apt-get update && apt-get install -y liblpsolve55-dev +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download -WORKDIR /app -COPY . . +# Copy the go source +COPY cmd/main.go cmd/main.go +COPY api/ api/ +COPY internal/ internal/ -# Set CGO flags for lpsolve package -ENV CGO_CFLAGS="-I/usr/include/lpsolve" -ENV CGO_LDFLAGS="-llpsolve55 -lm -ldl -lcolamd" +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go -# Build all main.go files in cmd directory -RUN for file in $(find cmd -name "main.go"); do \ - dir=$(dirname "$file"); \ - name=$(basename "$dir"); \ - go build -o bin/$name $file; \ - done +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/manager . +USER 65532:65532 -# Create the final image -FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y liblpsolve55-dev -COPY --from=builder /app/bin /bin - -# Expose the port the API will listen on -EXPOSE 8080 - -# Command to run the binary when the container starts -CMD ["optimizer"] \ No newline at end of file +ENTRYPOINT ["/manager"] diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..d95a2bbb4 --- /dev/null +++ b/Makefile @@ -0,0 +1,225 @@ +# Image URL to use all building/pushing image targets +IMG ?= controller:latest + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# CONTAINER_TOOL defines the container tool to be used for building images. +# Be aware that the target commands are only tested with Docker which is +# scaffolded by default. However, you might want to replace it to use other +# tools. (i.e. podman) +CONTAINER_TOOL ?= docker + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk command is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: manifests generate fmt vet setup-envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out + +# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. +# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. +# CertManager is installed by default; skip with: +# - CERT_MANAGER_INSTALL_SKIP=true +.PHONY: test-e2e +test-e2e: manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. + @command -v $(KIND) >/dev/null 2>&1 || { \ + echo "Kind is not installed. Please install Kind manually."; \ + exit 1; \ + } + @$(KIND) get clusters | grep -q 'kind' || { \ + echo "No Kind cluster is running. Please start a Kind cluster before running the e2e tests."; \ + exit 1; \ + } + go test ./test/e2e/ -v -ginkgo.v + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter + $(GOLANGCI_LINT) run + +.PHONY: lint-fix +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes + $(GOLANGCI_LINT) run --fix + +.PHONY: lint-config +lint-config: golangci-lint ## Verify golangci-lint linter configuration + $(GOLANGCI_LINT) config verify + +##@ Build + +.PHONY: build +build: manifests generate fmt vet ## Build manager binary. + go build -o bin/manager cmd/main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./cmd/main.go + +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: ## Build docker image with the manager. + $(CONTAINER_TOOL) build -t ${IMG} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + $(CONTAINER_TOOL) push ${IMG} + +# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ +# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) +# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - $(CONTAINER_TOOL) buildx create --name inferno-autoscaler-builder + $(CONTAINER_TOOL) buildx use inferno-autoscaler-builder + - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - $(CONTAINER_TOOL) buildx rm inferno-autoscaler-builder + rm Dockerfile.cross + +.PHONY: build-installer +build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. + mkdir -p dist + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default > dist/install.yaml + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + +.PHONY: undeploy +undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUBECTL ?= kubectl +KIND ?= kind +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.6.0 +CONTROLLER_TOOLS_VERSION ?= v0.17.2 +#ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) +ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') +#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) +ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') +GOLANGCI_LINT_VERSION ?= v1.63.4 + +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. +$(KUSTOMIZE): $(LOCALBIN) + $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +$(CONTROLLER_GEN): $(LOCALBIN) + $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) + +.PHONY: setup-envtest +setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory. + @echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..." + @$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path || { \ + echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \ + exit 1; \ + } + +.PHONY: envtest +envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. +$(ENVTEST): $(LOCALBIN) + $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) + +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. +$(GOLANGCI_LINT): $(LOCALBIN) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + +# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist +# $1 - target path with name of binary +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f "$(1)-$(3)" ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +rm -f $(1) || true ;\ +GOBIN=$(LOCALBIN) go install $${package} ;\ +mv $(1) $(1)-$(3) ;\ +} ;\ +ln -sf $(1)-$(3) $(1) +endef diff --git a/PROJECT b/PROJECT new file mode 100644 index 000000000..29f0a0a68 --- /dev/null +++ b/PROJECT @@ -0,0 +1,20 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: llm-d.ai +layout: +- go.kubebuilder.io/v4 +projectName: inferno-autoscaler +repo: github.com/llm-d-incubation/inferno-autoscaler +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: llm-d.ai + group: llmd + kind: Optimizer + path: github.com/llm-d-incubation/inferno-autoscaler/api/v1alpha1 + version: v1alpha1 +version: "3" diff --git a/README.md b/README.md index c9dfd5567..05097dba4 100644 --- a/README.md +++ b/README.md @@ -1,155 +1,135 @@ -# Inference system optimizer +# inferno-autoscaler +// TODO(user): Add simple overview of use/purpose -The inference system optimizer assigns GPU types to inference model servers and decides on the number of replicas for each model for a given request traffic load and classes of service, as well as the batch size. ([slides](docs/slides/inferno-dynamic.pdf)) +## Description +// TODO(user): An in-depth paragraph about your project and overview of use -## Building +## Getting Started -```bash -docker build -t inferno . --load -``` - -## Prerequisites - -- lp_solve Mixed Integer Linear Programming (MILP) solver - - [Installation instructions and code](https://github.com/llm-inferno/lpsolve) - -- IBM CPLEX (optional) - - Information and instructions [IBM CPLEX as a solver](https://github.com/llm-inferno/lpsolve/tree/main/cplex) - -## Running - -First, install [prerequisites](#prerequisites) if running locally (not using an image). - -### I. Optimizer only - -There are two ways to run the optimizer. - -1. **Direct function calls**: An example is provided in [main.go](demos/main/main.go). - - ```bash - cd demos/main - go run main.go - ``` - -2. **REST API server**: The optimizer may run as a REST API server ([steps](#steps-to-run-the-optimizer-as-a-rest-api-server)). - -### II. Optimized auto-scaler - -One may run the optimizer as part of an auto-scaling control system, in one of two ways. - -1. **Kubernetes controller**: Running in a Kubernetes cluster and using custom resources and a Kubernetes runtime controller, the optimizer may be excercised in reconciliation to updates to the Optimizer custom resource ([reference](https://github.com/llm-inferno/controller)). - -2. **Optimization control loop**: The control loop comprises (1) a Collector to get data about the inference servers through Prometheus and server deployments, (2) an Optimizer to make decisions, (3) an Actuator to realize such decisions by updating server deployments, and (4) a periodic Controller that has access to static and dynamic data. The [control loop](https://github.com/llm-inferno/control-loop) may run either externally or in a Kubernetes cluster. - -### Steps to run the optimizer as a REST API server +### Prerequisites +- go version v1.23.0+ +- docker version 17.03+. +- kubectl version v1.11.3+. +- Access to a Kubernetes v1.11.3+ cluster. -The REST API specifications are [documented](rest-server/README.md). +### To Deploy on the cluster +**Build and push your image to the location specified by `IMG`:** -Clone this repository and set environment variable `INFERNO_REPO` to the path to it. - -#### Option A: Run externally - -```bash -cd $INFERNO_REPO/cmd/optimizer -go run main.go [-F] +```sh +make docker-build docker-push IMG=/inferno-autoscaler:tag ``` -The default is to run the server in **Stateless** mode. Use the optional `-F` argument to run in **Statefull** mode. ([Description of modes](rest-server/README.md#rest-server-modes)) - -You may then curl [API commands](rest-server/README.md#commands-list) to `http://localhost:8080`. - -#### Option B: Run in cluster +**NOTE:** This image ought to be published in the personal registry you specified. +And it is required to have access to pull the image from the working environment. +Make sure you have the proper permission to the registry if the above commands don’t work. -- Deploy optimizer as a deployment, along with a service on port `80`, in name space `inferno` in the cluster. (The deployment yaml file starts the server in a container with the `-F` flag.) +**Install the CRDs into the cluster:** - ```bash - cd $INFERNO_REPO/manifests/yamls - kubectl apply -f deploy-optimizer.yaml - ``` - -- Forward port to local host. - - ```bash - kubectl port-forward service/inferno-optimizer -n inferno 8080:80 - ``` - - You may then curl API commands (above) to `http://localhost:8080`. +```sh +make install +``` -- (Optional) Inspect logs. +**Deploy the Manager to the cluster with the image specified by `IMG`:** - ```bash - POD=$(kubectl get pod -l app=inferno-optimizer -n inferno -o jsonpath="{.items[0].metadata.name}") - kubectl logs -f $POD -n inferno - ``` +```sh +make deploy IMG=/inferno-autoscaler:tag +``` -- Cleanup. +> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin +privileges or be logged in as admin. - ```bash - kubectl delete -f deploy-optimizer.yaml - ``` +**Create instances of your solution** +You can apply the samples (examples) from the config/sample: -## Detailed description of the optimizer +```sh +kubectl apply -k config/samples/ +``` -![problem-scope](docs/figs/Slide5.png) +>**NOTE**: Ensure that the samples has default values to test it out. -![timing-definitions](docs/figs/Slide30.png) +### To Uninstall +**Delete the instances (CRs) from the cluster:** -![request-batching](docs/figs/Slide6.png) +```sh +kubectl delete -k config/samples/ +``` -![token-time-fitting](docs/figs/Slide7.png) +**Delete the APIs(CRDs) from the cluster:** -![modeling-batching](docs/figs/Slide9.png) +```sh +make uninstall +``` -![qn-model](docs/figs/Slide8.png) +**UnDeploy the controller from the cluster:** -![system-occupancy](docs/figs/Slide32.png) +```sh +make undeploy +``` -![impact-batch](docs/figs/Slide33.png) +## Project Distribution -![target-service](docs/figs/Slide34.png) +Following the options to release and provide this solution to the users. -Decision variables +### By providing a bundle with all YAML files -For each pair of (class of service, model): +1. Build the installer for the image built and published in the registry: -- gpuProfile: the GPU type allocated -- numReplicas: the number of replicas -- batchSize: the batch size, given continuous batching +```sh +make build-installer IMG=/inferno-autoscaler:tag +``` -## Specifications: Accelerators and models +**NOTE:** The makefile target mentioned above generates an 'install.yaml' +file in the dist directory. This file contains all the resources built +with Kustomize, which are necessary to install this project without its +dependencies. -![accelerators](docs/figs/Slide13.png) +2. Using the installer -![models](docs/figs/Slide14.png) +Users can just run 'kubectl apply -f ' to install +the project, i.e.: -## Example 1: Unlimited accelerators +```sh +kubectl apply -f https://raw.githubusercontent.com//inferno-autoscaler//dist/install.yaml +``` -![unlimited-assign](docs/figs/Slide16.png) +### By providing a Helm Chart -![unlimited-perf](docs/figs/Slide17.png) +1. Build the chart using the optional helm plugin -## Example 2: Load change - Unlimited accelerators +```sh +kubebuilder edit --plugins=helm/v1-alpha +``` -![unlimited-change-assign](docs/figs/Slide19.png) +2. See that a chart was generated under 'dist/chart', and users +can obtain this solution from there. -![unlimited-change](docs/figs/Slide20.png) +**NOTE:** If you change the project, you need to update the Helm Chart +using the same command above to sync the latest changes. Furthermore, +if you create webhooks, you need to use the above command with +the '--force' flag and manually ensure that any custom configuration +previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml' +is manually re-applied afterwards. -![unlimited-change-perf](docs/figs/Slide21.png) +## Contributing +// TODO(user): Add detailed information on how you would like others to contribute to this project -## Example 3: Limited accelerators +**NOTE:** Run `make help` for more information on all potential `make` targets -![limited-count](docs/figs/Slide22.png) +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) -![limited-assign](docs/figs/Slide23.png) +## License -![limited-perf](docs/figs/Slide24.png) +Copyright 2025. -## Example 4: Load change - Limited accelerators +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -![limited-change-assign](docs/figs/Slide26.png) + http://www.apache.org/licenses/LICENSE-2.0 -![limited-change](docs/figs/Slide27.png) +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. -![limited-change-perf](docs/figs/Slide28.png) diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go new file mode 100644 index 000000000..597751e91 --- /dev/null +++ b/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains API Schema definitions for the llmd v1alpha1 API group. +// +kubebuilder:object:generate=true +// +groupName=llmd.llm-d.ai +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "llmd.llm-d.ai", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1alpha1/optimizer_types.go b/api/v1alpha1/optimizer_types.go new file mode 100644 index 000000000..de1c6c64e --- /dev/null +++ b/api/v1alpha1/optimizer_types.go @@ -0,0 +1,72 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=opt +// +kubebuilder:printcolumn:name="Model",type=string,JSONPath=".spec.modelID",description="Target model ID" +// +kubebuilder:printcolumn:name="LastRun",type=date,JSONPath=".status.lastRunTime",description="Last optimization run" + +// Optimizer is the Schema for the optimizers API +type Optimizer struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec OptimizerSpec `json:"spec,omitempty"` + Status OptimizerStatus `json:"status,omitempty"` +} + +// OptimizerSpec defines the desired configuration for the optimizer controller. +type OptimizerSpec struct { + // ModelID is the unique identifier for the model to optimize + ModelID string `json:"modelID"` +} + +// ReplicaTargetEntry defines how many replicas are needed for a given role and variant. +type ReplicaTargetEntry struct { + VariantID string `json:"variantID"` + Role string `json:"role"` + Replicas int `json:"replicas"` + PreviousReplicas int `json:"previousReplicas,omitempty"` + Reason string `json:"reason,omitempty"` +} + +// OptimizerStatus captures the latest outcome of optimization. +// +kubebuilder:validation:Optional +type OptimizerStatus struct { + LastRunTime metav1.Time `json:"lastRunTime,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + ReplicaTargets []ReplicaTargetEntry `json:"replicaTargets,omitempty"` +} + +// +kubebuilder:object:root=true + +// OptimizerList contains a list of Optimizer +type OptimizerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Optimizer `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Optimizer{}, &OptimizerList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..49ccbd6c0 --- /dev/null +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,143 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Optimizer) DeepCopyInto(out *Optimizer) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Optimizer. +func (in *Optimizer) DeepCopy() *Optimizer { + if in == nil { + return nil + } + out := new(Optimizer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Optimizer) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OptimizerList) DeepCopyInto(out *OptimizerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Optimizer, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OptimizerList. +func (in *OptimizerList) DeepCopy() *OptimizerList { + if in == nil { + return nil + } + out := new(OptimizerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OptimizerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OptimizerSpec) DeepCopyInto(out *OptimizerSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OptimizerSpec. +func (in *OptimizerSpec) DeepCopy() *OptimizerSpec { + if in == nil { + return nil + } + out := new(OptimizerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OptimizerStatus) DeepCopyInto(out *OptimizerStatus) { + *out = *in + in.LastRunTime.DeepCopyInto(&out.LastRunTime) + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ReplicaTargets != nil { + in, out := &in.ReplicaTargets, &out.ReplicaTargets + *out = make([]ReplicaTargetEntry, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OptimizerStatus. +func (in *OptimizerStatus) DeepCopy() *OptimizerStatus { + if in == nil { + return nil + } + out := new(OptimizerStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReplicaTargetEntry) DeepCopyInto(out *ReplicaTargetEntry) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReplicaTargetEntry. +func (in *ReplicaTargetEntry) DeepCopy() *ReplicaTargetEntry { + if in == nil { + return nil + } + out := new(ReplicaTargetEntry) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 000000000..4365b2be3 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,244 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "crypto/tls" + "flag" + "os" + "path/filepath" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/certwatcher" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + llmdv1alpha1 "github.com/llm-d-incubation/inferno-autoscaler/api/v1alpha1" + "github.com/llm-d-incubation/inferno-autoscaler/internal/controller" + // +kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(llmdv1alpha1.AddToScheme(scheme)) + // +kubebuilder:scaffold:scheme +} + +// nolint:gocyclo +func main() { + var metricsAddr string + var metricsCertPath, metricsCertName, metricsCertKey string + var webhookCertPath, webhookCertName, webhookCertKey string + var enableLeaderElection bool + var probeAddr string + var secureMetrics bool + var enableHTTP2 bool + var tlsOpts []func(*tls.Config) + flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ + "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.BoolVar(&secureMetrics, "metrics-secure", true, + "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") + flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") + flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") + flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") + flag.StringVar(&metricsCertPath, "metrics-cert-path", "", + "The directory that contains the metrics server certificate.") + flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") + flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") + flag.BoolVar(&enableHTTP2, "enable-http2", false, + "If set, HTTP/2 will be enabled for the metrics and webhook servers") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + // if the enable-http2 flag is false (the default), http/2 should be disabled + // due to its vulnerabilities. More specifically, disabling http/2 will + // prevent from being vulnerable to the HTTP/2 Stream Cancellation and + // Rapid Reset CVEs. For more information see: + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 + // - https://github.com/advisories/GHSA-4374-p667-p6c8 + disableHTTP2 := func(c *tls.Config) { + setupLog.Info("disabling http/2") + c.NextProtos = []string{"http/1.1"} + } + + if !enableHTTP2 { + tlsOpts = append(tlsOpts, disableHTTP2) + } + + // Create watchers for metrics and webhooks certificates + var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher + + // Initial webhook TLS options + webhookTLSOpts := tlsOpts + + if len(webhookCertPath) > 0 { + setupLog.Info("Initializing webhook certificate watcher using provided certificates", + "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) + + var err error + webhookCertWatcher, err = certwatcher.New( + filepath.Join(webhookCertPath, webhookCertName), + filepath.Join(webhookCertPath, webhookCertKey), + ) + if err != nil { + setupLog.Error(err, "Failed to initialize webhook certificate watcher") + os.Exit(1) + } + + webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { + config.GetCertificate = webhookCertWatcher.GetCertificate + }) + } + + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: webhookTLSOpts, + }) + + // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. + // More info: + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.4/pkg/metrics/server + // - https://book.kubebuilder.io/reference/metrics.html + metricsServerOptions := metricsserver.Options{ + BindAddress: metricsAddr, + SecureServing: secureMetrics, + TLSOpts: tlsOpts, + } + + if secureMetrics { + // FilterProvider is used to protect the metrics endpoint with authn/authz. + // These configurations ensure that only authorized users and service accounts + // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.4/pkg/metrics/filters#WithAuthenticationAndAuthorization + metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization + } + + // If the certificate is not specified, controller-runtime will automatically + // generate self-signed certificates for the metrics server. While convenient for development and testing, + // this setup is not recommended for production. + // + // TODO(user): If you enable certManager, uncomment the following lines: + // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates + // managed by cert-manager for the metrics server. + // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. + if len(metricsCertPath) > 0 { + setupLog.Info("Initializing metrics certificate watcher using provided certificates", + "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) + + var err error + metricsCertWatcher, err = certwatcher.New( + filepath.Join(metricsCertPath, metricsCertName), + filepath.Join(metricsCertPath, metricsCertKey), + ) + if err != nil { + setupLog.Error(err, "to initialize metrics certificate watcher", "error", err) + os.Exit(1) + } + + metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) { + config.GetCertificate = metricsCertWatcher.GetCertificate + }) + } + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsServerOptions, + WebhookServer: webhookServer, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "72dd1cf1.llm-d.ai", + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + // LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + if err = (&controller.OptimizerReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Optimizer") + os.Exit(1) + } + // +kubebuilder:scaffold:builder + + if metricsCertWatcher != nil { + setupLog.Info("Adding metrics certificate watcher to manager") + if err := mgr.Add(metricsCertWatcher); err != nil { + setupLog.Error(err, "unable to add metrics certificate watcher to manager") + os.Exit(1) + } + } + + if webhookCertWatcher != nil { + setupLog.Info("Adding webhook certificate watcher to manager") + if err := mgr.Add(webhookCertWatcher); err != nil { + setupLog.Error(err, "unable to add webhook certificate watcher to manager") + os.Exit(1) + } + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/cmd/optimizer/main.go b/cmd/optimizer/main.go deleted file mode 100644 index 510079c40..000000000 --- a/cmd/optimizer/main.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "os" - - rest "github.com/llm-inferno/inferno/rest-server" -) - -// create and run a REST API Optimizer server -// - stateless (default) or statefull (with -F argument) -func main() { - var server rest.RESTServer - statefull := len(os.Args) > 1 && os.Args[1] == rest.DefaultStatefull - if statefull { - server = rest.NewStateFullServer() - } else { - server = rest.NewStateLessServer() - } - server.Run() -} diff --git a/config/crd/bases/llmd.llm-d.ai_optimizers.yaml b/config/crd/bases/llmd.llm-d.ai_optimizers.yaml new file mode 100644 index 000000000..1d2223e76 --- /dev/null +++ b/config/crd/bases/llmd.llm-d.ai_optimizers.yaml @@ -0,0 +1,148 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: optimizers.llmd.llm-d.ai +spec: + group: llmd.llm-d.ai + names: + kind: Optimizer + listKind: OptimizerList + plural: optimizers + shortNames: + - opt + singular: optimizer + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Target model ID + jsonPath: .spec.modelID + name: Model + type: string + - description: Last optimization run + jsonPath: .status.lastRunTime + name: LastRun + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Optimizer is the Schema for the optimizers API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: OptimizerSpec defines the desired configuration for the optimizer + controller. + properties: + modelID: + description: ModelID is the unique identifier for the model to optimize + type: string + required: + - modelID + type: object + status: + description: OptimizerStatus captures the latest outcome of optimization. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastRunTime: + format: date-time + type: string + replicaTargets: + items: + description: ReplicaTargetEntry defines how many replicas are needed + for a given role and variant. + properties: + previousReplicas: + type: integer + reason: + type: string + replicas: + type: integer + role: + type: string + variantID: + type: string + required: + - replicas + - role + - variantID + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml new file mode 100644 index 000000000..c152b3076 --- /dev/null +++ b/config/crd/kustomization.yaml @@ -0,0 +1,16 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/llmd.llm-d.ai_optimizers.yaml +# +kubebuilder:scaffold:crdkustomizeresource + +patches: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +# +kubebuilder:scaffold:crdkustomizewebhookpatch + +# [WEBHOOK] To enable webhook, uncomment the following section +# the following config is for teaching kustomize how to do kustomization for CRDs. +#configurations: +#- kustomizeconfig.yaml diff --git a/config/crd/kustomizeconfig.yaml b/config/crd/kustomizeconfig.yaml new file mode 100644 index 000000000..ec5c150a9 --- /dev/null +++ b/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/config/default/cert_metrics_manager_patch.yaml b/config/default/cert_metrics_manager_patch.yaml new file mode 100644 index 000000000..d97501553 --- /dev/null +++ b/config/default/cert_metrics_manager_patch.yaml @@ -0,0 +1,30 @@ +# This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs. + +# Add the volumeMount for the metrics-server certs +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + mountPath: /tmp/k8s-metrics-server/metrics-certs + name: metrics-certs + readOnly: true + +# Add the --metrics-cert-path argument for the metrics server +- op: add + path: /spec/template/spec/containers/0/args/- + value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs + +# Add the metrics-server certs volume configuration +- op: add + path: /spec/template/spec/volumes/- + value: + name: metrics-certs + secret: + secretName: metrics-server-cert + optional: false + items: + - key: ca.crt + path: ca.crt + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml new file mode 100644 index 000000000..931d48530 --- /dev/null +++ b/config/default/kustomization.yaml @@ -0,0 +1,234 @@ +# Adds namespace to all resources. +namespace: inferno-autoscaler-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: inferno-autoscaler- + +# Labels to add to all resources and selectors. +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue + +resources: +- ../crd +- ../rbac +- ../manager +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus +# [METRICS] Expose the controller manager metrics service. +- metrics_service.yaml +# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. +# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. +# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will +# be able to communicate with the Webhook Server. +#- ../network-policy + +# Uncomment the patches line if you enable Metrics +patches: +# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. +# More info: https://book.kubebuilder.io/reference/metrics +- path: manager_metrics_patch.yaml + target: + kind: Deployment + +# Uncomment the patches line if you enable Metrics and CertManager +# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. +# This patch will protect the metrics with certManager self-signed certs. +#- path: cert_metrics_manager_patch.yaml +# target: +# kind: Deployment + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- path: manager_webhook_patch.yaml +# target: +# kind: Deployment + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +# Uncomment the following replacements to add the cert-manager CA injection annotations +#replacements: +# - source: # Uncomment the following block to enable certificates for metrics +# kind: Service +# version: v1 +# name: controller-manager-metrics-service +# fieldPath: metadata.name +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: metrics-certs +# fieldPaths: +# - spec.dnsNames.0 +# - spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor +# kind: ServiceMonitor +# group: monitoring.coreos.com +# version: v1 +# name: controller-manager-metrics-monitor +# fieldPaths: +# - spec.endpoints.0.tlsConfig.serverName +# options: +# delimiter: '.' +# index: 0 +# create: true +# +# - source: +# kind: Service +# version: v1 +# name: controller-manager-metrics-service +# fieldPath: metadata.namespace +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: metrics-certs +# fieldPaths: +# - spec.dnsNames.0 +# - spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true +# - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor +# kind: ServiceMonitor +# group: monitoring.coreos.com +# version: v1 +# name: controller-manager-metrics-monitor +# fieldPaths: +# - spec.endpoints.0.tlsConfig.serverName +# options: +# delimiter: '.' +# index: 1 +# create: true +# +# - source: # Uncomment the following block if you have any webhook +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name # Name of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.namespace # Namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true +# +# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # This name should match the one in certificate.yaml +# fieldPath: .metadata.namespace # Namespace of the certificate CR +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# +# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.namespace # Namespace of the certificate CR +# targets: +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.name +# targets: +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# +# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.namespace # Namespace of the certificate CR +# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. +# +kubebuilder:scaffold:crdkustomizecainjectionns +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.name +# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. +# +kubebuilder:scaffold:crdkustomizecainjectionname diff --git a/config/default/manager_metrics_patch.yaml b/config/default/manager_metrics_patch.yaml new file mode 100644 index 000000000..2aaef6536 --- /dev/null +++ b/config/default/manager_metrics_patch.yaml @@ -0,0 +1,4 @@ +# This patch adds the args to allow exposing the metrics endpoint using HTTPS +- op: add + path: /spec/template/spec/containers/0/args/0 + value: --metrics-bind-address=:8443 diff --git a/config/default/metrics_service.yaml b/config/default/metrics_service.yaml new file mode 100644 index 000000000..217ec92c0 --- /dev/null +++ b/config/default/metrics_service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: inferno-autoscaler + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: controller-manager + app.kubernetes.io/name: inferno-autoscaler diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml new file mode 100644 index 000000000..5c5f0b84c --- /dev/null +++ b/config/manager/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- manager.yaml diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml new file mode 100644 index 000000000..4ca0f7e13 --- /dev/null +++ b/config/manager/manager.yaml @@ -0,0 +1,98 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: inferno-autoscaler + app.kubernetes.io/managed-by: kustomize + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager + app.kubernetes.io/name: inferno-autoscaler + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: controller-manager + app.kubernetes.io/name: inferno-autoscaler + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + app.kubernetes.io/name: inferno-autoscaler + spec: + # TODO(user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + securityContext: + # Projects are configured by default to adhere to the "restricted" Pod Security Standards. + # This ensures that deployments meet the highest security requirements for Kubernetes. + # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - command: + - /manager + args: + - --leader-elect + - --health-probe-bind-address=:8081 + image: controller:latest + name: manager + ports: [] + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + volumeMounts: [] + volumes: [] + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/config/network-policy/allow-metrics-traffic.yaml b/config/network-policy/allow-metrics-traffic.yaml new file mode 100644 index 000000000..688cc37e0 --- /dev/null +++ b/config/network-policy/allow-metrics-traffic.yaml @@ -0,0 +1,27 @@ +# This NetworkPolicy allows ingress traffic +# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those +# namespaces are able to gather data from the metrics endpoint. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/name: inferno-autoscaler + app.kubernetes.io/managed-by: kustomize + name: allow-metrics-traffic + namespace: system +spec: + podSelector: + matchLabels: + control-plane: controller-manager + app.kubernetes.io/name: inferno-autoscaler + policyTypes: + - Ingress + ingress: + # This allows ingress traffic from any namespace with the label metrics: enabled + - from: + - namespaceSelector: + matchLabels: + metrics: enabled # Only from namespaces with this label + ports: + - port: 8443 + protocol: TCP diff --git a/config/network-policy/kustomization.yaml b/config/network-policy/kustomization.yaml new file mode 100644 index 000000000..ec0fb5e57 --- /dev/null +++ b/config/network-policy/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- allow-metrics-traffic.yaml diff --git a/config/prometheus/kustomization.yaml b/config/prometheus/kustomization.yaml new file mode 100644 index 000000000..fdc5481b1 --- /dev/null +++ b/config/prometheus/kustomization.yaml @@ -0,0 +1,11 @@ +resources: +- monitor.yaml + +# [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus +# to securely reference certificates created and managed by cert-manager. +# Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml +# to mount the "metrics-server-cert" secret in the Manager Deployment. +#patches: +# - path: monitor_tls_patch.yaml +# target: +# kind: ServiceMonitor diff --git a/config/prometheus/monitor.yaml b/config/prometheus/monitor.yaml new file mode 100644 index 000000000..5eb5f4c02 --- /dev/null +++ b/config/prometheus/monitor.yaml @@ -0,0 +1,27 @@ +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: inferno-autoscaler + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https # Ensure this is the name of the port that exposes HTTPS metrics + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables + # certificate verification, exposing the system to potential man-in-the-middle attacks. + # For production environments, it is recommended to use cert-manager for automatic TLS certificate management. + # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml, + # which securely references the certificate from the 'metrics-server-cert' secret. + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager + app.kubernetes.io/name: inferno-autoscaler diff --git a/config/prometheus/monitor_tls_patch.yaml b/config/prometheus/monitor_tls_patch.yaml new file mode 100644 index 000000000..5bf84ce0d --- /dev/null +++ b/config/prometheus/monitor_tls_patch.yaml @@ -0,0 +1,19 @@ +# Patch for Prometheus ServiceMonitor to enable secure TLS configuration +# using certificates managed by cert-manager +- op: replace + path: /spec/endpoints/0/tlsConfig + value: + # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize + serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc + insecureSkipVerify: false + ca: + secret: + name: metrics-server-cert + key: ca.crt + cert: + secret: + name: metrics-server-cert + key: tls.crt + keySecret: + name: metrics-server-cert + key: tls.key diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml new file mode 100644 index 000000000..385f2040d --- /dev/null +++ b/config/rbac/kustomization.yaml @@ -0,0 +1,28 @@ +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# The following RBAC configurations are used to protect +# the metrics endpoint with authn/authz. These configurations +# ensure that only authorized users and service accounts +# can access the metrics endpoint. Comment the following +# permissions if you want to disable this protection. +# More info: https://book.kubebuilder.io/reference/metrics.html +- metrics_auth_role.yaml +- metrics_auth_role_binding.yaml +- metrics_reader_role.yaml +# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by +# default, aiding admins in cluster management. Those roles are +# not used by the {{ .ProjectName }} itself. You can comment the following lines +# if you do not want those helpers be installed with your Project. +- optimizer_admin_role.yaml +- optimizer_editor_role.yaml +- optimizer_viewer_role.yaml + diff --git a/config/rbac/leader_election_role.yaml b/config/rbac/leader_election_role.yaml new file mode 100644 index 000000000..567dfb985 --- /dev/null +++ b/config/rbac/leader_election_role.yaml @@ -0,0 +1,40 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: inferno-autoscaler + app.kubernetes.io/managed-by: kustomize + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/config/rbac/leader_election_role_binding.yaml b/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 000000000..cda7c0877 --- /dev/null +++ b/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: inferno-autoscaler + app.kubernetes.io/managed-by: kustomize + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/rbac/metrics_auth_role.yaml b/config/rbac/metrics_auth_role.yaml new file mode 100644 index 000000000..32d2e4ec6 --- /dev/null +++ b/config/rbac/metrics_auth_role.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-auth-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/config/rbac/metrics_auth_role_binding.yaml b/config/rbac/metrics_auth_role_binding.yaml new file mode 100644 index 000000000..e775d67ff --- /dev/null +++ b/config/rbac/metrics_auth_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: metrics-auth-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: metrics-auth-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/rbac/metrics_reader_role.yaml b/config/rbac/metrics_reader_role.yaml new file mode 100644 index 000000000..51a75db47 --- /dev/null +++ b/config/rbac/metrics_reader_role.yaml @@ -0,0 +1,9 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/config/rbac/optimizer_admin_role.yaml b/config/rbac/optimizer_admin_role.yaml new file mode 100644 index 000000000..d745fc1bc --- /dev/null +++ b/config/rbac/optimizer_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project inferno-autoscaler itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over llmd.llm-d.ai. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: inferno-autoscaler + app.kubernetes.io/managed-by: kustomize + name: optimizer-admin-role +rules: +- apiGroups: + - llmd.llm-d.ai + resources: + - optimizers + verbs: + - '*' +- apiGroups: + - llmd.llm-d.ai + resources: + - optimizers/status + verbs: + - get diff --git a/config/rbac/optimizer_editor_role.yaml b/config/rbac/optimizer_editor_role.yaml new file mode 100644 index 000000000..fa4e9c040 --- /dev/null +++ b/config/rbac/optimizer_editor_role.yaml @@ -0,0 +1,33 @@ +# This rule is not used by the project inferno-autoscaler itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the llmd.llm-d.ai. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: inferno-autoscaler + app.kubernetes.io/managed-by: kustomize + name: optimizer-editor-role +rules: +- apiGroups: + - llmd.llm-d.ai + resources: + - optimizers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - llmd.llm-d.ai + resources: + - optimizers/status + verbs: + - get diff --git a/config/rbac/optimizer_viewer_role.yaml b/config/rbac/optimizer_viewer_role.yaml new file mode 100644 index 000000000..9c5fdcc4c --- /dev/null +++ b/config/rbac/optimizer_viewer_role.yaml @@ -0,0 +1,29 @@ +# This rule is not used by the project inferno-autoscaler itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to llmd.llm-d.ai resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: inferno-autoscaler + app.kubernetes.io/managed-by: kustomize + name: optimizer-viewer-role +rules: +- apiGroups: + - llmd.llm-d.ai + resources: + - optimizers + verbs: + - get + - list + - watch +- apiGroups: + - llmd.llm-d.ai + resources: + - optimizers/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 000000000..3b9793e1c --- /dev/null +++ b/config/rbac/role.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +- apiGroups: + - llmd.llm-d.ai + resources: + - optimizers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - llmd.llm-d.ai + resources: + - optimizers/finalizers + verbs: + - update +- apiGroups: + - llmd.llm-d.ai + resources: + - optimizers/status + verbs: + - get + - patch + - update diff --git a/config/rbac/role_binding.yaml b/config/rbac/role_binding.yaml new file mode 100644 index 000000000..81f9f9dd2 --- /dev/null +++ b/config/rbac/role_binding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: inferno-autoscaler + app.kubernetes.io/managed-by: kustomize + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/rbac/service_account.yaml b/config/rbac/service_account.yaml new file mode 100644 index 000000000..2a2d1fd94 --- /dev/null +++ b/config/rbac/service_account.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: inferno-autoscaler + app.kubernetes.io/managed-by: kustomize + name: controller-manager + namespace: system diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml new file mode 100644 index 000000000..b6276ac17 --- /dev/null +++ b/config/samples/kustomization.yaml @@ -0,0 +1,4 @@ +## Append samples of your project ## +resources: +- llmd_v1alpha1_optimizer.yaml +# +kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/llmd_v1alpha1_optimizer.yaml b/config/samples/llmd_v1alpha1_optimizer.yaml new file mode 100644 index 000000000..991f53b99 --- /dev/null +++ b/config/samples/llmd_v1alpha1_optimizer.yaml @@ -0,0 +1,9 @@ +apiVersion: llmd.llm-d.ai/v1alpha1 +kind: Optimizer +metadata: + labels: + app.kubernetes.io/name: inferno-autoscaler + app.kubernetes.io/managed-by: kustomize + name: optimizer-sample +spec: + # TODO(user): Add fields here diff --git a/demos/generators/main.go b/demos/generators/main.go deleted file mode 100644 index 7dfb19378..000000000 --- a/demos/generators/main.go +++ /dev/null @@ -1,153 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - - "github.com/llm-inferno/inferno/pkg/config" -) - -func main() { - // accelerator names - aNames := []string{"AIU2", "L4", "L40S", "MI210", "A100", "G2", "MI250", "H100", - "MI300X", "2xAIU2", "2xMI210", "2xA100", "2xG2", "2xMI250", "2xH100", "2xMI300X", - "4xAIU2", "4xMI210", "4xA100", "4xG2", "4xMI250"} - - // model names - mNames := []string{"granite_13b", "granite_20b", "granite_34b", "llama_7b", "llama_13b", - "llama3_8b", "llama_70b", "mistral_7b", "mixtral_8_7b"} - - // model specs - // 1D: models - // 2D: accelerators x models - - // memSize := []int{39, 60, 102, 21, 39, 72, 210, 21, 168} - - alpha := [][]float32{ - {205.80, 297.80, 487.70, 116.90, 201.30, 129.30, 274.10, 123.20, 296.30}, - {137.20, 198.53, 325.13, 77.93, 134.20, 86.20, 182.73, 82.13, 197.53}, - {47.86, 69.26, 113.42, 27.19, 46.81, 30.07, 63.74, 28.65, 68.91}, - {25.10, 36.32, 59.48, 14.26, 24.55, 15.77, 33.43, 15.02, 36.13}, - {20.58, 29.78, 48.77, 11.69, 20.13, 12.93, 27.41, 12.32, 29.63}, - {17.15, 24.82, 40.64, 9.74, 16.78, 10.78, 22.84, 10.27, 24.69}, - {12.86, 18.61, 30.48, 7.31, 12.58, 8.08, 17.13, 7.70, 18.52}, - {12.25, 17.73, 29.03, 6.96, 11.98, 7.70, 16.32, 7.33, 17.64}, - {7.77, 11.24, 18.40, 4.41, 7.60, 4.88, 10.34, 4.65, 11.18}, - {147.00, 212.71, 348.36, 83.50, 143.79, 92.36, 195.79, 88.00, 211.64}, - {17.74, 25.67, 42.04, 10.08, 17.35, 11.15, 23.63, 10.62, 25.54}, - {14.60, 21.12, 34.59, 8.29, 14.28, 9.17, 19.44, 8.74, 21.01}, - {12.11, 17.52, 28.69, 6.88, 11.84, 7.61, 16.12, 7.25, 17.43}, - {9.11, 13.18, 21.58, 5.17, 8.91, 5.72, 12.13, 5.45, 13.11}, - {8.65, 12.51, 20.49, 4.91, 8.46, 5.43, 11.52, 5.18, 12.45}, - {5.49, 7.94, 13.01, 3.12, 5.37, 3.45, 7.31, 3.29, 7.90}, - {102.90, 148.90, 243.85, 58.45, 100.65, 64.65, 137.05, 61.60, 148.15}, - {12.55, 18.16, 29.74, 7.13, 12.27, 7.88, 16.71, 7.51, 18.07}, - {10.29, 14.89, 24.39, 5.85, 10.07, 6.47, 13.71, 6.16, 14.82}, - {8.58, 12.41, 20.32, 4.87, 8.39, 5.39, 11.42, 5.13, 12.35}, - {6.43, 9.31, 15.24, 3.65, 6.29, 4.04, 8.57, 3.85, 9.26}, - } - - beta := [][]float32{ - {4.10, 5.00, 8.20, 6.30, 8.70, 5.90, 70.70, 4.10, 60.10}, - {2.73, 3.33, 5.47, 4.20, 5.80, 3.93, 47.13, 2.73, 40.07}, - {0.95, 1.16, 1.91, 1.47, 2.02, 1.37, 16.44, 0.95, 13.98}, - {0.50, 0.61, 1.00, 0.77, 1.06, 0.72, 8.62, 0.50, 7.33}, - {0.41, 0.50, 0.82, 0.63, 0.87, 0.59, 7.07, 0.41, 6.01}, - {0.34, 0.42, 0.68, 0.53, 0.73, 0.49, 5.89, 0.34, 5.01}, - {0.26, 0.31, 0.51, 0.39, 0.54, 0.37, 4.42, 0.26, 3.76}, - {0.24, 0.30, 0.49, 0.38, 0.52, 0.35, 4.21, 0.24, 3.58}, - {0.15, 0.19, 0.31, 0.24, 0.33, 0.22, 2.67, 0.15, 2.27}, - {2.93, 3.57, 5.86, 4.50, 6.21, 4.21, 50.50, 2.93, 42.93}, - {0.35, 0.43, 0.71, 0.54, 0.75, 0.51, 6.09, 0.35, 5.18}, - {0.29, 0.35, 0.58, 0.45, 0.62, 0.42, 5.01, 0.29, 4.26}, - {0.24, 0.29, 0.48, 0.37, 0.51, 0.35, 4.16, 0.24, 3.54}, - {0.18, 0.22, 0.36, 0.28, 0.38, 0.26, 3.13, 0.18, 2.66}, - {0.17, 0.21, 0.34, 0.26, 0.37, 0.25, 2.97, 0.17, 2.53}, - {0.11, 0.13, 0.22, 0.17, 0.23, 0.16, 1.89, 0.11, 1.60}, - {2.05, 2.50, 4.10, 3.15, 4.35, 2.95, 35.35, 2.05, 30.05}, - {0.25, 0.30, 0.50, 0.38, 0.53, 0.36, 4.31, 0.25, 3.66}, - {0.21, 0.25, 0.41, 0.32, 0.44, 0.30, 3.54, 0.21, 3.01}, - {0.17, 0.21, 0.34, 0.26, 0.36, 0.25, 2.95, 0.17, 2.50}, - {0.13, 0.16, 0.26, 0.20, 0.27, 0.18, 2.21, 0.13, 1.88}, - } - - maxBatchSize := [][]int{ - {51, 38, 19, 102, 51, 25, 8, 102, 12}, - {9, 7, 3, 19, 9, 4, 1, 19, 2}, - {19, 14, 7, 38, 19, 9, 3, 38, 4}, - {25, 19, 9, 51, 25, 12, 4, 51, 6}, - {32, 24, 12, 64, 32, 16, 5, 64, 8}, - {38, 28, 14, 76, 38, 19, 6, 76, 9}, - {51, 38, 19, 102, 51, 25, 8, 102, 12}, - {32, 24, 12, 64, 32, 16, 5, 64, 8}, - {76, 57, 28, 153, 76, 38, 12, 153, 19}, - {102, 76, 38, 204, 102, 51, 16, 204, 25}, - {51, 38, 19, 102, 51, 25, 8, 102, 12}, - {64, 48, 24, 128, 64, 32, 10, 128, 16}, - {76, 57, 28, 153, 76, 38, 12, 153, 19}, - {102, 76, 38, 204, 102, 51, 16, 204, 25}, - {64, 48, 24, 128, 64, 32, 10, 128, 16}, - {153, 115, 57, 307, 153, 76, 24, 307, 38}, - {204, 153, 76, 409, 204, 102, 32, 409, 51}, - {102, 76, 38, 204, 102, 51, 16, 204, 25}, - {128, 96, 48, 256, 128, 64, 20, 256, 32}, - {153, 115, 57, 307, 153, 76, 24, 307, 38}, - {204, 153, 76, 409, 204, 102, 32, 409, 51}, - } - - count := [][]int{ - {1, 1, 1, 1, 1, 1, 2, 1, 2}, - {2, 4, 4, 1, 2, 4, 8, 1, 8}, - {1, 2, 4, 1, 1, 2, 4, 1, 4}, - {1, 1, 2, 1, 1, 2, 4, 1, 4}, - {1, 1, 2, 1, 1, 1, 4, 1, 4}, - {1, 1, 2, 1, 1, 1, 4, 1, 2}, - {1, 1, 1, 1, 1, 1, 2, 1, 2}, - {1, 1, 2, 1, 1, 1, 4, 1, 4}, - {1, 1, 1, 1, 1, 1, 2, 1, 1}, - {1, 1, 1, 1, 1, 1, 1, 1, 1}, - {1, 1, 1, 1, 1, 1, 2, 1, 2}, - {1, 1, 1, 1, 1, 1, 2, 1, 2}, - {1, 1, 1, 1, 1, 1, 2, 1, 1}, - {1, 1, 1, 1, 1, 1, 1, 1, 1}, - {1, 1, 1, 1, 1, 1, 2, 1, 2}, - {1, 1, 1, 1, 1, 1, 1, 1, 1}, - {1, 1, 1, 1, 1, 1, 1, 1, 1}, - {1, 1, 1, 1, 1, 1, 1, 1, 1}, - {1, 1, 1, 1, 1, 1, 1, 1, 1}, - {1, 1, 1, 1, 1, 1, 1, 1, 1}, - {1, 1, 1, 1, 1, 1, 1, 1, 1}, - } - - atTokens := 512 - - // create data structures - numAcc := len(aNames) - numModels := len(mNames) - models := config.ModelData{ - PerfData: make([]config.ModelAcceleratorPerfData, numModels*numAcc), - } - k := 0 - for i, n := range mNames { - for j, a := range aNames { - pd := config.ModelAcceleratorPerfData{ - Name: n, - Acc: a, - AccCount: count[j][i], - Alpha: alpha[j][i], - Beta: beta[j][i], - MaxBatchSize: maxBatchSize[j][i], - AtTokens: atTokens, - } - models.PerfData[k] = pd - k++ - } - } - - // generate json - if byteValue, err := json.Marshal(models); err != nil { - fmt.Println(err.Error()) - } else { - fmt.Println(string(byteValue)) - } -} diff --git a/demos/main/main.go b/demos/main/main.go deleted file mode 100644 index 9a4897482..000000000 --- a/demos/main/main.go +++ /dev/null @@ -1,116 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "os" - - "github.com/llm-inferno/inferno/pkg/config" - "github.com/llm-inferno/inferno/pkg/core" - "github.com/llm-inferno/inferno/pkg/manager" - "github.com/llm-inferno/inferno/pkg/solver" - "github.com/llm-inferno/inferno/pkg/utils" -) - -func main() { - size := "large" - if len(os.Args) > 1 { - size = os.Args[1] - } - prefix := "../../sample-data/" + size + "/" - fn_acc := prefix + "accelerator-data.json" - fn_cap := prefix + "capacity-data.json" - fn_mod := prefix + "model-data.json" - fn_svc := prefix + "serviceclass-data.json" - fn_srv := prefix + "server-data.json" - fn_opt := prefix + "optimizer-data.json" - fn_sol := prefix + "solution-data.json" - - system := core.NewSystem() - - bytes_acc, err_acc := os.ReadFile(fn_acc) - if err_acc != nil { - fmt.Println(err_acc) - } - if d, err := utils.FromDataToSpec(bytes_acc, config.AcceleratorData{}); err == nil { - system.SetAcceleratorsFromSpec(d) - } else { - fmt.Println(err) - return - } - - bytes_cap, err_cap := os.ReadFile(fn_cap) - if err_cap != nil { - fmt.Println(err_cap) - } - if d, err := utils.FromDataToSpec(bytes_cap, config.CapacityData{}); err == nil { - system.SetCapacityFromSpec(d) - } else { - fmt.Println(err) - return - } - - bytes_mod, err_mod := os.ReadFile(fn_mod) - if err_mod != nil { - fmt.Println(err_mod) - } - if d, err := utils.FromDataToSpec(bytes_mod, config.ModelData{}); err == nil { - system.SetModelsFromSpec(d) - } else { - fmt.Println(err) - return - } - - bytes_svc, err_svc := os.ReadFile(fn_svc) - if err_svc != nil { - fmt.Println(err_svc) - } - if d, err := utils.FromDataToSpec(bytes_svc, config.ServiceClassData{}); err == nil { - system.SetServiceClassesFromSpec(d) - } else { - fmt.Println(err) - return - } - - bytes_srv, err_srv := os.ReadFile(fn_srv) - if err_srv != nil { - fmt.Println(err_srv) - } - if d, err := utils.FromDataToSpec(bytes_srv, config.ServerData{}); err == nil { - system.SetServersFromSpec(d) - } else { - fmt.Println(err) - return - } - - var optimizer *solver.Optimizer - bytes_opt, err_opt := os.ReadFile(fn_opt) - if err_opt != nil { - fmt.Println(err_acc) - } - if d, err := utils.FromDataToSpec(bytes_opt, config.OptimizerData{}); err == nil { - optimizer = solver.NewOptimizerFromSpec(&d.Spec) - } else { - fmt.Println(err) - return - } - - manager := manager.NewManager(system, optimizer) - - system.Calculate() - if err := manager.Optimize(); err != nil { - fmt.Println(err) - return - } - allocationSolution := system.GenerateSolution() - - // generate json - if byteValue, err := json.Marshal(allocationSolution); err != nil { - fmt.Println(err) - } else { - os.WriteFile(fn_sol, byteValue, 0644) - } - - fmt.Printf("%v", system) - fmt.Printf("%v", optimizer) -} diff --git a/demos/scale/main.go b/demos/scale/main.go deleted file mode 100644 index 39c7351a8..000000000 --- a/demos/scale/main.go +++ /dev/null @@ -1,143 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/llm-inferno/inferno/pkg/config" - "github.com/llm-inferno/inferno/pkg/core" - "github.com/llm-inferno/inferno/pkg/manager" - "github.com/llm-inferno/inferno/pkg/solver" - "github.com/llm-inferno/inferno/pkg/utils" -) - -func main() { - size := "large" - if len(os.Args) > 1 { - size = os.Args[1] - } - prefix := "../../sample-data/" + size + "/" - fn_acc := prefix + "accelerator-data.json" - fn_cap := prefix + "capacity-data.json" - fn_mod := prefix + "model-data.json" - fn_svc := prefix + "serviceclass-data.json" - fn_srv := prefix + "server-data.json" - fn_opt := prefix + "optimizer-data.json" - - system := core.NewSystem() - - bytes_acc, err_acc := os.ReadFile(fn_acc) - if err_acc != nil { - fmt.Println(err_acc) - } - if d, err := utils.FromDataToSpec(bytes_acc, config.AcceleratorData{}); err == nil { - system.SetAcceleratorsFromSpec(d) - } else { - fmt.Println(err) - return - } - - bytes_cap, err_cap := os.ReadFile(fn_cap) - if err_cap != nil { - fmt.Println(err_cap) - } - if d, err := utils.FromDataToSpec(bytes_cap, config.CapacityData{}); err == nil { - system.SetCapacityFromSpec(d) - } else { - fmt.Println(err) - return - } - - bytes_mod, err_mod := os.ReadFile(fn_mod) - if err_mod != nil { - fmt.Println(err_mod) - } - if d, err := utils.FromDataToSpec(bytes_mod, config.ModelData{}); err == nil { - system.SetModelsFromSpec(d) - } else { - fmt.Println(err) - return - } - - bytes_svc, err_svc := os.ReadFile(fn_svc) - if err_svc != nil { - fmt.Println(err_svc) - } - if d, err := utils.FromDataToSpec(bytes_svc, config.ServiceClassData{}); err == nil { - system.SetServiceClassesFromSpec(d) - } else { - fmt.Println(err) - return - } - - bytes_srv, err_srv := os.ReadFile(fn_srv) - if err_srv != nil { - fmt.Println(err_srv) - } - if d, err := utils.FromDataToSpec(bytes_srv, config.ServerData{}); err == nil { - system.SetServersFromSpec(d) - } else { - fmt.Println(err) - return - } - - var optimizer *solver.Optimizer - bytes_opt, err_opt := os.ReadFile(fn_opt) - if err_opt != nil { - fmt.Println(err_acc) - } - if d, err := utils.FromDataToSpec(bytes_opt, config.OptimizerData{}); err == nil { - optimizer = solver.NewOptimizerFromSpec(&d.Spec) - } else { - fmt.Println(err) - return - } - - manager := manager.NewManager(system, optimizer) - - system.Calculate() - if err := manager.Optimize(); err != nil { - fmt.Println(err) - return - } - - serverName := "Premium-llama3_8b" - - server := system.Server(serverName) - if server == nil { - fmt.Printf("No server %s\n", serverName) - return - } - allocBefore := server.Allocation() - if allocBefore == nil { - fmt.Printf("No allocation for server %s \n", serverName) - return - } - // change load on server - load := server.Load() - if load == nil { - fmt.Printf("No model load data for server %s \n", serverName) - return - } - fmt.Println("AllocBefore: ", allocBefore) - newArv := load.ArrivalRate * 2.5 - newLength := int(float32(load.AvgLength) * 1.5) - newLoad := config.ServerLoadSpec{ - ArrivalRate: newArv, - AvgLength: newLength, - ArrivalCOV: load.ArrivalCOV, - ServiceCOV: load.ServiceCOV, - } - server.SetLoad(&newLoad) - - // scale allocation - allocAfter, inc := allocBefore.Scale(serverName) - fmt.Println("AllocAfter: ", allocAfter) - fmt.Println("Inc: ", inc) - - // reallocate - var gName string - allocAfter, gName = allocBefore.ReAllocate(serverName) - fmt.Println("AllocAfter: ", allocAfter) - fmt.Println("gName: ", gName) -} diff --git a/demos/transition/main.go b/demos/transition/main.go deleted file mode 100644 index e414704a9..000000000 --- a/demos/transition/main.go +++ /dev/null @@ -1,151 +0,0 @@ -package main - -import ( - "fmt" - "math" - "math/rand/v2" - "os" - - "github.com/llm-inferno/inferno/pkg/config" - "github.com/llm-inferno/inferno/pkg/core" - "github.com/llm-inferno/inferno/pkg/manager" - "github.com/llm-inferno/inferno/pkg/solver" - "github.com/llm-inferno/inferno/pkg/utils" -) - -func main() { - size := "large" - if len(os.Args) > 1 { - size = os.Args[1] - } - prefix := "../../sample-data/" + size + "/" - fn_acc := prefix + "accelerator-data.json" - fn_cap := prefix + "capacity-data.json" - fn_mod := prefix + "model-data.json" - fn_svc := prefix + "serviceclass-data.json" - fn_srv := prefix + "server-data.json" - fn_opt := prefix + "optimizer-data.json" - - system := core.NewSystem() - - bytes_acc, err_acc := os.ReadFile(fn_acc) - if err_acc != nil { - fmt.Println(err_acc) - } - if d, err := utils.FromDataToSpec(bytes_acc, config.AcceleratorData{}); err == nil { - system.SetAcceleratorsFromSpec(d) - } else { - fmt.Println(err) - return - } - - bytes_cap, err_cap := os.ReadFile(fn_cap) - if err_cap != nil { - fmt.Println(err_cap) - } - if d, err := utils.FromDataToSpec(bytes_cap, config.CapacityData{}); err == nil { - system.SetCapacityFromSpec(d) - } else { - fmt.Println(err) - return - } - - bytes_mod, err_mod := os.ReadFile(fn_mod) - if err_mod != nil { - fmt.Println(err_mod) - } - if d, err := utils.FromDataToSpec(bytes_mod, config.ModelData{}); err == nil { - system.SetModelsFromSpec(d) - } else { - fmt.Println(err) - return - } - - bytes_svc, err_svc := os.ReadFile(fn_svc) - if err_svc != nil { - fmt.Println(err_svc) - } - if d, err := utils.FromDataToSpec(bytes_svc, config.ServiceClassData{}); err == nil { - system.SetServiceClassesFromSpec(d) - } else { - fmt.Println(err) - return - } - - bytes_srv, err_srv := os.ReadFile(fn_srv) - if err_srv != nil { - fmt.Println(err_srv) - } - if d, err := utils.FromDataToSpec(bytes_srv, config.ServerData{}); err == nil { - system.SetServersFromSpec(d) - } else { - fmt.Println(err) - return - } - - var optimizer *solver.Optimizer - bytes_opt, err_opt := os.ReadFile(fn_opt) - if err_opt != nil { - fmt.Println(err_acc) - } - if d, err := utils.FromDataToSpec(bytes_opt, config.OptimizerData{}); err == nil { - optimizer = solver.NewOptimizerFromSpec(&d.Spec) - } else { - fmt.Println(err) - return - } - - manager := manager.NewManager(system, optimizer) - - system.Calculate() - if err := manager.Optimize(); err != nil { - fmt.Println(err) - return - } - - fmt.Printf("%v", system) - fmt.Printf("%v", optimizer) - - // generate random values in [alpha, 2 - alpha), where 0 < alpha < 1 - alpha := float32(0.1) - - for _, server := range system.Servers() { - load := server.Load() - if load == nil { - continue - } - - factorA := 2 * (rand.Float32() - 0.5) * (1 - alpha) - newArv := load.ArrivalRate * (1 + factorA) - if newArv <= 0 { - newArv = 1 - } - - factorB := 2 * (rand.Float32() - 0.5) * (1 - alpha) - newLength := int(math.Ceil(float64(float32(load.AvgLength) * (1 + factorB)))) - if newLength <= 0 { - newLength = 1 - } - newLoad := config.ServerLoadSpec{ - ArrivalRate: newArv, - AvgLength: newLength, - ArrivalCOV: load.ArrivalCOV, - ServiceCOV: load.ServiceCOV, - } - server.SetLoad(&newLoad) - if curAllocation := server.CurAllocation(); curAllocation != nil { - server.SetCurAllocation(server.Allocation().Clone()) - } - - // fmt.Printf("s=%s, rate=%v, tokens=%d \n", - // server.Name(), load.ArrivalRate, load.AvgLength) - } - - system.Calculate() - if err := manager.Optimize(); err != nil { - fmt.Println(err) - return - } - fmt.Printf("%v", system) - fmt.Printf("%v", optimizer) -} diff --git a/docs/arch/high-level.png b/docs/arch/high-level.png deleted file mode 100644 index a48690fdb60db3e2abe6a8178d1f36550e95a84d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44679 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 Ju9i#O{{dDXrMLh9 diff --git a/docs/arch/runtime.png b/docs/arch/runtime.png deleted file mode 100644 index 9765234e8f027ea9b766982827018cef531a1b92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72423 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