Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions api/worker/v1alpha1/generated.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions api/worker/v1alpha1/generated.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions api/worker/v1alpha1/model_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,14 @@ type ModelDeploymentRole struct {
// +listType=atomic
AdditionalVolumes []ModelDeploymentAdditionalVolume `json:"additionalVolumes,omitempty" protobuf:"bytes,13,rep,name=additionalVolumes"` // nolint: lll

// ShmSize limits the memory-backed /dev/shm volume in each role Pod. It must be positive.
// Omission renders 16Gi. An explicit /dev/shm mount in AdditionalVolumes takes precedence.
// Used shared memory counts toward the container's memory limit; this is not a reservation.
// Existing elastic members keep their mounts; new and replacement Pods use the current value.
//
// +optional
ShmSize *resource.Quantity `json:"shmSize,omitempty" protobuf:"bytes,19,opt,name=shmSize"`

// Command replaces the whole argv, which is the TAKE-OVER tier: the user owns the whole
// command line, the operator synthesizes no engine argument and no client environment, the
// role is marked unmanaged and CacheAttached goes to Unknown. Arguments fold into Command;
Expand Down
14 changes: 14 additions & 0 deletions api/worker/v1alpha1/zz_generated.crds.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions api/worker/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion api/worker/zz_generated.openapi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 24 additions & 3 deletions docs/modules/model-deployment/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ Without one, users patch the rendered Pod and the reconcile loop silently overwr
| Tier | Field | Semantics |
|---|---|---|
| append | `roles[].extraArgs`, `roles[].env` | appended **after** the operator-synthesized arguments; a key the operator owns is refused, never merged |
| overlay | the role's own Pod fields — `image`, `imagePullPolicy`, `imagePullSecrets`, `privileged`, `ports`, `additionalVolumes`, `terminationGracePeriodSeconds` | the operator renders first, then merges this overlay on top |
| overlay | the role's own Pod fields — `image`, `imagePullPolicy`, `imagePullSecrets`, `privileged`, `ports`, `additionalVolumes`, `shmSize`, `terminationGracePeriodSeconds` | the operator renders first, then merges this overlay on top |
| take over | `roles[].command` | the user owns the whole argv; the operator synthesizes **no** engine argument and **no** client environment |

A role's Pod fields sit on the role itself, unlike the `Instance` that keeps its pod shape inside an
Expand Down Expand Up @@ -296,6 +296,27 @@ That is one instance of a wider exposure, not its boundary. The boundary is stat
[#168](https://github.com/gpustack/gpustack-operator/issues/168), whose own void conditions include a
webhook-level one, so nothing here should be read as a claim about how that issue can be closed.

### Shared memory

`roles[].shmSize` sets the capacity limit of `/dev/shm` in each role Pod. It accepts a positive Kubernetes
quantity, such as `32Gi`. Omission renders a `16Gi` memory-backed `EmptyDir`.
This applies to both engines, take-over roles, and the Elastic Ray head and GPU members.
Each Pod has its own volume. Only the main container mounts it.

An explicit `/dev/shm` mount in `additionalVolumes` takes precedence, including when `shmSize` is set.
The operator preserves that mount's capacity and backing.
The default is a project capacity choice, not a universal engine requirement.

The capacity limit does not reserve RAM or increase the Pod's memory request.
Used shared memory counts toward the main container's memory limit.
Budget memory for both shared memory and the engine heap.
Ray's object store may need more than `16Gi`; size the volume for the configured workload.
Kubelet lowers the mount size when the Pod memory limit or node allocatable memory is smaller.

Changing the rendered capacity follows ordinary role rollout behavior.
Existing Elastic members keep their mounts; new or replacement Pods use the current value.
Upgrading the operator adds the default mount to ordinary roles and triggers their recreate rollout.

## Operator-owned keys

Ownership is per (engine, key): a key one engine owns is an ordinary user argument on another.
Expand Down Expand Up @@ -433,7 +454,7 @@ appearing as an unattributable `ImagePullBackOff`.
Changing `replicas` adds or removes instances and nothing more: the survivors are not restarted, do
not reload their weights and keep their cached blocks. What still replaces **every** instance of the
role is an edit that changes what a replica's Pod renders: `image`, `extraArgs`, `env`, `ports`,
`additionalVolumes`, `terminationGracePeriodSeconds`. A change to `size` is not on that list because
`additionalVolumes`, `shmSize`, `terminationGracePeriodSeconds`. A change to `size` is not on that list because
it cannot be made: see `roles[].size` above.

Such an edit **deletes and recreates** the role's replicas, one replica per role per pass, waited
Expand Down Expand Up @@ -521,7 +542,7 @@ editable; the table below lists both groups and the scheduling exceptions.
| the set of roles, and each role's `name` and `kind` | `roles[].replicas` |
| `roles[].size` | |
| `roles[].instanceType` | `roles[].extraArgs`, `roles[].env` |
| `roles[].resources` | the role's own Pod fields — `image`, `imagePullPolicy`, `imagePullSecrets`, `privileged`, `ports`, `additionalVolumes`, `terminationGracePeriodSeconds` |
| `roles[].resources` | the role's own Pod fields — `image`, `imagePullPolicy`, `imagePullSecrets`, `privileged`, `ports`, `additionalVolumes`, `shmSize`, `terminationGracePeriodSeconds` |
| `roles[].command` | labels and annotations |

`roles[].resources` is frozen because changing it renegotiates scheduling, much like deleting and
Expand Down
13 changes: 5 additions & 8 deletions docs/modules/model-deployment/elastic-ep.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The engine reconfigures while Kubernetes, Kueue, Ray and accelerator accounting

- [What the profile means](#what-the-profile-means)
- [Requirements](#requirements)
- [Shared memory for GPU members](#shared-memory-for-gpu-members)
- [Shared memory](#shared-memory)
- [A minimal manifest](#a-minimal-manifest)
- [Changing the width](#changing-the-width)
- [What you see while it runs](#what-you-see-while-it-runs)
Expand Down Expand Up @@ -70,14 +70,11 @@ The profile increases the number of GPU members. It does not resize the underlyi
cloud node elasticity, and it does not patch anything upstream in the engine. It drives the engine
through the resize interface the engine already exposes.

## Shared memory for GPU members
## Shared memory

Each Elastic GPU member gets a separate memory-backed `EmptyDir` mounted at `/dev/shm`.
Its size limit is 512 MiB. This includes the master and workers. The CPU head has no default mount.
The volume is isolated per Pod and uses the Pod's memory allocation.

An explicit `/dev/shm` mount takes precedence. Its capacity and backing remain the user's responsibility.
Sufficiency at 512 MiB is not established. A workload that needs more must declare its own `/dev/shm` mount.
The CPU head, master and GPU workers each get a separate memory-backed `EmptyDir` at `/dev/shm`.
Changing the role's `shmSize` affects new or replacement members; existing members keep their mounts.
See [Shared memory](deployment.md#shared-memory) for configuration, precedence and memory budgeting.

## A minimal manifest

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (

core "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
Expand All @@ -19,12 +18,6 @@ import (
"gpustack.ai/gpustack/pkg/systemmeta"
)

const (
modelDeploymentElasticSharedMemoryPath = "/dev/shm"
modelDeploymentElasticSharedMemoryVolume = "elastic-shm"
modelDeploymentElasticSharedMemorySize = "512Mi"
)

// ModelDeploymentElasticTensorParallelSize reads the fixed TP degree from extraArgs.
// Non-elastic roles retain their ordinary accelerator default of one.
func ModelDeploymentElasticTensorParallelSize(role *workercore.ModelDeploymentRole) (int, error) {
Expand Down Expand Up @@ -138,7 +131,6 @@ func (r *ModelDeploymentReconciler) renderModelDeploymentElasticPods(ctx context
c.LivenessProbe = nil
c.Lifecycle = nil
}
modelDeploymentElasticSharedMemory(pod)
pod.Annotations[modelDeploymentPodSpecHashAnnotation] = modelDeploymentPodSpecHash(pod)
}
head := gpu.DeepCopy()
Expand Down Expand Up @@ -176,32 +168,6 @@ func (r *ModelDeploymentReconciler) renderModelDeploymentElasticPods(ctx context
return desired, nil
}

// modelDeploymentElasticSharedMemory gives each GPU member an isolated shared-memory volume.
// Preserve an explicit mount, whose backing and capacity remain the user's responsibility.
func modelDeploymentElasticSharedMemory(pod *core.Pod) {
if len(pod.Spec.Containers) == 0 {
return
}
container := &pod.Spec.Containers[0]
for _, m := range container.VolumeMounts {
if m.MountPath == modelDeploymentElasticSharedMemoryPath {
return
}
}
size := resource.MustParse(modelDeploymentElasticSharedMemorySize)
pod.Spec.Volumes = append(pod.Spec.Volumes, core.Volume{
Name: modelDeploymentElasticSharedMemoryVolume,
VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{
Medium: core.StorageMediumMemory,
SizeLimit: &size,
}},
})
container.VolumeMounts = append(container.VolumeMounts, core.VolumeMount{
Name: modelDeploymentElasticSharedMemoryVolume,
MountPath: modelDeploymentElasticSharedMemoryPath,
})
}

func renderModelDeploymentElasticHeadService(md *workercore.ModelDeployment) *core.Service {
name := modelDeploymentElasticHeadName(md)
svc := &core.Service{
Expand Down
Loading
Loading