Skip to content

Commit eb30c30

Browse files
ZhiXiao-LinRoy Lin
andauthored
feat(ksm): typed config — BoxConfig.ksm / InstanceSpec.ksm / pool start --ksm (#25)
KSM page-merging is now a first-class config field instead of an inherited env: BoxConfig.ksm → InstanceSpec.ksm → the controller sets A3S_BOX_KSM=1 on the shim (which does the prctl opt-in). `pool start --ksm` enables it for pooled same-image VMs — the highest-value case (measured 3.2× dedup). The A3S_BOX_KSM env still works as an override. Co-authored-by: Roy Lin <roylin@a3s.box>
1 parent 8f341bb commit eb30c30

5 files changed

Lines changed: 36 additions & 0 deletions

File tree

src/cli/src/commands/pool.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ pub struct PoolStartArgs {
7878
#[arg(long)]
7979
pub deferred: bool,
8080

81+
/// Mark pooled VM memory KSM-mergeable so the host dedups identical pages
82+
/// across same-image VMs (Linux 6.4+; needs /sys/kernel/mm/ksm/run=1).
83+
#[arg(long)]
84+
pub ksm: bool,
85+
8186
/// Output as JSON
8287
#[arg(long)]
8388
pub json: bool,
@@ -265,6 +270,8 @@ struct PoolRegistry {
265270
/// When true, pooled VMs boot IDLE and `pool run` spawns the command as the
266271
/// box's real MAIN (full box semantics), instead of exec-into-keepalive.
267272
deferred: bool,
273+
/// Mark pooled VM memory KSM-mergeable (host page dedup across same-image VMs).
274+
ksm: bool,
268275
}
269276

270277
impl PoolRegistry {
@@ -292,6 +299,7 @@ impl PoolRegistry {
292299
cmd: keepalive_cmd(),
293300
pool: pool_config.clone(),
294301
deferred_main: self.deferred,
302+
ksm: self.ksm,
295303
..Default::default()
296304
};
297305
let pool = std::sync::Arc::new(
@@ -360,6 +368,7 @@ async fn execute_start(args: PoolStartArgs) -> Result<(), Box<dyn std::error::Er
360368
max: args.max,
361369
ttl: args.ttl,
362370
deferred: args.deferred,
371+
ksm: args.ksm,
363372
});
364373

365374
// Pre-warm the default image, if one was given.
@@ -892,6 +901,7 @@ mod tests {
892901
socket: DEFAULT_SOCKET.to_string(),
893902
warm: vec![],
894903
deferred: false,
904+
ksm: false,
895905
json: false,
896906
};
897907
let result = execute_start(args).await;
@@ -909,6 +919,7 @@ mod tests {
909919
socket: DEFAULT_SOCKET.to_string(),
910920
warm: vec![],
911921
deferred: false,
922+
ksm: false,
912923
json: false,
913924
};
914925
let result = execute_start(args).await;

src/core/src/config.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,12 @@ pub struct BoxConfig {
317317
#[serde(default)]
318318
pub deferred_main: bool,
319319

320+
/// Mark guest memory KSM-mergeable so the host kernel dedups identical pages
321+
/// across same-image VMs (Linux 6.4+; needs /sys/kernel/mm/ksm/run=1 on the
322+
/// host). Most valuable for pools of same-image sandboxes.
323+
#[serde(default)]
324+
pub ksm: bool,
325+
320326
/// Port mappings: "host_port:guest_port" (e.g., "8080:80")
321327
/// Maps host ports to guest ports via TSI (Transparent Socket Impersonation).
322328
#[serde(default)]
@@ -415,6 +421,7 @@ impl Default for BoxConfig {
415421
cache: CacheConfig::default(),
416422
pool: PoolConfig::default(),
417423
deferred_main: false,
424+
ksm: false,
418425
port_map: vec![],
419426
dns: vec![],
420427
add_hosts: vec![],

src/core/src/vmm.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ pub struct InstanceSpec {
125125
/// Guest agent entrypoint
126126
pub entrypoint: Entrypoint,
127127

128+
/// Mark guest memory KSM-mergeable (host page dedup across same-image VMs;
129+
/// Linux 6.4+, requires /sys/kernel/mm/ksm/run=1 on the host).
130+
#[serde(default)]
131+
pub ksm: bool,
132+
128133
/// Optional console output file path
129134
pub console_output: Option<PathBuf>,
130135

@@ -175,6 +180,7 @@ impl Default for InstanceSpec {
175180
args: Vec::new(),
176181
env: Vec::new(),
177182
},
183+
ksm: false,
178184
console_output: None,
179185
workdir: "/".to_string(),
180186
tee_config: None,
@@ -395,6 +401,7 @@ mod tests {
395401
fn test_instance_spec_serde_roundtrip() {
396402
let spec = InstanceSpec {
397403
box_id: "test-box-123".to_string(),
404+
ksm: false,
398405
vcpus: 4,
399406
memory_mib: 2048,
400407
rootfs_path: PathBuf::from("/tmp/rootfs"),

src/runtime/src/vm/spec.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,11 @@ impl VmManager {
344344
network: None, // Network config is set by CLI when --network is specified
345345
resource_limits: self.config.resource_limits.clone(),
346346
log_config: self.log_config.clone(),
347+
// KSM page-merging: config field, or the A3S_BOX_KSM env override.
348+
ksm: self.config.ksm
349+
|| std::env::var("A3S_BOX_KSM")
350+
.map(|v| matches!(v.as_str(), "1" | "true" | "yes" | "on"))
351+
.unwrap_or(false),
347352
})
348353
}
349354

src/runtime/src/vmm/controller.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,12 @@ impl VmmProvider for VmController {
413413
cmd.arg("--config").arg(&config_json).stdin(Stdio::null());
414414
self.configure_shim_stdio(&mut cmd, spec);
415415

416+
// KSM page-merging: the shim opts its (guest) memory in via prctl when this
417+
// env is set; driven by InstanceSpec.ksm (BoxConfig.ksm or A3S_BOX_KSM).
418+
if spec.ksm {
419+
cmd.env("A3S_BOX_KSM", "1");
420+
}
421+
416422
// On macOS, set DYLD_LIBRARY_PATH to help find libkrunfw
417423
#[cfg(target_os = "macos")]
418424
{

0 commit comments

Comments
 (0)