use std::{collections::HashMap, sync::Arc};
use async_trait::async_trait;
use lazy_static::lazy_static;
use lunchbox::{
chroot::ChrootFS,
path::{LunchboxPathUtils, PathBuf},
types::{MaybeSend, MaybeSync},
};
use semver::VersionReq;
use url::{ParseError, Url};
use zipfs::{GetReader, ZipFS};
use crate::{
error::CartonError,
http::HTTPFile,
httpfs::{FileInfo, HttpFS},
info::CartonInfoWithExtras,
overlayfs::OverlayFS,
types::{CartonInfo, Device, GenericStorage, LoadOpts, TensorStorage},
};
pub(crate) async fn load(url_or_path: &str, opts: LoadOpts) -> ReturnType {
fetch(url_or_path, opts, false).await
}
pub(crate) async fn get_carton_info(
url_or_path: &str,
) -> crate::error::Result<CartonInfoWithExtras<GenericStorage>> {
let (info, _) = fetch(url_or_path, Default::default(), true).await?;
Ok(info)
}
pub(crate) type ReturnType =
crate::error::Result<(CartonInfoWithExtras<GenericStorage>, Option<Runner>)>;
pub(crate) enum Runner {
V1(runner_interface_v1::Runner),
}
const MAX_SUPPORTED_INTERFACE_VERSION: u64 = 1;
async fn fetch(url: &str, opts: LoadOpts, skip_runner: bool) -> ReturnType {
let url = parse_protocol(url);
match url {
#[cfg(not(target_family = "wasm"))]
LocatorWithProtocol::LocalFilePath(path) => {
if tokio::fs::metadata(&path.0).await?.is_dir() {
maybe_resolve_links(
&Arc::new(lunchbox::LocalFS::with_base_dir(path.0).await.unwrap()),
opts,
skip_runner,
)
.await
} else {
unwrap_container(path, opts, skip_runner).await
}
}
#[cfg(target_family = "wasm")]
LocatorWithProtocol::LocalFilePath(_) => panic!("Local file paths not supported on wasm!"),
LocatorWithProtocol::HttpURL(url) => unwrap_container(url, opts, skip_runner).await,
}
}
async fn unwrap_container<T>(item: T, opts: LoadOpts, skip_runner: bool) -> ReturnType
where
T: GetReader + 'static + MaybeSync + MaybeSend,
T::R: MaybeSync + MaybeSend,
{
let zip = ZipFS::new(item).await;
maybe_resolve_links(&Arc::new(zip), opts, skip_runner).await
}
async fn maybe_resolve_links<T>(fs: &Arc<T>, opts: LoadOpts, skip_runner: bool) -> ReturnType
where
T: lunchbox::ReadableFileSystem + MaybeSend + MaybeSync + 'static,
T::FileType: lunchbox::types::ReadableFile + MaybeSend + MaybeSync + Unpin,
T::ReadDirPollerType: MaybeSend,
{
let has_manifest = PathBuf::from("/MANIFEST").exists(fs.as_ref()).await;
let has_links = PathBuf::from("/LINKS").exists(fs.as_ref()).await;
if !has_manifest {
todo!()
}
if !has_links {
load_carton(fs, opts, skip_runner).await
} else {
let mut contents = HashMap::new();
let manifest = fs.read_to_string("/MANIFEST").await?;
for line in manifest.lines() {
if let Some((file_path, sha256)) = line.rsplit_once("=") {
contents.insert(file_path, sha256);
} else {
return Err(CartonError::Other(
"MANIFEST was not in the form {path}={sha256}",
));
}
}
let links = fs.read_to_string("/LINKS").await?;
let links: crate::format::v1::links::Links = toml::from_str(&links)?;
let file_mapping = contents
.into_iter()
.filter_map(|(path, sha256)| {
if let Some(urls) = links.urls.get(sha256) {
if let Some(url) = urls.first() {
Some((
path.into(),
FileInfo {
url: url.clone(),
sha256: sha256.to_owned(),
},
))
} else {
None
}
} else {
None
}
})
.collect();
let httpfs = Arc::new(HttpFS::new(CLIENT.clone(), file_mapping));
let overlay = Arc::new(OverlayFS::new(httpfs, fs.clone()));
load_carton(&overlay, opts, skip_runner).await
}
}
async fn load_carton<T>(fs: &Arc<T>, opts: LoadOpts, skip_runner: bool) -> ReturnType
where
T: lunchbox::ReadableFileSystem + MaybeSend + MaybeSync + 'static,
T::FileType: lunchbox::types::ReadableFile + MaybeSend + MaybeSync + Unpin,
T::ReadDirPollerType: MaybeSend,
{
let info_with_extras = crate::format::v1::load(fs).await?;
let visible_device = opts.visible_device.clone();
let info_with_extras = merge_in_load_opts(info_with_extras, opts)?;
if skip_runner {
Ok((info_with_extras, None))
} else {
let (runner, _) =
discover_or_get_runner_and_launch(&info_with_extras.info, &visible_device).await?;
let wrapped = Arc::new(ChrootFS::new(fs.clone(), "model".into()));
load_model(&wrapped, &runner, &info_with_extras, visible_device).await?;
Ok((info_with_extras, Some(runner)))
}
}
#[cfg(not(target_family = "wasm"))]
pub(crate) async fn discover_or_get_runner_and_launch<T>(
info: &CartonInfo<T>,
visible_device: &Device,
) -> crate::error::Result<(Runner, carton_runner_packager::discovery::RunnerInfo)>
where
T: TensorStorage,
{
use carton_runner_packager::{
discovery::RunnerFilterConstraints,
fetch::{get_or_install_runner, RunnerInstallConstraints},
};
use runner_interface_v1::slowlog::slowlog;
let filters = RunnerFilterConstraints {
runner_name: Some(info.runner.runner_name.clone()),
framework_version_range: Some(info.runner.required_framework_version.clone()),
runner_compat_version: info.runner.runner_compat_version,
max_runner_interface_version: MAX_SUPPORTED_INTERFACE_VERSION,
platform: target_lexicon::HOST.to_string(),
};
let mut sl = slowlog(
format!(
"Fetching runner for '{}' version '{}'",
filters.runner_name.as_ref().unwrap(),
filters.framework_version_range.as_ref().unwrap()
),
5,
)
.await
.without_progress();
let candidate = get_or_install_runner(
"https://nightly.carton.run/v1/runners",
&RunnerInstallConstraints { id: None, filters },
false,
)
.await;
sl.done();
match candidate {
Ok(candidate) => {
match candidate.runner_interface_version {
1 => {
let runner = runner_interface_v1::Runner::new(
&std::path::PathBuf::from(&candidate.runner_path),
visible_device.clone().into(),
)
.await
.unwrap();
Ok((Runner::V1(runner), candidate))
}
version => unreachable!(
"This runner requires a newer interface ({version}) than we have. Shouldn't happen because we filtered above."
),
}
}
Err(e) => {
panic!("No matching runner: {e}")
}
}
}
#[cfg(target_family = "wasm")]
pub(crate) async fn discover_or_get_runner_and_launch<T>(
c: &CartonInfo<T>,
visible_device: &Device,
) -> crate::error::Result<(Runner, ())>
where
T: TensorStorage,
{
todo!()
}
pub(crate) async fn load_model<T, U>(
fs: &Arc<T>,
runner: &Runner,
c: &CartonInfoWithExtras<U>,
visible_device: Device,
) -> crate::error::Result<()>
where
T: lunchbox::ReadableFileSystem + MaybeSend + MaybeSync + 'static,
T::FileType: lunchbox::types::ReadableFile + MaybeSend + MaybeSync + Unpin,
T::ReadDirPollerType: MaybeSend,
U: TensorStorage,
{
match runner {
Runner::V1(runner) => {
runner
.load(
fs,
c.info.runner.runner_name.clone(),
c.info.runner.required_framework_version.clone(),
c.info.runner.runner_compat_version.unwrap(),
c.info
.runner
.opts
.clone()
.map(|item| item.into_iter().map(|(k, v)| (k, v.into())).collect()),
visible_device.into(),
c.manifest_sha256.clone(),
)
.await
.map_err(|e| CartonError::ErrorFromRunner(e))?;
}
}
Ok(())
}
pub(crate) fn merge_in_load_opts<T>(
mut info_with_extras: CartonInfoWithExtras<T>,
opts: LoadOpts,
) -> crate::error::Result<CartonInfoWithExtras<T>>
where
T: TensorStorage,
{
if let Some(v) = opts.override_runner_name {
info_with_extras.info.runner.runner_name = v;
}
if let Some(v) = opts.override_required_framework_version {
info_with_extras.info.runner.required_framework_version =
VersionReq::parse(&v).map_err(|_| {
CartonError::Other(
"`override_required_framework_version` was not a valid semver version range",
)
})?;
}
if let Some(v) = opts.override_runner_opts {
info_with_extras.info.runner.opts =
if let Some(mut orig) = info_with_extras.info.runner.opts {
for (k, val) in v.into_iter() {
orig.insert(k, val);
}
Some(orig)
} else {
Some(v)
}
}
Ok(info_with_extras)
}
fn parse_protocol(input: &str) -> LocatorWithProtocol {
match Url::parse(input) {
Ok(parsed) => match parsed.scheme() {
"file" => LocatorWithProtocol::LocalFilePath(input.into()),
"http" | "https" => LocatorWithProtocol::HttpURL(input.into()),
_other => todo!(),
},
Err(ParseError::RelativeUrlWithoutBase) => LocatorWithProtocol::LocalFilePath(input.into()),
Err(_e) => todo!(), }
}
enum LocatorWithProtocol {
LocalFilePath(protocol::LocalFilePath),
HttpURL(protocol::HttpURL),
}
mod protocol {
pub struct LocalFilePath(pub String);
pub struct HttpURL(pub String);
impl From<&str> for LocalFilePath {
fn from(value: &str) -> Self {
Self(value.to_owned())
}
}
impl From<&str> for HttpURL {
fn from(value: &str) -> Self {
Self(value.to_owned())
}
}
}
#[cfg(not(target_family = "wasm"))]
#[async_trait]
impl GetReader for protocol::LocalFilePath {
type R = tokio::fs::File;
async fn get(&self) -> Self::R {
tokio::fs::File::open(&self.0).await.unwrap()
}
}
lazy_static! {
static ref CLIENT: reqwest::Client = {
#[cfg(not(target_family = "wasm"))]
return reqwest::ClientBuilder::new()
.http1_only()
.use_rustls_tls()
.build()
.unwrap();
#[cfg(target_family = "wasm")]
return reqwest::Client::new();
};
}
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
impl GetReader for protocol::HttpURL {
type R = crate::http::HTTPFile;
async fn get(&self) -> Self::R {
HTTPFile::new(CLIENT.clone(), self.0.clone(), true)
.await
.unwrap()
}
}