Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support pulling Ollama [non-]OCI image #2539

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

yeahdongcn
Copy link

@yeahdongcn yeahdongcn commented Aug 26, 2024

Background:

Kubernetes 1.31 introduced a new feature: Read-Only Volumes Based on OCI Artifacts. I believe this feature could be very useful for deploying a dedicated model alongside Ollama in Kubernetes.

Ollama has introduced several new media types (e.g. application/vnd.ollama.image.model) for storing GGUF models, system prompts, and more. Each layer is essentially a file and does not need to be untarred.

This PR adds a new field, layerFilename, to addedLayerInfo, and the overlay driver will handle the layer creation separately.

A separate PR for containers/storage will be submitted later.

Please see the following logs for instructions on how to mount the Ollama image as a volume:

# Copied from testdata and added mounts information
❯ cat container.json
{
  "metadata": {
    "name": "podsandbox-sleep"
  },
  "image": {
    "image": "registry.docker.com/ollama/ollama:latest"
  },
  "command": [
    "/bin/sleep",
    "6000"
  ],
  "args": [
    "6000"
  ],
  "working_dir": "/",
  "envs": [
    {
      "key": "PATH",
      "value": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    },
    {
      "key": "GLIBC_TUNABLES",
      "value": "glibc.pthread.rseq=0"
    }
  ],
  "annotations": {
    "pod": "podsandbox"
  },
  "log_path": "",
  "stdin": false,
  "stdin_once": false,
  "tty": false,
  "linux": {
    "security_context": {
      "namespace_options": {
        "pid": 1
      },
      "readonly_rootfs": false
    },
    "resources": {
      "cpu_period": 10000,
      "cpu_quota": 20000,
      "cpu_shares": 512,
      "oom_score_adj": 30,
      "memory_limit_in_bytes": 268435456
    }
  },
  "mounts": [
    {
      "host_path": "",
      "container_path": "/volume",
      "image": {
        "image": "registry.ollama.ai/library/tinyllama:latest"
      },
      "readonly": true
    }
  ]
}
# copied from testdata
❯ cat sandbox_config.json
{
        "metadata": {
                "name": "podsandbox1",
                "uid": "redhat-test-crio",
                "namespace": "redhat.test.crio",
                "attempt": 1
        },
        "hostname": "crictl_host",
        "log_directory": "",
        "dns_config": {
                "servers": [
                        "8.8.8.8"
                ]
        },
        "port_mappings": [],
        "resources": {
                "cpu": {
                        "limits": 3,
                        "requests": 2
                },
                "memory": {
                        "limits": 50000000,
                        "requests": 2000000
                }
        },
        "labels": {
                "group": "test"
        },
        "annotations": {
                "owner": "hmeng",
                "security.alpha.kubernetes.io/seccomp/pod": "unconfined",
                "com.example.test": "sandbox annotation"
        },
        "linux": {
                "cgroup_parent": "pod_123-456.slice",
                "security_context": {
                        "namespace_options": {
                                "network": 2,
                                "pid": 1,
                                "ipc": 0
                        },
                        "selinux_options": {
                                "user": "system_u",
                                "role": "system_r",
                                "type": "svirt_lxc_net_t",
                                "level": "s0:c4,c5"
                        }
                }
        }
}
❯ sudo crictl --timeout=200s --runtime-endpoint unix:///run/crio/crio.sock run ./container.json ./sandbox_config.json
INFO[0005] Pulling container image: registry.docker.com/ollama/ollama:latest 
INFO[0005] Pulling image registry.ollama.ai/library/tinyllama:latest to be mounted to container path: /volume 
7e437894449f6429799cc5ef236c4a4570a69e3769bf324bbf700045e383cae8
❯ sudo crictl --timeout=200s --runtime-endpoint unix:///run/crio/crio.sock ps
CONTAINER           IMAGE                                        CREATED             STATE               NAME                ATTEMPT             POD ID              POD
7e437894449f6       registry.docker.com/ollama/ollama:latest     8 seconds ago       Running             podsandbox-sleep    0                   4d1766fdf286b       unknown
❯ sudo crictl --timeout=200s --runtime-endpoint unix:///run/crio/crio.sock exec -it 7e437894449f6 bash
root@crictl_host:/# cd volume/
root@crictl_host:/volume# ls -l
total 622772
-rw-r--r-- 1 root root 637699456 Aug 26 08:32 model
-rw-r--r-- 1 root root        98 Aug 26 08:32 params
-rw-r--r-- 1 root root        31 Aug 26 08:32 system
-rw-r--r-- 1 root root        70 Aug 26 08:32 template
root@crictl_host:/volume# 

Signed-off-by: Xiaodong Ye <xiaodong.ye@mthreads.com>
Signed-off-by: Xiaodong Ye <xiaodong.ye@mthreads.com>
Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

We need to have a proper design discussion about storing non-image artifacts, and what that means.

Do not merge until that happens.

Cc: @baude


The “image volume” feature explicitly talks about image volumes. I don’t see that arbitrarily extending it to also accept other content is obviously desirable.

And even if it were desirable at the Kubernetes level, that doesn’t at all imply that the data should be stored in “images” with “layers” and OverlayFS and that it should be possible to podman run the thing.


We really need to have a design discussion about the storage first; but even if we did accept the proposed mechanism, the c/image implementation would have to be much more extensive. This is nowhere near enough.

@@ -996,7 +997,18 @@ func (c *dockerClient) fetchManifest(ctx context.Context, ref dockerReference, t
if err != nil {
return nil, "", err
}
return manblob, simplifyContentType(res.Header.Get("Content-Type")), nil
// Extra check for the content type in the manifest
if contentType == "text/plain" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • It is fundamentally unsound to override an externally-supplied MIME type of some data by parsing the data in another way and trusting its contents. The whole point of having the MIME type separate is that it directs how the data is to be interpreted. I’m unhappy about such special cases, and I’m especially unhappy about adding them in entirely new use cases where we could just choose not to do that. Why is this necessary here?
  • The docker transport has no business interpreting image contents this way; that’s the responsibility of the generic code.
  • … and the generic code already has a heuristic fallback for text/plain; this would override/break that.

@@ -43,6 +64,8 @@ func SupportedSchema2MediaType(m string) error {
switch m {
case DockerV2ListMediaType, DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, DockerV2Schema2ConfigMediaType, DockerV2Schema2ForeignLayerMediaType, DockerV2Schema2ForeignLayerMediaTypeGzip, DockerV2Schema2LayerMediaType, DockerV2Schema2MediaType, DockerV2SchemaLayerMediaTypeUncompressed:
return nil
case OllamaImageModelLayerMediaType, OllamaImageAdapterLayerMediaType, OllamaImageProjectorLayerMediaType, OllamaImagePromptLayerMediaType, OllamaImageTemplateLayerMediaType, OllamaImageSystemLayerMediaType, OllamaImageParamsLayerMediaType, OllamaImageMessagesLayerMediaType, OllamaImageLicenseLayerMediaType:
return nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t know why we would add all of these to v2s2; that’s a ~frozen format.

And if we did add them, we would need to adjust all of the rest of the v2s2 code to correctly interpret such data, at the very least to not parse it as an ordinary image.

Either way, it seems to me that this should either be completely opaque (a proper OCI artifact, maybe), or intentionally a completely new manifest / image format implementation.

@@ -218,9 +220,18 @@ func (s *storageImageDestination) PutBlobWithOptions(ctx context.Context, stream
return info, nil
}

layerFilename := func() *string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t see why a nested function is necessary here.

@@ -218,9 +220,18 @@ func (s *storageImageDestination) PutBlobWithOptions(ctx context.Context, stream
return info, nil
}

layerFilename := func() *string {
if strings.HasPrefix(blobinfo.MediaType, manifest.OllamaImageLayerMediaTypePrefix) {
filename := strings.TrimPrefix(blobinfo.MediaType, manifest.OllamaImageLayerMediaTypePrefix)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choosing a “filename”, whatever the value is, this way makes no sense to me. What happens if there are two layers with the same MIME type in the image?

@@ -218,9 +220,18 @@ func (s *storageImageDestination) PutBlobWithOptions(ctx context.Context, stream
return info, nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an early return above; in that case .filename would not be set.

OK, that’s somewhat theoretical. But also, nothing sets .filename on the TryReusingBlobWithOptions code path. That one is 100% a blocker.

@@ -218,9 +220,18 @@ func (s *storageImageDestination) PutBlobWithOptions(ctx context.Context, stream
return info, nil
}

layerFilename := func() *string {
if strings.HasPrefix(blobinfo.MediaType, manifest.OllamaImageLayerMediaTypePrefix) {
filename := strings.TrimPrefix(blobinfo.MediaType, manifest.OllamaImageLayerMediaTypePrefix)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would the storageImageSource counterpart look like?!

@cgwalters cgwalters marked this pull request as draft August 26, 2024 16:55
@mtrmac
Copy link
Collaborator

mtrmac commented Aug 26, 2024

Compare also containers/skopeo#2395 (and, I think, ollama/ollama#6510 ): “Ollama” itself is not even remotely an OCI-compliant registry right now.

What is the ecosystem and community situation for this data format? Is this a multi-faceted interoperability effort, where all of these things are going to happily fall into place, or is this setting us up on an adversarial interoperability path?

Cc: @baude , another thing to have a very clear position on.

@cgwalters
Copy link
Contributor

Agree that we should only support compliant registries.

@yeahdongcn
Copy link
Author

Compare also containers/skopeo#2395 (and, I think, ollama/ollama#6510 ): “Ollama” itself is not even remotely an OCI-compliant registry right now.

What is the ecosystem and community situation for this data format? Is this a multi-faceted interoperability effort, where all of these things are going to happily fall into place, or is this setting us up on an adversarial interoperability path?

Cc: @baude , another thing to have a very clear position on.

Yes. I faced two issues pulling from registry.ollama.ai:

I thought decoding the manifest JSON might make MIME type determination more robust, but it clearly looks incorrect.

@mtrmac mtrmac changed the title Support pulling Ollama OCI image Support pulling Ollama [non-]OCI image Aug 27, 2024
@mtrmac
Copy link
Collaborator

mtrmac commented Aug 27, 2024

Separate from the storage discussion happening in containers/storage#2075, and assuming the Ollama format and transport will not change (will it? I have no idea)

  • We need to actually decide on what is the target format. Will there be some kind of actually-OCI artifact format to target, or is the Ollama format the one to support?
  • If there will be an OCI format (regardless of transport), it’s not clear that the conversion between the Ollama transport+format should exist in c/image, it might very reasonably be a small custom tool. There’s just not that much to share with the rest of c/image.
  • If c/image should support the Ollama transport natively (regardless of format), it might well make sense to make that a separate c/image ImageTransport. If it doesn’t really work as Docker, it can reasonably be a separate HTTP client.

@yeahdongcn
Copy link
Author

yeahdongcn commented Aug 28, 2024

The issue I’m trying to solve is how to make OCI artifacts mountable as volumes in containers (the Ollama image is a practical example, but it uses some custom MIME types—can it still be defined as an OCI artifact?).

This blog post Kubernetes 1.31: Image Volume Source discusses some practical use cases that I find quite relevant. In particular, using OCI artifacts to distribute AI models (weights, parameters, system prompts, etc.) in enterprise AI inference services is crucial, especially for version management and signing.

The volume image used in the blog is quay.io/crio/artifact:v1, and inspecting it with Skopeo reveals that its layer MIME type is application/vnd.oci.image.layer.v1.tar+gzip, which is correctly recognized and parsed by c/image.

➜ ./bin/skopeo inspect docker://quay.io/crio/artifact:v1                   
{
    "Name": "quay.io/crio/artifact",
    "Digest": "sha256:52c997d8906f39c5b5550dd1f05fd785fb46bbb50eb055f9df61df6dc9e3a6cd",
    "RepoTags": [
        "v1"
    ],
    "Created": null,
    "DockerVersion": "",
    "Labels": null,
    "Architecture": "amd64",
    "Os": "linux",
    "Layers": [
        "sha256:4e80bd845ae8736c96d644d96142807d958f44505e973f6eb4145689e05e86d2"
    ],
    "LayersData": [
        {
            "MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
            "Digest": "sha256:4e80bd845ae8736c96d644d96142807d958f44505e973f6eb4145689e05e86d2",
            "Size": 170,
            "Annotations": null
        }
    ],
    "Env": null
}

However, for Ollama images or other images with custom MIME types, should we consider adding support for these, or perhaps introducing a plugin mechanism to allow vendors to implement their own solutions?

➜ ./bin/skopeo inspect docker://sh-harbor.mthreads.com/cloud-mirror/tinyllama:latest
{
    "Name": "sh-harbor.mthreads.com/cloud-mirror/tinyllama",
    "Digest": "sha256:2644915ede352ea7bdfaff0bfac0be74c719d5d5202acb63a6fb095b52f394a4",
    "RepoTags": [
        "latest"
    ],
    "Created": "0001-01-01T00:00:00Z",
    "DockerVersion": "",
    "Labels": null,
    "Architecture": "amd64",
    "Os": "linux",
    "Layers": [
        "sha256:2af3b81862c6be03c769683af18efdadb2c33f60ff32ab6f83e42c043d6c7816",
        "sha256:af0ddbdaaa26f30d54d727f9dd944b76bdb926fdaf9a58f63f78c532f57c191f",
        "sha256:c8472cd9daed5e7c20aa53689e441e10620a002aacd58686aeac2cb188addb5c",
        "sha256:fa956ab37b8c21152f975a7fcdd095c4fee8754674b21d9b44d710435697a00d"
    ],
    "LayersData": [
        {
            "MIMEType": "application/vnd.ollama.image.model",
            "Digest": "sha256:2af3b81862c6be03c769683af18efdadb2c33f60ff32ab6f83e42c043d6c7816",
            "Size": 637699456,
            "Annotations": null
        },
        {
            "MIMEType": "application/vnd.ollama.image.template",
            "Digest": "sha256:af0ddbdaaa26f30d54d727f9dd944b76bdb926fdaf9a58f63f78c532f57c191f",
            "Size": 70,
            "Annotations": null
        },
        {
            "MIMEType": "application/vnd.ollama.image.system",
            "Digest": "sha256:c8472cd9daed5e7c20aa53689e441e10620a002aacd58686aeac2cb188addb5c",
            "Size": 31,
            "Annotations": null
        },
        {
            "MIMEType": "application/vnd.ollama.image.params",
            "Digest": "sha256:fa956ab37b8c21152f975a7fcdd095c4fee8754674b21d9b44d710435697a00d",
            "Size": 98,
            "Annotations": null
        }
    ],
    "Env": null
}

@mtrmac
Copy link
Collaborator

mtrmac commented Aug 28, 2024

The issue I’m trying to solve is how to make OCI artifacts mountable as volumes in containers (the Ollama image is a practical example, but it uses some custom MIME types—can it still be defined as an OCI artifact?).

I think it’s necessary to be precise here. The Ollama format is not even in OCI format, and it is not distributed on an OCI registry; so it can’t be an OCI artifact. We can talk about supporting the Ollama format, and about supporting OCI artifacts, but they are not the same thing at all.


This blog post Kubernetes 1.31: Image Volume Source

The volume image used in the blog is quay.io/crio/artifact:v1

That… is also not an OCI artifact; it’s an OCI image.

And if you read https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/4639-oci-volume-source :

The runtimes (CRI-O, containerd, others) will have to agree on the implementation of how artifacts are manifested as directories.

It’s been an explicit non-goal to have a defined way to use OCI artifacts.

Now, that doesn’t mean that we shouldn’t talk about it and design it. It’s clearly desirable to use something else than images for non-image data. But let’s be clear that we are designing it; not implementing a consensus spec.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants