Skip to content

Commit da994a6

Browse files
committed
Merge branch 'main' into gpu-cr-01-baseline-tests
Signed-off-by: Edwinhr716 <edandres249@gmail.com>
2 parents 556a174 + 9e238e8 commit da994a6

8 files changed

Lines changed: 622 additions & 20 deletions

File tree

cmd/snapshot-agent/main.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,11 @@ func main() {
7979
// server's WorkloadChannel RPC handler.
8080
channelRegistry := backends.NewChannelRegistry()
8181
registeredBackends := map[backends.BackendType]backends.Backend{
82-
backends.BackendCuda: backends.NewCudaCheckpoint(),
83-
backends.BackendNoop: backends.NewNoopBackend(),
84-
backends.BackendAppEndpoint: backends.NewAppEndpointBackend(),
85-
backends.BackendAppChannel: backends.NewAppChannelBackend(channelRegistry),
82+
backends.BackendCuda: backends.NewCudaCheckpoint(),
83+
backends.BackendNoop: backends.NewNoopBackend(),
84+
backends.BackendAppEndpoint: backends.NewAppEndpointBackend(),
85+
backends.BackendAppChannel: backends.NewAppChannelBackend(channelRegistry),
86+
backends.BackendDirectMemory: backends.NewDirectMemory(),
8687
}
8788

8889
slog.InfoContext(ctx, "Starting Snapshot Agent",

pkg/snapshot-agent/backends/checkpoint.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const (
2323
// BackendMemoryRegions selectively checkpoints/restores explicit device
2424
// memory regions of a running process via the GPU-CR cr_client.
2525
BackendMemoryRegions BackendType = "memory-regions"
26+
// BackendDirectMemory is the Direct Memory (process-level) checkpointing backend.
27+
BackendDirectMemory BackendType = "direct-memory"
2628
)
2729

2830
// Request carries one backend invocation: the job it targets and the
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package backends
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
"os"
8+
"os/exec"
9+
"strconv"
10+
"sync"
11+
"time"
12+
13+
pb "github.com/llm-d-incubation/llm-d-rl-time-slicing/pkg/snapshot-agent/api/v1alpha1"
14+
)
15+
16+
// crClientPath is the only location cr_client is expected at: the
17+
// snapshot-agent image installs it there, so anything else is a broken
18+
// deployment and should fail loudly rather than resolve to a dangling path.
19+
const crClientPath = "/usr/local/bin/cr_client"
20+
21+
// DirectMemory implements the Backend interface using cr_client.
22+
type DirectMemory struct {
23+
mu sync.Mutex
24+
execCommand func(ctx context.Context, name string, args ...string) ([]byte, error)
25+
statFunc func(string) (os.FileInfo, error)
26+
}
27+
28+
// NewDirectMemory creates a new DirectMemory backend.
29+
func NewDirectMemory() *DirectMemory {
30+
return &DirectMemory{
31+
execCommand: func(ctx context.Context, name string, args ...string) ([]byte, error) {
32+
return exec.CommandContext(ctx, name, args...).CombinedOutput()
33+
},
34+
statFunc: os.Stat,
35+
}
36+
}
37+
38+
// Snapshot triggers a snapshot of the target processes for a job using cr_client.
39+
func (d *DirectMemory) Snapshot(ctx context.Context, req Request) error {
40+
pids := ExtractDirectMemoryPIDStrings(req.Config)
41+
if len(pids) == 0 {
42+
return fmt.Errorf("at least one PID is required for Direct Memory snapshot")
43+
}
44+
45+
d.mu.Lock()
46+
defer d.mu.Unlock()
47+
48+
slog.InfoContext(ctx, "Snapshotting PIDs using Direct Memory", "pids", pids)
49+
50+
t0 := time.Now()
51+
for _, pid := range pids {
52+
if err := d.checkpointPID(ctx, pid); err != nil {
53+
return fmt.Errorf("cr_client checkpoint failed for PID %s: %w", pid, err)
54+
}
55+
}
56+
slog.InfoContext(ctx, "cr_client checkpoint took", "duration", time.Since(t0))
57+
return nil
58+
}
59+
60+
// Restore triggers a restoration of the target processes for a job using cr_client.
61+
func (d *DirectMemory) Restore(ctx context.Context, req Request) error {
62+
pids := ExtractDirectMemoryPIDStrings(req.Config)
63+
if len(pids) == 0 {
64+
return fmt.Errorf("at least one PID is required for Direct Memory restore")
65+
}
66+
67+
d.mu.Lock()
68+
defer d.mu.Unlock()
69+
70+
slog.InfoContext(ctx, "Restoring PIDs using Direct Memory", "pids", pids)
71+
t0 := time.Now()
72+
for _, pid := range pids {
73+
if err := d.restorePID(ctx, pid); err != nil {
74+
return fmt.Errorf("cr_client restore failed for PID %s: %w", pid, err)
75+
}
76+
}
77+
slog.InfoContext(ctx, "cr_client restore took", "duration", time.Since(t0), "pids", pids)
78+
return nil
79+
}
80+
81+
func (d *DirectMemory) getCrClientPath() (string, error) {
82+
if _, err := d.statFunc(crClientPath); err != nil {
83+
return "", fmt.Errorf("cr_client not found at %s: %w", crClientPath, err)
84+
}
85+
return crClientPath, nil
86+
}
87+
88+
func (d *DirectMemory) runCommand(ctx context.Context, name string, args ...string) error {
89+
// A workload that dies mid-operation can leave cr_client blocked on its
90+
// shared-memory control channel forever; without a deadline that wedges
91+
// the job in TRANSITIONING and holds d.mu across all future requests.
92+
ctx, cancel := context.WithTimeout(ctx, directMemoryOpTimeout())
93+
defer cancel()
94+
if out, err := d.execCommand(ctx, name, args...); err != nil {
95+
return fmt.Errorf("command failed: %w, output: %s", err, string(out))
96+
}
97+
return nil
98+
}
99+
100+
// directMemoryOpTimeout is the per-cr_client-invocation deadline,
101+
// configurable via DIRECT_MEMORY_OP_TIMEOUT_SEC (default 120).
102+
func directMemoryOpTimeout() time.Duration {
103+
if v := os.Getenv("DIRECT_MEMORY_OP_TIMEOUT_SEC"); v != "" {
104+
if n, err := strconv.Atoi(v); err == nil && n > 0 {
105+
return time.Duration(n) * time.Second
106+
}
107+
}
108+
return 120 * time.Second
109+
}
110+
111+
func (d *DirectMemory) checkpointPID(ctx context.Context, pid string) error {
112+
binaryPath, err := d.getCrClientPath()
113+
if err != nil {
114+
return err
115+
}
116+
return d.runCommand(ctx, binaryPath, "-c", "-p", pid)
117+
}
118+
119+
func (d *DirectMemory) restorePID(ctx context.Context, pid string) error {
120+
binaryPath, err := d.getCrClientPath()
121+
if err != nil {
122+
return err
123+
}
124+
return d.runCommand(ctx, binaryPath, "-r", "-p", pid)
125+
}
126+
127+
// HealthCheck checks if the Direct Memory backend is healthy.
128+
func (d *DirectMemory) HealthCheck(ctx context.Context) error {
129+
_, err := d.getCrClientPath()
130+
return err
131+
}
132+
133+
// ExtractDirectMemoryPIDStrings extracts PID strings from a DirectMemory BackendConfig.
134+
func ExtractDirectMemoryPIDStrings(config *pb.BackendConfig) []string {
135+
if config == nil {
136+
return nil
137+
}
138+
dm := config.GetDirectMemory()
139+
if dm == nil {
140+
return nil
141+
}
142+
target := dm.GetExplicitTarget()
143+
if target == nil {
144+
return nil
145+
}
146+
pids := make([]string, 0, len(target.GetPids()))
147+
for _, pid := range target.GetPids() {
148+
pids = append(pids, strconv.Itoa(int(pid)))
149+
}
150+
return pids
151+
}
152+
153+
// BuildDirectMemoryConfig wraps PID strings into a DirectMemory BackendConfig.
154+
func BuildDirectMemoryConfig(pidStrings []string) (*pb.BackendConfig, error) {
155+
pids := make([]int32, 0, len(pidStrings))
156+
for _, s := range pidStrings {
157+
pid, err := strconv.ParseInt(s, 10, 32)
158+
if err != nil {
159+
return nil, fmt.Errorf("invalid PID string %q: %w", s, err)
160+
}
161+
pids = append(pids, int32(pid))
162+
}
163+
return &pb.BackendConfig{
164+
Backend: &pb.BackendConfig_DirectMemory{
165+
DirectMemory: &pb.DirectMemoryBackendConfig{
166+
ExplicitTarget: &pb.ProcessTarget{Pids: pids},
167+
},
168+
},
169+
}, nil
170+
}

0 commit comments

Comments
 (0)