Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ jobs:
- 'rust-toolchain*'
# clippy runs `cargo xtask manifest --check` against this file.
- 'MANIFEST.md'
# rocm-deps compiles the pins into constants, so a pin change is a
# source change even though no *.rs file moved.
- 'runtime-deps.toml'
- '.github/workflows/**'
# build-and-test runs cargo AND the python/shell smoke steps plus the
# install-lifecycle E2E (`cargo xtask package` + the real installer),
Expand All @@ -111,6 +114,7 @@ jobs:
- '**/Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain*'
- 'runtime-deps.toml'
- 'scripts/**'
- 'xtask/**'
- 'engines/**'
Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ members = [
"apps/rocm",
"apps/rocmd",
"crates/rocm-core",
# Pinned third-party runtime versions, generated from `runtime-deps.toml`.
"crates/rocm-deps",
"crates/rocm-engine-protocol",
# rocm-dash telemetry/dashboard libraries (rocm-dash merge).
"crates/rocm-dash-core",
Expand Down
1 change: 1 addition & 0 deletions apps/rocm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ crossterm.workspace = true
flate2 = "1.1"
keyring-core.workspace = true
rocm-core = { path = "../../crates/rocm-core" }
rocm-deps = { path = "../../crates/rocm-deps" }
# rocm-dash unified dashboard launch. The
# telemetry daemon + ratatui-0.30 TUI are launched from the `dash` verb; tokio
# drives the async daemon/TUI from the otherwise-sync `rocm` binary.
Expand Down
25 changes: 18 additions & 7 deletions apps/rocm/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3408,7 +3408,7 @@ fn resolve_engine_install_runtime_id(
runtime_id: Option<String>,
) -> Result<String> {
if engine_manages_own_runtime(engine) {
return Ok(runtime_id.unwrap_or_else(|| managed_engine_runtime_id(engine).to_owned()));
return Ok(runtime_id.unwrap_or_else(|| managed_engine_runtime_id(engine)));
}
let Some(selector) = runtime_id
.or_else(|| config.active_runtime_key.clone())
Expand Down Expand Up @@ -3512,10 +3512,15 @@ fn env_root_for_service(
}
}

fn managed_engine_runtime_id(engine: &str) -> &'static str {
/// Label recorded for the runtime a self-managing engine installs for itself.
///
/// For `lemonade` this must be the `env_id` its adapter reports, which is
/// derived from the single Lemonade pin — it was previously a hand-written
/// literal and had drifted several minor versions behind what is installed.
fn managed_engine_runtime_id(engine: &str) -> String {
match engine {
"lemonade" => "lemonade-embeddable-10.6.0",
_ => "managed-engine-runtime",
"lemonade" => format!("lemonade-embeddable-{}", rocm_deps::lemonade_version()),
_ => "managed-engine-runtime".to_owned(),
}
}

Expand All @@ -3527,7 +3532,7 @@ fn ensure_self_managed_engine_ready(
if !engine_manages_own_runtime(engine) {
return Ok(());
}
let runtime_id = managed_engine_runtime_id(engine).to_owned();
let runtime_id = managed_engine_runtime_id(engine);
let env_root = env_root_for_self_managed_engine(paths, config)?;
let detect = engine_request::<_, DetectResponse>(
Some(paths),
Expand All @@ -3539,8 +3544,14 @@ fn ensure_self_managed_engine_ready(
},
)
.ok();
// For a self-managing engine the runtime id *is* the env id its adapter
// reports for the pinned version, so a version bump leaves an older
// install detected-but-not-current. Requiring the ids to match makes the
// bump trigger an install instead of silently keeping the old runtime.
let installed = detect.as_ref().is_some_and(|detect| {
detect.installed && detect_runtime_matches_env_root(detect, env_root.as_deref())
detect.installed
&& detect.env_id.as_deref() == Some(runtime_id.as_str())
&& detect_runtime_matches_env_root(detect, env_root.as_deref())
});
let response = if installed {
None
Expand Down Expand Up @@ -21765,7 +21776,7 @@ ID_LIKE="suse opensuse"
assert!(error.contains("no active ROCm runtime is configured"));
assert_eq!(
resolve_engine_install_runtime_id(&paths, &RocmCliConfig::default(), "lemonade", None)?,
"lemonade-embeddable-10.6.0"
format!("lemonade-embeddable-{}", rocm_deps::lemonade_version()),
);
write_test_pip_runtime(
&paths,
Expand Down
4 changes: 2 additions & 2 deletions crates/e2e-report/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2409,7 +2409,7 @@ mod tests {
let platform = r#"{
"platform_slug": "mi300x",
"capability": {"effective_serve_engine": "vllm"},
"versions": {"os":"Ubuntu 24.04.3 LTS","rocm":"7.13.0","vllm":"0.23.0+rocm723","lemonade":"10.6.0"},
"versions": {"os":"Ubuntu 24.04.3 LTS","rocm":"7.13.0","vllm":"0.23.0+rocm723","lemonade":"11.5.1"},
"expectations": [
{"id":"serve-x","effective_engine":"vllm","expected":"pass"}
]
Expand All @@ -2421,7 +2421,7 @@ mod tests {
"Ubuntu 24.04.3 LTS",
"ROCm 7.13.0",
"vLLM 0.23.0+rocm723",
"lemonade 10.6.0",
"lemonade 11.5.1",
] {
assert!(md.contains(token), "matrix cell missing {token:?}:\n{md}");
}
Expand Down
1 change: 1 addition & 0 deletions crates/rocm-dash-tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ path = "src/lib.rs"

[dependencies]
rocm-dash-core = { path = "../rocm-dash-core" }
rocm-deps = { path = "../rocm-deps" }
# Unified dashboard TUI base: ratatui 0.30 + crossterm 0.28. This is the only
# ratatui in the workspace — the `rocm` binary no longer carries a (dead) 0.29
# dependency, so no second major is pulled in. Exact wrapped-row counts must
Expand Down
55 changes: 40 additions & 15 deletions crates/rocm-dash-tui/src/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,28 +254,38 @@ pub fn auto_config_change(detected_endpoint: Option<&str>) -> Option<ConfigChang
// ---------------------------------------------------------------------------

/// GitHub repo that publishes the Lemonade embeddable archives.
pub const LEMONADE_GITHUB_REPO: &str = "lemonade-sdk/lemonade";
/// Pinned embeddable version used as the offline fallback when the GitHub
/// releases API is unreachable. Bump deliberately. (Latest at authoring time.)
pub const LEMONADE_EMBEDDABLE_FALLBACK_VERSION: &str = "10.6.0";
pub use rocm_deps::LEMONADE_GITHUB_REPO;

/// Embeddable version used as the offline fallback when the GitHub releases API
/// is unreachable.
///
/// This is the same pin the `lemonade` engine adapter installs, resolved from
/// `runtime-deps.toml` (and the `ROCM_CLI_LEMONADE_VERSION` override) rather
/// than restated here — the two used to be separate constants and silently
/// drifted five minor versions apart.
#[must_use]
pub fn lemonade_embeddable_fallback_version() -> String {
rocm_deps::lemonade_version()
}

/// A selected embeddable archive for a host triple — enough to download, extract,
/// and locate the server binary. Pure data; no I/O.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmbeddableArtifact {
/// Version without a leading `v`, e.g. `10.6.0`.
/// Version without a leading `v`, e.g. `11.5.1`.
pub version: String,
/// The archive's `browser_download_url`.
pub url: String,
/// The archive file name, e.g. `lemonade-embeddable-10.6.0-ubuntu-x64.tar.gz`.
/// The archive file name, e.g. `lemonade-embeddable-11.5.1-ubuntu-x64.tar.gz`.
pub archive_name: String,
/// The unpacked server executable name (`lemond` / `lemond.exe`).
pub server_bin: String,
}

/// Map a host (os, arch) — `std::env::consts::{OS, ARCH}` values — to the
/// embeddable asset's `<os-arch>` token + archive extension. `None` for an
/// unsupported triple (the release ships only these three).
/// unsupported triple (only these three are supported here; the release also
/// ships `ubuntu-arm64`, which we do not select).
fn embeddable_os_arch(os: &str, arch: &str) -> Option<(&'static str, &'static str)> {
match (os, arch) {
("linux", "x86_64") => Some(("ubuntu-x64", "tar.gz")),
Expand All @@ -294,20 +304,18 @@ fn server_bin_for(os: &str) -> &'static str {
}
}

/// Strip a leading `v` from a release tag (`v10.6.0` → `10.6.0`).
/// Strip a leading `v` from a release tag (`v11.5.1` → `11.5.1`).
fn strip_v(tag: &str) -> &str {
tag.strip_prefix('v').unwrap_or(tag)
rocm_deps::strip_v(tag)
}

/// PURE: build the canonical embeddable artifact for `(os, arch, version)` with no
/// network — the offline-fallback path. `None` for an unsupported triple.
pub fn embeddable_artifact(os: &str, arch: &str, version: &str) -> Option<EmbeddableArtifact> {
let (os_arch, ext) = embeddable_os_arch(os, arch)?;
let ver = strip_v(version);
let archive_name = format!("lemonade-embeddable-{ver}-{os_arch}.{ext}");
let url = format!(
"https://github.com/{LEMONADE_GITHUB_REPO}/releases/download/v{ver}/{archive_name}"
);
let archive_name = rocm_deps::lemonade_archive_name(ver, os_arch, ext);
let url = rocm_deps::lemonade_download_url(ver, &archive_name);
Some(EmbeddableArtifact {
version: ver.to_string(),
url,
Expand Down Expand Up @@ -506,9 +514,26 @@ mod tests {
assert_eq!(b.server_bin, "lemond.exe");
// Unsupported triple → None.
assert!(embeddable_artifact("linux", "aarch64", "10.6.0").is_none());
// The fallback const resolves for the common host.
}

/// The offline fallback and the version the `lemonade` engine adapter
/// installs must never disagree. They cannot: both come from the single
/// `runtime-deps.toml` pin, and this pins that down against a regression
/// that reintroduces a second constant.
#[test]
fn fallback_version_is_the_single_pinned_version() {
let fallback = lemonade_embeddable_fallback_version();
assert_eq!(fallback, rocm_deps::lemonade_version());
let artifact = embeddable_artifact("linux", "x86_64", &fallback).expect("linux");
assert_eq!(artifact.version, fallback);
assert_eq!(
artifact.archive_name,
rocm_deps::lemonade_archive_name(&fallback, "ubuntu-x64", "tar.gz")
);
assert!(
embeddable_artifact("linux", "x86_64", LEMONADE_EMBEDDABLE_FALLBACK_VERSION).is_some()
artifact.url.contains(&format!("/download/v{fallback}/")),
"url {} does not carry the pinned version",
artifact.url
);
}

Expand Down
17 changes: 17 additions & 0 deletions crates/rocm-deps/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "rocm-deps"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
publish.workspace = true
# Turns the workspace-root `runtime-deps.toml` pins into constants. Declared
# explicitly so the file is not mistaken for a stray script.
build = "build.rs"

[lints]
workspace = true

[build-dependencies]
toml = "0.8"
84 changes: 84 additions & 0 deletions crates/rocm-deps/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright © Advanced Micro Devices, Inc., or its affiliates.
//
// SPDX-License-Identifier: MIT

//! Turn the workspace-root `runtime-deps.toml` pins into Rust constants.
//!
//! Every `[runtime.<name>]` field becomes a `pub const <NAME>_<FIELD>: &str`
//! written to `$OUT_DIR/pins.rs`, which `src/lib.rs` includes. Generating at
//! build time (rather than committing a generated source file) means the pin
//! exists exactly once in the tree, so there is nothing to drift and no
//! `--check` gate to keep honest; a missing or malformed pin is a build error.

use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::{env, fs};

/// Pin file, relative to this crate's manifest directory.
const PINS_FILE: &str = "../../runtime-deps.toml";

fn main() {
let manifest_dir = PathBuf::from(
env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by cargo"),
);
let pins_path = manifest_dir.join(PINS_FILE);
println!("cargo:rerun-if-changed={}", pins_path.display());

let text = fs::read_to_string(&pins_path)
.unwrap_or_else(|err| panic!("failed to read {}: {err}", pins_path.display()));
let generated = render(&text, &pins_path);

let out_path =
PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by cargo")).join("pins.rs");
fs::write(&out_path, generated)
.unwrap_or_else(|err| panic!("failed to write {}: {err}", out_path.display()));
}

/// Render the constants for one pin file. Panics with a pointed message on any
/// schema violation so a bad pin fails the build instead of the runtime.
fn render(text: &str, path: &Path) -> String {
let doc: toml::Value = toml::from_str(text)
.unwrap_or_else(|err| panic!("{} is not valid TOML: {err}", path.display()));
let runtimes = doc
.get("runtime")
.and_then(toml::Value::as_table)
.unwrap_or_else(|| panic!("{} must define a [runtime] table", path.display()));

let mut out = String::from(
"// @generated by build.rs from runtime-deps.toml -- do not edit; edit the pin file.\n",
);
// `a-b` and `a.b` both fold to `A_B`, so two differently spelled entries
// could generate the same constant. Reject that here rather than emitting
// a duplicate definition and a confusing compile error in the generated
// file.
let mut seen: BTreeMap<String, String> = BTreeMap::new();
for (name, entry) in runtimes {
let fields = entry
.as_table()
.unwrap_or_else(|| panic!("[runtime.{name}] must be a table of string fields"));
for (field, value) in fields {
let literal = value
.as_str()
.unwrap_or_else(|| panic!("runtime.{name}.{field} must be a string"));
let ident = format!("{}_{}", const_ident(name), const_ident(field));
let source = format!("runtime.{name}.{field}");
if let Some(previous) = seen.insert(ident.clone(), source.clone()) {
panic!("{previous} and {source} both generate {ident}; rename one");
}
// `{:?}` renders a correctly escaped Rust string literal, so any
// quote, backslash or control character in the pin stays valid.
writeln!(
out,
"/// `{source}` from `runtime-deps.toml`.\npub const {ident}: &str = {literal:?};"
)
.expect("writing to a String cannot fail");
}
}
out
}

/// `rocm-abi` -> `ROCM_ABI`: upper-case, with separators folded to `_`.
fn const_ident(raw: &str) -> String {
raw.to_uppercase().replace(['-', '.'], "_")
}
Loading
Loading