Skip to content

Commit

Permalink
Add support for http, https and file pulls
Browse files Browse the repository at this point in the history
Summary by Sourcery

Add support for pulling models using http, https, and file URLs, enabling direct execution of models from web or local sources. Update documentation and add tests to cover the new functionality.

New Features:

    Add support for pulling models using http, https, and file URLs, allowing models to be run directly from web sources or local files.

Enhancements:

    Refactor the symlink creation process to use os.symlink directly instead of run_cmd.

Documentation:

    Update documentation to include new URL syntax support for http, https, and file protocols, explaining how models can be pulled from these sources.

Tests:

    Add system tests to verify the functionality of pulling models using file and https URLs, ensuring they can be listed and removed correctly.

Signed-off-by: Daniel J Walsh <[email protected]>
  • Loading branch information
rhatdan committed Nov 18, 2024
1 parent fb5ee93 commit 45911be
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 30 deletions.
10 changes: 9 additions & 1 deletion docs/ramalama.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,21 @@ RamaLama supports multiple AI model registries types called transports. Supporte
| OCI Container Registries | [`opencontainers.org`](https://opencontainers.org)|
||Examples: [`quay.io`](https://quay.io), [`Docker Hub`](https://docker.io), and [`Artifactory`](https://artifactory.com)|

RamaLama can also pull directly using URL syntax.

http://, https:// and file://.

This means if a model is on a web site or even on your local system, you can run it directly.

RamaLama uses the Ollama registry transport by default. The default can be overridden in the ramalama.conf file or use the RAMALAMA_TRANSPORTS
environment. `export RAMALAMA_TRANSPORT=huggingface` Changes RamaLama to use huggingface transport.

Individual model transports can be modifies when specifying a model via the `huggingface://`, `oci://`, or `ollama://` prefix.
Individual model transports can be modifies when specifying a model via the `huggingface://`, `oci://`, `ollama://`, `https://`, `http://`, `file://` prefix.

ramalama pull `huggingface://`afrideva/Tiny-Vicuna-1B-GGUF/tiny-vicuna-1b.q2_k.gguf

ramalama run `file://`$HOME/granite-7b-lab-Q4_K_M.gguf

To make it easier for users, RamaLama uses shortname files, which container
alias names for fully specified AI Models allowing users to specify the shorter
names when referring to models. RamaLama reads shortnames.conf files if they
Expand Down
8 changes: 7 additions & 1 deletion ramalama/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
)
from ramalama.oci import OCI
from ramalama.ollama import Ollama
from ramalama.url import URL
from ramalama.shortnames import Shortnames
from ramalama.toml_parser import TOMLParser
from ramalama.version import version, print_version
Expand Down Expand Up @@ -430,7 +431,10 @@ def _list_models(args):
# Collect model data
for path in list_files_by_modification():
if path.is_symlink():
name = str(path).replace("/", "://", 1)
if str(path).startswith("file/"):
name = str(path).replace("/", ":///", 1)
else:
name = str(path).replace("/", "://", 1)
file_epoch = path.lstat().st_mtime
modified = int(time.time() - file_epoch)
size = get_size(path)
Expand Down Expand Up @@ -762,6 +766,8 @@ def New(model, args):
return Ollama(model)
if model.startswith("oci://") or model.startswith("docker://"):
return OCI(model, args.engine)
if model.startswith("http://") or model.startswith("https://") or model.startswith("file://"):
return URL(model)

transport = config.get("transport", "ollama")
if transport == "huggingface":
Expand Down
24 changes: 3 additions & 21 deletions ramalama/huggingface.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def __init__(self, model):
model = model.removeprefix("huggingface://")
model = model.removeprefix("hf://")
super().__init__(model)
self.type = "HuggingFace"
self.type = "huggingface"
split = self.model.rsplit("/", 1)
self.directory = split[0] if len(split) > 1 else ""
self.filename = split[1] if len(split) > 1 else split[0]
Expand All @@ -61,16 +61,6 @@ def logout(self, args):
conman_args.extend(["--token", args.token])
self.exec(conman_args)

def path(self, args):
return self.model_path(args)

def exists(self, args):
model_path = self.model_path(args)
if not os.path.exists(model_path):
return None

return model_path

def pull(self, args):
model_path = self.model_path(args)
directory_path = os.path.join(args.store, "repos", "huggingface", self.directory, self.filename)
Expand All @@ -93,13 +83,12 @@ def pull(self, args):
if os.path.exists(target_path) and verify_checksum(target_path):
relative_target_path = os.path.relpath(target_path, start=os.path.dirname(model_path))
if not self.check_valid_model_path(relative_target_path, model_path):
run_cmd(["ln", "-sf", relative_target_path, model_path], debug=args.debug)
os.symlink(relative_target_path, model_path)
return model_path

# Download the model file to the target path
url = f"https://huggingface.co/{self.directory}/resolve/main/{self.filename}"
download_file(url, target_path, headers={}, show_progress=True)

if not verify_checksum(target_path):
print(f"Checksum mismatch for {target_path}, retrying download...")
os.remove(target_path)
Expand All @@ -112,8 +101,7 @@ def pull(self, args):
# Symlink is already correct, no need to update it
return model_path

run_cmd(["ln", "-sf", relative_target_path, model_path], debug=args.debug)

os.symlink(relative_target_path, model_path)
return model_path

def push(self, source, args):
Expand All @@ -137,12 +125,6 @@ def push(self, source, args):
)
return proc.stdout.decode("utf-8")

def model_path(self, args):
return os.path.join(args.store, "models", "huggingface", self.directory, self.filename)

def check_valid_model_path(self, relative_target_path, model_path):
return os.path.exists(model_path) and os.readlink(model_path) == relative_target_path

def exec(self, args):
try:
exec_cmd(args, args.debug)
Expand Down
23 changes: 16 additions & 7 deletions ramalama/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ def login(self, args):
def logout(self, args):
raise NotImplementedError(f"ramalama logout for {self.type} not implemented")

def path(self, source, args):
raise NotImplementedError(f"ramalama path for {self.type} not implemented")

def pull(self, args):
raise NotImplementedError(f"ramalama pull for {self.type} not implemented")

Expand All @@ -67,7 +64,7 @@ def is_symlink_to(self, file_path, target_path):
return False

def garbage_collection(self, args):
repo_paths = ["huggingface", "oci", "ollama"]
repo_paths = ["huggingface", "oci", "ollama", "file", "http", "https"]
for repo in repo_paths:
repo_dir = f"{args.store}/repos/{repo}"
model_dir = f"{args.store}/models/{repo}"
Expand Down Expand Up @@ -102,9 +99,6 @@ def remove(self, args):

self.garbage_collection(args)

def model_path(self, args):
raise NotImplementedError(f"model_path for {self.type} not implemented")

def _image(self, args):
if args.image != default_image():
return args.image
Expand Down Expand Up @@ -343,6 +337,21 @@ def kube(self, model, args, exec_args):
kube = Kube(model, args, exec_args)
kube.generate()

def path(self, args):
return self.model_path(args)

def model_path(self, args):
return os.path.join(args.store, "models", self.type, self.directory, self.filename)

def exists(self, args):
model_path = self.model_path(args)
if not os.path.exists(model_path):
return None

return model_path

def check_valid_model_path(self, relative_target_path, model_path):
return os.path.exists(model_path) and os.readlink(model_path) == relative_target_path

def get_gpu():
i = 0
Expand Down
43 changes: 43 additions & 0 deletions ramalama/url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import os
from ramalama.common import download_file
from ramalama.model import Model


class URL(Model):
def __init__(self, model):
self.type = ""
for prefix in ["file", "http", "https"]:
if model.startswith(f"{prefix}://"):
self.type = prefix
model = model.removeprefix(f"{prefix}://")
break

super().__init__(model)
split = self.model.rsplit("/", 1)
self.directory = split[0].removeprefix("/") if len(split) > 1 else ""
self.filename = split[1] if len(split) > 1 else split[0]

def pull(self, args):
model_path = self.model_path(args)
directory_path = os.path.join(args.store, "repos", self.type, self.directory, self.filename)
os.makedirs(directory_path, exist_ok=True)

symlink_dir = os.path.dirname(model_path)
os.makedirs(symlink_dir, exist_ok=True)

target_path = os.path.join(directory_path, self.filename)

if self.type == "file":
os.symlink(self.model, os.path.join(symlink_dir, self.filename))
os.symlink(self.model, target_path)
else:
url = self.type + "://" + self.model
# Download the model file to the target path
download_file(url, target_path, headers={}, show_progress=True)
relative_target_path = os.path.relpath(target_path, start=os.path.dirname(model_path))
if self.check_valid_model_path(relative_target_path, model_path):
# Symlink is already correct, no need to update it
return model_path
os.symlink(relative_target_path, model_path)

return model_path
16 changes: 16 additions & 0 deletions test/system/050-pull.bats
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ load setup_suite
run_ramalama rm oci://quay.io/mmortari/gguf-py-example:v1
}

@test "ramalama file URL" {
model=$RAMALAMA_TMPDIR/mymodel.gguf
touch $model
file_url=file://${model}
https_url=https://github.com/containers/ramalama/blob/main/README.md

for url in $file_url $https_url; do
run_ramalama pull $url
run_ramalama list
is "$output" ".*$url" "URL exists"
run_ramalama rm $url
run_ramalama list
assert "$output" !~ ".*$url" "URL no longer exists"
done
}

@test "ramalama use registry" {
skip_if_darwin
skip_if_docker
Expand Down

0 comments on commit 45911be

Please sign in to comment.