浏览代码

Merge branch 'main' into install-instructions-archlinux

Matt Williams 1 年之前
父节点
当前提交
5ebcde1541

+ 1 - 0
.dockerignore

@@ -6,3 +6,4 @@ scripts
 llm/llama.cpp/ggml
 llm/llama.cpp/ggml
 llm/llama.cpp/gguf
 llm/llama.cpp/gguf
 .env
 .env
+.cache

+ 1 - 0
.gitignore

@@ -6,3 +6,4 @@
 dist
 dist
 ollama
 ollama
 ggml-metal.metal
 ggml-metal.metal
+.cache

+ 9 - 0
README.md

@@ -216,6 +216,10 @@ See the [API documentation](./docs/api.md) for all endpoints.
 
 
 ## Community Integrations
 ## Community Integrations
 
 
+### Mobile
+
+- [Mobile Artificial Intelligence Distribution](https://github.com/MaidFoundation/Maid) (Maid)
+
 ### Web & Desktop
 ### Web & Desktop
 
 
 - [HTML UI](https://github.com/rtcfirefly/ollama-ui)
 - [HTML UI](https://github.com/rtcfirefly/ollama-ui)
@@ -234,12 +238,15 @@ See the [API documentation](./docs/api.md) for all endpoints.
 - [Emacs client](https://github.com/zweifisch/ollama)
 - [Emacs client](https://github.com/zweifisch/ollama)
 - [gen.nvim](https://github.com/David-Kunz/gen.nvim)
 - [gen.nvim](https://github.com/David-Kunz/gen.nvim)
 - [ollama.nvim](https://github.com/nomnivore/ollama.nvim)
 - [ollama.nvim](https://github.com/nomnivore/ollama.nvim)
+- [ogpt.nvim](https://github.com/huynle/ogpt.nvim)
 - [gptel Emacs client](https://github.com/karthink/gptel)
 - [gptel Emacs client](https://github.com/karthink/gptel)
 - [ollama package for archlinux](https://archlinux.org/packages/extra/x86_64/ollama/)
 - [ollama package for archlinux](https://archlinux.org/packages/extra/x86_64/ollama/)
+- [Oatmeal](https://github.com/dustinblackman/oatmeal)
 
 
 ### Libraries
 ### Libraries
 
 
 - [LangChain](https://python.langchain.com/docs/integrations/llms/ollama) and [LangChain.js](https://js.langchain.com/docs/modules/model_io/models/llms/integrations/ollama) with [example](https://js.langchain.com/docs/use_cases/question_answering/local_retrieval_qa)
 - [LangChain](https://python.langchain.com/docs/integrations/llms/ollama) and [LangChain.js](https://js.langchain.com/docs/modules/model_io/models/llms/integrations/ollama) with [example](https://js.langchain.com/docs/use_cases/question_answering/local_retrieval_qa)
+- [LangChainGo](https://github.com/tmc/langchaingo/) with [example](https://github.com/tmc/langchaingo/tree/main/examples/ollama-completion-example)
 - [LlamaIndex](https://gpt-index.readthedocs.io/en/stable/examples/llm/ollama.html)
 - [LlamaIndex](https://gpt-index.readthedocs.io/en/stable/examples/llm/ollama.html)
 - [LiteLLM](https://github.com/BerriAI/litellm)
 - [LiteLLM](https://github.com/BerriAI/litellm)
 - [OllamaSharp for .NET](https://github.com/awaescher/OllamaSharp)
 - [OllamaSharp for .NET](https://github.com/awaescher/OllamaSharp)
@@ -248,6 +255,7 @@ See the [API documentation](./docs/api.md) for all endpoints.
 - [ModelFusion Typescript Library](https://modelfusion.dev/integration/model-provider/ollama)
 - [ModelFusion Typescript Library](https://modelfusion.dev/integration/model-provider/ollama)
 - [OllamaKit for Swift](https://github.com/kevinhermawan/OllamaKit)
 - [OllamaKit for Swift](https://github.com/kevinhermawan/OllamaKit)
 - [Ollama for Dart](https://github.com/breitburg/dart-ollama)
 - [Ollama for Dart](https://github.com/breitburg/dart-ollama)
+- [Ollama for Laravel](https://github.com/cloudstudio/ollama-laravel)
 
 
 ### Mobile
 ### Mobile
 
 
@@ -263,3 +271,4 @@ See the [API documentation](./docs/api.md) for all endpoints.
 - [Dagger Chatbot](https://github.com/samalba/dagger-chatbot)
 - [Dagger Chatbot](https://github.com/samalba/dagger-chatbot)
 - [Discord AI Bot](https://github.com/mekb-turtle/discord-ai-bot)
 - [Discord AI Bot](https://github.com/mekb-turtle/discord-ai-bot)
 - [Hass Ollama Conversation](https://github.com/ej52/hass-ollama-conversation)
 - [Hass Ollama Conversation](https://github.com/ej52/hass-ollama-conversation)
+- [Rivet plugin](https://github.com/abrenneke/rivet-plugin-ollama)

+ 95 - 77
cmd/cmd.go

@@ -30,7 +30,7 @@ import (
 	"github.com/jmorganca/ollama/api"
 	"github.com/jmorganca/ollama/api"
 	"github.com/jmorganca/ollama/format"
 	"github.com/jmorganca/ollama/format"
 	"github.com/jmorganca/ollama/parser"
 	"github.com/jmorganca/ollama/parser"
-	"github.com/jmorganca/ollama/progressbar"
+	"github.com/jmorganca/ollama/progress"
 	"github.com/jmorganca/ollama/readline"
 	"github.com/jmorganca/ollama/readline"
 	"github.com/jmorganca/ollama/server"
 	"github.com/jmorganca/ollama/server"
 	"github.com/jmorganca/ollama/version"
 	"github.com/jmorganca/ollama/version"
@@ -48,14 +48,16 @@ func CreateHandler(cmd *cobra.Command, args []string) error {
 		return err
 		return err
 	}
 	}
 
 
+	p := progress.NewProgress(os.Stderr)
+	defer p.Stop()
+
+	bars := make(map[string]*progress.Bar)
+
 	modelfile, err := os.ReadFile(filename)
 	modelfile, err := os.ReadFile(filename)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	spinner := NewSpinner("transferring context")
-	go spinner.Spin(100 * time.Millisecond)
-
 	commands, err := parser.Parse(bytes.NewReader(modelfile))
 	commands, err := parser.Parse(bytes.NewReader(modelfile))
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -66,6 +68,10 @@ func CreateHandler(cmd *cobra.Command, args []string) error {
 		return err
 		return err
 	}
 	}
 
 
+	status := "transferring model data"
+	spinner := progress.NewSpinner(status)
+	p.Add(status, spinner)
+
 	for _, c := range commands {
 	for _, c := range commands {
 		switch c.Name {
 		switch c.Name {
 		case "model", "adapter":
 		case "model", "adapter":
@@ -76,6 +82,10 @@ func CreateHandler(cmd *cobra.Command, args []string) error {
 				path = filepath.Join(home, path[2:])
 				path = filepath.Join(home, path[2:])
 			}
 			}
 
 
+			if !filepath.IsAbs(path) {
+				path = filepath.Join(filepath.Dir(filename), path)
+			}
+
 			bin, err := os.Open(path)
 			bin, err := os.Open(path)
 			if errors.Is(err, os.ErrNotExist) && c.Name == "model" {
 			if errors.Is(err, os.ErrNotExist) && c.Name == "model" {
 				continue
 				continue
@@ -99,41 +109,34 @@ func CreateHandler(cmd *cobra.Command, args []string) error {
 		}
 		}
 	}
 	}
 
 
-	var currentDigest string
-	var bar *progressbar.ProgressBar
-
-	request := api.CreateRequest{Name: args[0], Path: filename, Modelfile: string(modelfile)}
 	fn := func(resp api.ProgressResponse) error {
 	fn := func(resp api.ProgressResponse) error {
-		if resp.Digest != currentDigest && resp.Digest != "" {
+		if resp.Digest != "" {
 			spinner.Stop()
 			spinner.Stop()
-			currentDigest = resp.Digest
-			// pulling
-			bar = progressbar.DefaultBytes(
-				resp.Total,
-				resp.Status,
-			)
-			bar.Set64(resp.Completed)
-		} else if resp.Digest == currentDigest && resp.Digest != "" {
-			bar.Set64(resp.Completed)
-		} else {
-			currentDigest = ""
+
+			bar, ok := bars[resp.Digest]
+			if !ok {
+				bar = progress.NewBar(fmt.Sprintf("pulling %s...", resp.Digest[7:19]), resp.Total, resp.Completed)
+				bars[resp.Digest] = bar
+				p.Add(resp.Digest, bar)
+			}
+
+			bar.Set(resp.Completed)
+		} else if status != resp.Status {
 			spinner.Stop()
 			spinner.Stop()
-			spinner = NewSpinner(resp.Status)
-			go spinner.Spin(100 * time.Millisecond)
+
+			status = resp.Status
+			spinner = progress.NewSpinner(status)
+			p.Add(status, spinner)
 		}
 		}
 
 
 		return nil
 		return nil
 	}
 	}
 
 
+	request := api.CreateRequest{Name: args[0], Modelfile: string(modelfile)}
 	if err := client.Create(context.Background(), &request, fn); err != nil {
 	if err := client.Create(context.Background(), &request, fn); err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	spinner.Stop()
-	if spinner.description != "success" {
-		return errors.New("unexpected end to create model")
-	}
-
 	return nil
 	return nil
 }
 }
 
 
@@ -170,36 +173,46 @@ func PushHandler(cmd *cobra.Command, args []string) error {
 		return err
 		return err
 	}
 	}
 
 
-	var currentDigest string
-	var bar *progressbar.ProgressBar
+	p := progress.NewProgress(os.Stderr)
+	defer p.Stop()
+
+	bars := make(map[string]*progress.Bar)
+	var status string
+	var spinner *progress.Spinner
 
 
-	request := api.PushRequest{Name: args[0], Insecure: insecure}
 	fn := func(resp api.ProgressResponse) error {
 	fn := func(resp api.ProgressResponse) error {
-		if resp.Digest != currentDigest && resp.Digest != "" {
-			currentDigest = resp.Digest
-			bar = progressbar.DefaultBytes(
-				resp.Total,
-				fmt.Sprintf("pushing %s...", resp.Digest[7:19]),
-			)
-
-			bar.Set64(resp.Completed)
-		} else if resp.Digest == currentDigest && resp.Digest != "" {
-			bar.Set64(resp.Completed)
-		} else {
-			currentDigest = ""
-			fmt.Println(resp.Status)
+		if resp.Digest != "" {
+			if spinner != nil {
+				spinner.Stop()
+			}
+
+			bar, ok := bars[resp.Digest]
+			if !ok {
+				bar = progress.NewBar(fmt.Sprintf("pushing %s...", resp.Digest[7:19]), resp.Total, resp.Completed)
+				bars[resp.Digest] = bar
+				p.Add(resp.Digest, bar)
+			}
+
+			bar.Set(resp.Completed)
+		} else if status != resp.Status {
+			if spinner != nil {
+				spinner.Stop()
+			}
+
+			status = resp.Status
+			spinner = progress.NewSpinner(status)
+			p.Add(status, spinner)
 		}
 		}
+
 		return nil
 		return nil
 	}
 	}
 
 
+	request := api.PushRequest{Name: args[0], Insecure: insecure}
 	if err := client.Push(context.Background(), &request, fn); err != nil {
 	if err := client.Push(context.Background(), &request, fn); err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	if bar != nil && !bar.IsFinished() {
-		return errors.New("unexpected end to push model")
-	}
-
+	spinner.Stop()
 	return nil
 	return nil
 }
 }
 
 
@@ -350,46 +363,51 @@ func PullHandler(cmd *cobra.Command, args []string) error {
 		return err
 		return err
 	}
 	}
 
 
-	return pull(args[0], insecure)
-}
-
-func pull(model string, insecure bool) error {
 	client, err := api.ClientFromEnvironment()
 	client, err := api.ClientFromEnvironment()
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	var currentDigest string
-	var bar *progressbar.ProgressBar
+	p := progress.NewProgress(os.Stderr)
+	defer p.Stop()
+
+	bars := make(map[string]*progress.Bar)
+
+	var status string
+	var spinner *progress.Spinner
 
 
-	request := api.PullRequest{Name: model, Insecure: insecure}
 	fn := func(resp api.ProgressResponse) error {
 	fn := func(resp api.ProgressResponse) error {
-		if resp.Digest != currentDigest && resp.Digest != "" {
-			currentDigest = resp.Digest
-			bar = progressbar.DefaultBytes(
-				resp.Total,
-				fmt.Sprintf("pulling %s...", resp.Digest[7:19]),
-			)
-
-			bar.Set64(resp.Completed)
-		} else if resp.Digest == currentDigest && resp.Digest != "" {
-			bar.Set64(resp.Completed)
-		} else {
-			currentDigest = ""
-			fmt.Println(resp.Status)
+		if resp.Digest != "" {
+			if spinner != nil {
+				spinner.Stop()
+			}
+
+			bar, ok := bars[resp.Digest]
+			if !ok {
+				bar = progress.NewBar(fmt.Sprintf("pulling %s...", resp.Digest[7:19]), resp.Total, resp.Completed)
+				bars[resp.Digest] = bar
+				p.Add(resp.Digest, bar)
+			}
+
+			bar.Set(resp.Completed)
+		} else if status != resp.Status {
+			if spinner != nil {
+				spinner.Stop()
+			}
+
+			status = resp.Status
+			spinner = progress.NewSpinner(status)
+			p.Add(status, spinner)
 		}
 		}
 
 
 		return nil
 		return nil
 	}
 	}
 
 
+	request := api.PullRequest{Name: args[0], Insecure: insecure}
 	if err := client.Pull(context.Background(), &request, fn); err != nil {
 	if err := client.Pull(context.Background(), &request, fn); err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	if bar != nil && !bar.IsFinished() {
-		return errors.New("unexpected end to pull model")
-	}
-
 	return nil
 	return nil
 }
 }
 
 
@@ -442,8 +460,11 @@ func generate(cmd *cobra.Command, model, prompt string, wordWrap bool, format st
 		return err
 		return err
 	}
 	}
 
 
-	spinner := NewSpinner("")
-	go spinner.Spin(60 * time.Millisecond)
+	p := progress.NewProgress(os.Stderr)
+	defer p.StopAndClear()
+
+	spinner := progress.NewSpinner("")
+	p.Add("", spinner)
 
 
 	var latest api.GenerateResponse
 	var latest api.GenerateResponse
 
 
@@ -475,9 +496,7 @@ func generate(cmd *cobra.Command, model, prompt string, wordWrap bool, format st
 
 
 	request := api.GenerateRequest{Model: model, Prompt: prompt, Context: generateContext, Format: format}
 	request := api.GenerateRequest{Model: model, Prompt: prompt, Context: generateContext, Format: format}
 	fn := func(response api.GenerateResponse) error {
 	fn := func(response api.GenerateResponse) error {
-		if !spinner.IsFinished() {
-			spinner.Finish()
-		}
+		p.StopAndClear()
 
 
 		latest = response
 		latest = response
 
 
@@ -511,7 +530,6 @@ func generate(cmd *cobra.Command, model, prompt string, wordWrap bool, format st
 
 
 	if err := client.Generate(cancelCtx, &request, fn); err != nil {
 	if err := client.Generate(cancelCtx, &request, fn); err != nil {
 		if strings.Contains(err.Error(), "context canceled") && abort {
 		if strings.Contains(err.Error(), "context canceled") && abort {
-			spinner.Finish()
 			return nil
 			return nil
 		}
 		}
 		return err
 		return err

+ 0 - 44
cmd/spinner.go

@@ -1,44 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-	"os"
-	"time"
-
-	"github.com/jmorganca/ollama/progressbar"
-)
-
-type Spinner struct {
-	description string
-	*progressbar.ProgressBar
-}
-
-func NewSpinner(description string) *Spinner {
-	return &Spinner{
-		description: description,
-		ProgressBar: progressbar.NewOptions(-1,
-			progressbar.OptionSetWriter(os.Stderr),
-			progressbar.OptionThrottle(60*time.Millisecond),
-			progressbar.OptionSpinnerType(14),
-			progressbar.OptionSetRenderBlankState(true),
-			progressbar.OptionSetElapsedTime(false),
-			progressbar.OptionClearOnFinish(),
-			progressbar.OptionSetDescription(description),
-		),
-	}
-}
-
-func (s *Spinner) Spin(tick time.Duration) {
-	for range time.Tick(tick) {
-		if s.IsFinished() {
-			break
-		}
-
-		s.Add(1)
-	}
-}
-
-func (s *Spinner) Stop() {
-	s.Finish()
-	fmt.Println(s.description)
-}

+ 3 - 1
docs/api.md

@@ -51,7 +51,9 @@ Advanced parameters (optional):
 
 
 ### JSON mode
 ### JSON mode
 
 
-Enable JSON mode by setting the `format` parameter to `json` and specifying the model should use JSON in the `prompt`. This will structure the response as valid JSON. See the JSON mode [example](#request-json-mode) below.
+Enable JSON mode by setting the `format` parameter to `json`. This will structure the response as valid JSON. See the JSON mode [example](#request-json-mode) below.
+
+> Note: it's important to instruct the model to use JSON in the `prompt`. Otherwise, the model may generate large amounts whitespace.
 
 
 ### Examples
 ### Examples
 
 

+ 2 - 2
docs/faq.md

@@ -149,8 +149,8 @@ docker build -t ollama-with-ca .
 docker run -d -e HTTPS_PROXY=https://my.proxy.example.com -p 11434:11434 ollama-with-ca
 docker run -d -e HTTPS_PROXY=https://my.proxy.example.com -p 11434:11434 ollama-with-ca
 ```
 ```
 
 
-### How do I use Ollama with GPU acceleration in Docker?
+## How do I use Ollama with GPU acceleration in Docker?
 
 
-The Ollama Docker container can be configured with GPU acceleration in Linux or Windows (with WSL2). This requires the [nvidia-container-toolkit](https://github.com/NVIDIA/nvidia-container-toolkit).
+The Ollama Docker container can be configured with GPU acceleration in Linux or Windows (with WSL2). This requires the [nvidia-container-toolkit](https://github.com/NVIDIA/nvidia-container-toolkit). See [ollama/ollama](https://hub.docker.com/r/ollama/ollama) for more details.
 
 
 GPU acceleration is not available for Docker Desktop in macOS due to the lack of GPU passthrough and emulation.
 GPU acceleration is not available for Docker Desktop in macOS due to the lack of GPU passthrough and emulation.

+ 33 - 0
docs/modelfile.md

@@ -41,6 +41,8 @@ INSTRUCTION arguments
 
 
 ## Examples
 ## Examples
 
 
+### Basic `Modelfile`
+
 An example of a `Modelfile` creating a mario blueprint:
 An example of a `Modelfile` creating a mario blueprint:
 
 
 ```modelfile
 ```modelfile
@@ -63,6 +65,35 @@ To use this:
 
 
 More examples are available in the [examples directory](../examples).
 More examples are available in the [examples directory](../examples).
 
 
+### `Modelfile`s in [ollama.ai/library][1]
+
+There are two ways to view `Modelfile`s underlying the models in [ollama.ai/library][1]:
+
+- Option 1: view a details page from a model's tags page:
+   1. Go to a particular model's tags (e.g. https://ollama.ai/library/llama2/tags)
+   2. Click on a tag (e.g. https://ollama.ai/library/llama2:13b)
+   3. Scroll down to "Layers"
+      - Note: if the [`FROM` instruction](#from-required) is not present,
+        it means the model was created from a local file
+- Option 2: use `ollama show` to print the `Modelfile` like so:
+
+  ```bash
+  > ollama show --modelfile llama2:13b
+  # Modelfile generated by "ollama show"
+  # To build a new Modelfile based on this one, replace the FROM line with:
+  # FROM llama2:13b
+
+  FROM /root/.ollama/models/blobs/sha256:123abc
+  TEMPLATE """[INST] {{ if and .First .System }}<<SYS>>{{ .System }}<</SYS>>
+
+  {{ end }}{{ .Prompt }} [/INST] """
+  SYSTEM """"""
+  PARAMETER stop [INST]
+  PARAMETER stop [/INST]
+  PARAMETER stop <<SYS>>
+  PARAMETER stop <</SYS>>
+  ```
+
 ## Instructions
 ## Instructions
 
 
 ### FROM (Required)
 ### FROM (Required)
@@ -177,3 +208,5 @@ LICENSE """
 
 
 - the **`Modelfile` is not case sensitive**. In the examples, we use uppercase for instructions to make it easier to distinguish it from arguments.
 - the **`Modelfile` is not case sensitive**. In the examples, we use uppercase for instructions to make it easier to distinguish it from arguments.
 - Instructions can be in any order. In the examples, we start with FROM instruction to keep it easily readable.
 - Instructions can be in any order. In the examples, we start with FROM instruction to keep it easily readable.
+
+[1]: https://ollama.ai/library

+ 5 - 0
examples/jupyter-notebook/README.md

@@ -0,0 +1,5 @@
+# Ollama Jupyter Notebook
+
+This example downloads and installs Ollama in a Jupyter instance such as Google Colab. It will start the Ollama service and expose an endpoint using `ngrok` which can be used to communicate with the Ollama instance remotely.
+
+For best results, use an instance with GPU accelerator.

+ 102 - 0
examples/jupyter-notebook/ollama.ipynb

@@ -0,0 +1,102 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "93f59dcb-c588-41b8-a792-55d88ade739c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Download and run the Ollama Linux install script\n",
+    "!curl https://ollama.ai/install.sh | sh\n",
+    "!command -v systemctl >/dev/null && sudo systemctl stop ollama"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "658c147e-c7f8-490e-910e-62b80f577dda",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "!pip install aiohttp pyngrok\n",
+    "\n",
+    "import os\n",
+    "import asyncio\n",
+    "from aiohttp import ClientSession\n",
+    "\n",
+    "# Set LD_LIBRARY_PATH so the system NVIDIA library becomes preferred\n",
+    "# over the built-in library. This is particularly important for \n",
+    "# Google Colab which installs older drivers\n",
+    "os.environ.update({'LD_LIBRARY_PATH': '/usr/lib64-nvidia'})\n",
+    "\n",
+    "async def run(cmd):\n",
+    "  '''\n",
+    "  run is a helper function to run subcommands asynchronously.\n",
+    "  '''\n",
+    "  print('>>> starting', *cmd)\n",
+    "  p = await asyncio.subprocess.create_subprocess_exec(\n",
+    "      *cmd,\n",
+    "      stdout=asyncio.subprocess.PIPE,\n",
+    "      stderr=asyncio.subprocess.PIPE,\n",
+    "  )\n",
+    "\n",
+    "  async def pipe(lines):\n",
+    "    async for line in lines:\n",
+    "      print(line.strip().decode('utf-8'))\n",
+    "\n",
+    "  await asyncio.gather(\n",
+    "      pipe(p.stdout),\n",
+    "      pipe(p.stderr),\n",
+    "  )\n",
+    "\n",
+    "\n",
+    "await asyncio.gather(\n",
+    "    run(['ollama', 'serve']),\n",
+    "    run(['ngrok', 'http', '--log', 'stderr', '11434']),\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "e7735a55-9aad-4caf-8683-52e2163ba53b",
+   "metadata": {},
+   "source": [
+    "The previous cell starts two processes, `ollama` and `ngrok`. The log output will show a line like the following which describes the external address.\n",
+    "\n",
+    "```\n",
+    "t=2023-11-12T22:55:56+0000 lvl=info msg=\"started tunnel\" obj=tunnels name=command_line addr=http://localhost:11434 url=https://8249-34-125-179-11.ngrok.io\n",
+    "```\n",
+    "\n",
+    "The external address in this case is `https://8249-34-125-179-11.ngrok.io` which can be passed into `OLLAMA_HOST` to access this instance.\n",
+    "\n",
+    "```bash\n",
+    "export OLLAMA_HOST=https://8249-34-125-179-11.ngrok.io\n",
+    "ollama list\n",
+    "ollama run mistral\n",
+    "```"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}

+ 29 - 7
format/bytes.go

@@ -1,23 +1,45 @@
 package format
 package format
 
 
-import "fmt"
+import (
+	"fmt"
+	"math"
+)
 
 
 const (
 const (
 	Byte     = 1
 	Byte     = 1
 	KiloByte = Byte * 1000
 	KiloByte = Byte * 1000
 	MegaByte = KiloByte * 1000
 	MegaByte = KiloByte * 1000
 	GigaByte = MegaByte * 1000
 	GigaByte = MegaByte * 1000
+	TeraByte = GigaByte * 1000
 )
 )
 
 
 func HumanBytes(b int64) string {
 func HumanBytes(b int64) string {
+	var value float64
+	var unit string
+
 	switch {
 	switch {
-	case b > GigaByte:
-		return fmt.Sprintf("%.1f GB", float64(b)/GigaByte)
-	case b > MegaByte:
-		return fmt.Sprintf("%.1f MB", float64(b)/MegaByte)
-	case b > KiloByte:
-		return fmt.Sprintf("%.1f KB", float64(b)/KiloByte)
+	case b >= TeraByte:
+		value = float64(b) / TeraByte
+		unit = "TB"
+	case b >= GigaByte:
+		value = float64(b) / GigaByte
+		unit = "GB"
+	case b >= MegaByte:
+		value = float64(b) / MegaByte
+		unit = "MB"
+	case b >= KiloByte:
+		value = float64(b) / KiloByte
+		unit = "KB"
 	default:
 	default:
 		return fmt.Sprintf("%d B", b)
 		return fmt.Sprintf("%d B", b)
 	}
 	}
+
+	switch {
+	case value >= 100:
+		return fmt.Sprintf("%d %s", int(value), unit)
+	case value != math.Trunc(value):
+		return fmt.Sprintf("%.1f %s", value, unit)
+	default:
+		return fmt.Sprintf("%d %s", int(value), unit)
+	}
 }
 }

+ 2 - 2
llm/llama.cpp/generate_darwin_amd64.go

@@ -7,13 +7,13 @@ package llm
 //go:generate git -C ggml apply ../patches/0002-34B-model-support.patch
 //go:generate git -C ggml apply ../patches/0002-34B-model-support.patch
 //go:generate git -C ggml apply ../patches/0003-metal-fix-synchronization-in-new-matrix-multiplicati.patch
 //go:generate git -C ggml apply ../patches/0003-metal-fix-synchronization-in-new-matrix-multiplicati.patch
 //go:generate git -C ggml apply ../patches/0004-metal-add-missing-barriers-for-mul-mat-2699.patch
 //go:generate git -C ggml apply ../patches/0004-metal-add-missing-barriers-for-mul-mat-2699.patch
-//go:generate cmake -S ggml -B ggml/build/cpu -DLLAMA_ACCELERATE=on -DLLAMA_K_QUANTS=on -DCMAKE_SYSTEM_PROCESSOR=x86_64 -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0
+//go:generate cmake -S ggml -B ggml/build/cpu -DLLAMA_ACCELERATE=on -DLLAMA_K_QUANTS=on -DCMAKE_SYSTEM_NAME=Darwin -DCMAKE_SYSTEM_PROCESSOR=x86_64 -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0
 //go:generate cmake --build ggml/build/cpu --target server --config Release
 //go:generate cmake --build ggml/build/cpu --target server --config Release
 //go:generate mv ggml/build/cpu/bin/server ggml/build/cpu/bin/ollama-runner
 //go:generate mv ggml/build/cpu/bin/server ggml/build/cpu/bin/ollama-runner
 
 
 //go:generate git submodule update --force gguf
 //go:generate git submodule update --force gguf
 //go:generate git -C gguf apply ../patches/0001-update-default-log-target.patch
 //go:generate git -C gguf apply ../patches/0001-update-default-log-target.patch
 //go:generate git -C gguf apply ../patches/0001-metal-handle-ggml_scale-for-n-4-0-close-3754.patch
 //go:generate git -C gguf apply ../patches/0001-metal-handle-ggml_scale-for-n-4-0-close-3754.patch
-//go:generate cmake -S gguf -B gguf/build/cpu -DLLAMA_METAL=off -DLLAMA_ACCELERATE=on -DLLAMA_K_QUANTS=on -DCMAKE_SYSTEM_PROCESSOR=x86_64 -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DLLAMA_NATIVE=off -DLLAMA_AVX=on -DLLAMA_AVX2=off -DLLAMA_AVX512=off -DLLAMA_FMA=off -DLLAMA_F16C=off
+//go:generate cmake -S gguf -B gguf/build/cpu -DLLAMA_METAL=off -DLLAMA_ACCELERATE=on -DLLAMA_K_QUANTS=on -DCMAKE_SYSTEM_NAME=Darwin -DCMAKE_SYSTEM_PROCESSOR=x86_64 -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DLLAMA_NATIVE=off -DLLAMA_AVX=on -DLLAMA_AVX2=on -DLLAMA_AVX512=off -DLLAMA_FMA=on -DLLAMA_F16C=on
 //go:generate cmake --build gguf/build/cpu --target server --config Release
 //go:generate cmake --build gguf/build/cpu --target server --config Release
 //go:generate mv gguf/build/cpu/bin/server gguf/build/cpu/bin/ollama-runner
 //go:generate mv gguf/build/cpu/bin/server gguf/build/cpu/bin/ollama-runner

+ 6 - 1
llm/llama.go

@@ -226,7 +226,7 @@ type llama struct {
 }
 }
 
 
 var (
 var (
-	errNvidiaSMI     = errors.New("nvidia-smi command failed")
+	errNvidiaSMI     = errors.New("warning: gpu support may not be enabled, check that you have installed GPU drivers: nvidia-smi command failed")
 	errAvailableVRAM = errors.New("not enough VRAM available, falling back to CPU only")
 	errAvailableVRAM = errors.New("not enough VRAM available, falling back to CPU only")
 )
 )
 
 
@@ -343,6 +343,10 @@ func newLlama(model string, adapters []string, runners []ModelRunner, numLayers
 		"--embedding",
 		"--embedding",
 	}
 	}
 
 
+	if opts.MainGPU > 0 {
+		params = append(params, "--main-gpu", fmt.Sprintf("%d", opts.MainGPU))
+	}
+
 	if opts.RopeFrequencyBase > 0 {
 	if opts.RopeFrequencyBase > 0 {
 		params = append(params, "--rope-freq-base", fmt.Sprintf("%f", opts.RopeFrequencyBase))
 		params = append(params, "--rope-freq-base", fmt.Sprintf("%f", opts.RopeFrequencyBase))
 	}
 	}
@@ -544,6 +548,7 @@ func (llm *llama) Predict(ctx context.Context, prevContext []int, prompt string,
 		"stream":            true,
 		"stream":            true,
 		"n_predict":         llm.NumPredict,
 		"n_predict":         llm.NumPredict,
 		"n_keep":            llm.NumKeep,
 		"n_keep":            llm.NumKeep,
+		"main_gpu":          llm.MainGPU,
 		"temperature":       llm.Temperature,
 		"temperature":       llm.Temperature,
 		"top_k":             llm.TopK,
 		"top_k":             llm.TopK,
 		"top_p":             llm.TopP,
 		"top_p":             llm.TopP,

+ 1 - 8
llm/llm.go

@@ -41,20 +41,13 @@ func New(workDir, model string, adapters []string, opts api.Options) (LLM, error
 
 
 	if runtime.GOOS == "darwin" {
 	if runtime.GOOS == "darwin" {
 		switch ggml.FileType() {
 		switch ggml.FileType() {
-		case "Q8_0":
+		case "F32", "Q5_0", "Q5_1", "Q8_0":
 			if ggml.Name() != "gguf" && opts.NumGPU != 0 {
 			if ggml.Name() != "gguf" && opts.NumGPU != 0 {
 				// GGML Q8_0 do not support Metal API and will
 				// GGML Q8_0 do not support Metal API and will
 				// cause the runner to segmentation fault so disable GPU
 				// cause the runner to segmentation fault so disable GPU
 				log.Printf("WARNING: GPU disabled for F32, Q5_0, Q5_1, and Q8_0")
 				log.Printf("WARNING: GPU disabled for F32, Q5_0, Q5_1, and Q8_0")
 				opts.NumGPU = 0
 				opts.NumGPU = 0
 			}
 			}
-		case "F32", "Q5_0", "Q5_1":
-			if opts.NumGPU != 0 {
-				// F32, Q5_0, Q5_1, and Q8_0 do not support Metal API and will
-				// cause the runner to segmentation fault so disable GPU
-				log.Printf("WARNING: GPU disabled for F32, Q5_0, Q5_1, and Q8_0")
-				opts.NumGPU = 0
-			}
 		}
 		}
 
 
 		var requiredMemory int64
 		var requiredMemory int64

+ 155 - 0
progress/bar.go

@@ -0,0 +1,155 @@
+package progress
+
+import (
+	"fmt"
+	"math"
+	"os"
+	"strings"
+	"time"
+
+	"github.com/jmorganca/ollama/format"
+	"golang.org/x/term"
+)
+
+type Stats struct {
+	rate      int64
+	value     int64
+	remaining time.Duration
+}
+
+type Bar struct {
+	message      string
+	messageWidth int
+
+	maxValue     int64
+	initialValue int64
+	currentValue int64
+
+	started time.Time
+
+	stats   Stats
+	statted time.Time
+}
+
+func NewBar(message string, maxValue, initialValue int64) *Bar {
+	return &Bar{
+		message:      message,
+		messageWidth: -1,
+		maxValue:     maxValue,
+		initialValue: initialValue,
+		currentValue: initialValue,
+		started:      time.Now(),
+	}
+}
+
+func (b *Bar) String() string {
+	termWidth, _, err := term.GetSize(int(os.Stderr.Fd()))
+	if err != nil {
+		termWidth = 80
+	}
+
+	var pre, mid, suf strings.Builder
+
+	if b.message != "" {
+		message := strings.TrimSpace(b.message)
+		if b.messageWidth > 0 && len(message) > b.messageWidth {
+			message = message[:b.messageWidth]
+		}
+
+		fmt.Fprintf(&pre, "%s", message)
+		if b.messageWidth-pre.Len() >= 0 {
+			pre.WriteString(strings.Repeat(" ", b.messageWidth-pre.Len()))
+		}
+
+		pre.WriteString(" ")
+	}
+
+	fmt.Fprintf(&pre, "%3.0f%% ", math.Floor(b.percent()))
+	fmt.Fprintf(&suf, "(%s/%s", format.HumanBytes(b.currentValue), format.HumanBytes(b.maxValue))
+
+	stats := b.Stats()
+	rate := int64(stats.rate)
+	if rate > 0 {
+		fmt.Fprintf(&suf, ", %s/s", format.HumanBytes(rate))
+	}
+
+	fmt.Fprintf(&suf, ")")
+
+	elapsed := time.Since(b.started)
+	if b.percent() < 100 && rate > 0 {
+		fmt.Fprintf(&suf, " [%s:%s]", elapsed.Round(time.Second), stats.remaining)
+	} else {
+		fmt.Fprintf(&suf, "        ")
+	}
+
+	mid.WriteString("▕")
+
+	// add 3 extra spaces: 2 boundary characters and 1 space at the end
+	f := termWidth - pre.Len() - suf.Len() - 3
+	n := int(float64(f) * b.percent() / 100)
+
+	if n > 0 {
+		mid.WriteString(strings.Repeat("█", n))
+	}
+
+	if f-n > 0 {
+		mid.WriteString(strings.Repeat(" ", f-n))
+	}
+
+	mid.WriteString("▏")
+
+	return pre.String() + mid.String() + suf.String()
+}
+
+func (b *Bar) Set(value int64) {
+	if value >= b.maxValue {
+		value = b.maxValue
+	}
+
+	b.currentValue = value
+}
+
+func (b *Bar) percent() float64 {
+	if b.maxValue > 0 {
+		return float64(b.currentValue) / float64(b.maxValue) * 100
+	}
+
+	return 0
+}
+
+func (b *Bar) Stats() Stats {
+	if time.Since(b.statted) < time.Second {
+		return b.stats
+	}
+
+	switch {
+	case b.statted.IsZero():
+		b.stats = Stats{
+			value:     b.initialValue,
+			rate:      0,
+			remaining: 0,
+		}
+	case b.currentValue >= b.maxValue:
+		b.stats = Stats{
+			value:     b.maxValue,
+			rate:      0,
+			remaining: 0,
+		}
+	default:
+		rate := b.currentValue - b.stats.value
+		var remaining time.Duration
+		if rate > 0 {
+			remaining = time.Second * time.Duration((float64(b.maxValue-b.currentValue))/(float64(rate)))
+		}
+
+		b.stats = Stats{
+			value:     b.currentValue,
+			rate:      rate,
+			remaining: remaining,
+		}
+	}
+
+	b.statted = time.Now()
+
+	return b.stats
+}

+ 113 - 0
progress/progress.go

@@ -0,0 +1,113 @@
+package progress
+
+import (
+	"fmt"
+	"io"
+	"sync"
+	"time"
+)
+
+type State interface {
+	String() string
+}
+
+type Progress struct {
+	mu sync.Mutex
+	w  io.Writer
+
+	pos int
+
+	ticker *time.Ticker
+	states []State
+}
+
+func NewProgress(w io.Writer) *Progress {
+	p := &Progress{w: w}
+	go p.start()
+	return p
+}
+
+func (p *Progress) stop() bool {
+	for _, state := range p.states {
+		if spinner, ok := state.(*Spinner); ok {
+			spinner.Stop()
+		}
+	}
+
+	if p.ticker != nil {
+		p.ticker.Stop()
+		p.ticker = nil
+		p.render()
+		return true
+	}
+
+	return false
+}
+
+func (p *Progress) Stop() bool {
+	stopped := p.stop()
+	if stopped {
+		fmt.Fprint(p.w, "\n")
+	}
+	return stopped
+}
+
+func (p *Progress) StopAndClear() bool {
+	fmt.Fprint(p.w, "\033[?25l")
+	defer fmt.Fprint(p.w, "\033[?25h")
+
+	stopped := p.stop()
+	if stopped {
+		// clear all progress lines
+		for i := 0; i < p.pos; i++ {
+			if i > 0 {
+				fmt.Fprint(p.w, "\033[A")
+			}
+			fmt.Fprint(p.w, "\033[2K\033[1G")
+		}
+	}
+
+	return stopped
+}
+
+func (p *Progress) Add(key string, state State) {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+
+	p.states = append(p.states, state)
+}
+
+func (p *Progress) render() error {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+
+	fmt.Fprint(p.w, "\033[?25l")
+	defer fmt.Fprint(p.w, "\033[?25h")
+
+	// clear already rendered progress lines
+	for i := 0; i < p.pos; i++ {
+		if i > 0 {
+			fmt.Fprint(p.w, "\033[A")
+		}
+		fmt.Fprint(p.w, "\033[2K\033[1G")
+	}
+
+	// render progress lines
+	for i, state := range p.states {
+		fmt.Fprint(p.w, state.String())
+		if i < len(p.states)-1 {
+			fmt.Fprint(p.w, "\n")
+		}
+	}
+
+	p.pos = len(p.states)
+
+	return nil
+}
+
+func (p *Progress) start() {
+	p.ticker = time.NewTicker(100 * time.Millisecond)
+	for range p.ticker.C {
+		p.render()
+	}
+}

+ 73 - 0
progress/spinner.go

@@ -0,0 +1,73 @@
+package progress
+
+import (
+	"fmt"
+	"strings"
+	"time"
+)
+
+type Spinner struct {
+	message      string
+	messageWidth int
+
+	parts []string
+
+	value int
+
+	ticker  *time.Ticker
+	started time.Time
+	stopped time.Time
+}
+
+func NewSpinner(message string) *Spinner {
+	s := &Spinner{
+		message: message,
+		parts: []string{
+			"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
+		},
+		started: time.Now(),
+	}
+	go s.start()
+	return s
+}
+
+func (s *Spinner) String() string {
+	var sb strings.Builder
+	if len(s.message) > 0 {
+		message := strings.TrimSpace(s.message)
+		if s.messageWidth > 0 && len(message) > s.messageWidth {
+			message = message[:s.messageWidth]
+		}
+
+		fmt.Fprintf(&sb, "%s", message)
+		if s.messageWidth-sb.Len() >= 0 {
+			sb.WriteString(strings.Repeat(" ", s.messageWidth-sb.Len()))
+		}
+
+		sb.WriteString(" ")
+	}
+
+	if s.stopped.IsZero() {
+		spinner := s.parts[s.value]
+		sb.WriteString(spinner)
+		sb.WriteString(" ")
+	}
+
+	return sb.String()
+}
+
+func (s *Spinner) start() {
+	s.ticker = time.NewTicker(100 * time.Millisecond)
+	for range s.ticker.C {
+		s.value = (s.value + 1) % len(s.parts)
+		if !s.stopped.IsZero() {
+			return
+		}
+	}
+}
+
+func (s *Spinner) Stop() {
+	if s.stopped.IsZero() {
+		s.stopped = time.Now()
+	}
+}

+ 0 - 21
progressbar/LICENSE

@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2017 Zack
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.

+ 0 - 121
progressbar/README.md

@@ -1,121 +0,0 @@
-# progressbar
-
-[![CI](https://github.com/schollz/progressbar/actions/workflows/ci.yml/badge.svg?branch=main&event=push)](https://github.com/schollz/progressbar/actions/workflows/ci.yml)
-[![go report card](https://goreportcard.com/badge/github.com/schollz/progressbar)](https://goreportcard.com/report/github.com/schollz/progressbar) 
-[![coverage](https://img.shields.io/badge/coverage-84%25-brightgreen.svg)](https://gocover.io/github.com/schollz/progressbar)
-[![godocs](https://godoc.org/github.com/schollz/progressbar?status.svg)](https://godoc.org/github.com/schollz/progressbar/v3) 
-
-A very simple thread-safe progress bar which should work on every OS without problems. I needed a progressbar for [croc](https://github.com/schollz/croc) and everything I tried had problems, so I made another one. In order to be OS agnostic I do not plan to support [multi-line outputs](https://github.com/schollz/progressbar/issues/6).
-
-
-## Install
-
-```
-go get -u github.com/schollz/progressbar/v3
-```
-
-## Usage 
-
-### Basic usage
-
-```golang
-bar := progressbar.Default(100)
-for i := 0; i < 100; i++ {
-    bar.Add(1)
-    time.Sleep(40 * time.Millisecond)
-}
-```
-
-which looks like:
-
-![Example of basic bar](examples/basic/basic.gif)
-
-
-### I/O operations
-
-The `progressbar` implements an `io.Writer` so it can automatically detect the number of bytes written to a stream, so you can use it as a progressbar for an `io.Reader`.
-
-```golang
-req, _ := http.NewRequest("GET", "https://dl.google.com/go/go1.14.2.src.tar.gz", nil)
-resp, _ := http.DefaultClient.Do(req)
-defer resp.Body.Close()
-
-f, _ := os.OpenFile("go1.14.2.src.tar.gz", os.O_CREATE|os.O_WRONLY, 0644)
-defer f.Close()
-
-bar := progressbar.DefaultBytes(
-    resp.ContentLength,
-    "downloading",
-)
-io.Copy(io.MultiWriter(f, bar), resp.Body)
-```
-
-which looks like:
-
-![Example of download bar](examples/download/download.gif)
-
-
-### Progress bar with unknown length
-
-A progressbar with unknown length is a spinner. Any bar with -1 length will automatically convert it to a spinner with a customizable spinner type. For example, the above code can be run and set the `resp.ContentLength` to `-1`.
-
-which looks like:
-
-![Example of download bar with unknown length](examples/download-unknown/download-unknown.gif)
-
-
-### Customization
-
-There is a lot of customization that you can do - change the writer, the color, the width, description, theme, etc. See [all the options](https://pkg.go.dev/github.com/schollz/progressbar/v3?tab=doc#Option).
-
-```golang
-bar := progressbar.NewOptions(1000,
-    progressbar.OptionSetWriter(ansi.NewAnsiStdout()),
-    progressbar.OptionEnableColorCodes(true),
-    progressbar.OptionShowBytes(true),
-    progressbar.OptionSetWidth(15),
-    progressbar.OptionSetDescription("[cyan][1/3][reset] Writing moshable file..."),
-    progressbar.OptionSetTheme(progressbar.Theme{
-        Saucer:        "[green]=[reset]",
-        SaucerHead:    "[green]>[reset]",
-        SaucerPadding: " ",
-        BarStart:      "[",
-        BarEnd:        "]",
-    }))
-for i := 0; i < 1000; i++ {
-    bar.Add(1)
-    time.Sleep(5 * time.Millisecond)
-}
-```
-
-which looks like:
-
-![Example of customized bar](examples/customization/customization.gif)
-
-
-## Contributing
-
-Pull requests are welcome. Feel free to...
-
-- Revise documentation
-- Add new features
-- Fix bugs
-- Suggest improvements
-
-## Thanks
-
-Thanks [@Dynom](https://github.com/dynom) for massive improvements in version 2.0!
-
-Thanks [@CrushedPixel](https://github.com/CrushedPixel) for adding descriptions and color code support!
-
-Thanks [@MrMe42](https://github.com/MrMe42) for adding some minor features!
-
-Thanks [@tehstun](https://github.com/tehstun) for some great PRs!
-
-Thanks [@Benzammour](https://github.com/Benzammour) and [@haseth](https://github.com/haseth) for helping create v3!
-
-Thanks [@briandowns](https://github.com/briandowns) for compiling the list of spinners.
-
-## License
-
-MIT

+ 0 - 1098
progressbar/progressbar.go

@@ -1,1098 +0,0 @@
-package progressbar
-
-import (
-	"errors"
-	"fmt"
-	"io"
-	"math"
-	"os"
-	"regexp"
-	"strings"
-	"sync"
-	"time"
-
-	"github.com/mattn/go-runewidth"
-	"github.com/mitchellh/colorstring"
-	"golang.org/x/term"
-)
-
-// ProgressBar is a thread-safe, simple
-// progress bar
-type ProgressBar struct {
-	state  state
-	config config
-	lock   sync.Mutex
-}
-
-// State is the basic properties of the bar
-type State struct {
-	CurrentPercent float64
-	CurrentBytes   float64
-	SecondsSince   float64
-	SecondsLeft    float64
-	KBsPerSecond   float64
-}
-
-type state struct {
-	currentNum        int64
-	currentPercent    int
-	lastPercent       int
-	currentSaucerSize int
-	isAltSaucerHead   bool
-
-	lastShown time.Time
-	startTime time.Time
-
-	counterTime         time.Time
-	counterNumSinceLast int64
-	counterLastTenRates []float64
-
-	maxLineWidth int
-	currentBytes float64
-	finished     bool
-	exit         bool // Progress bar exit halfway
-
-	rendered string
-}
-
-type config struct {
-	max                  int64 // max number of the counter
-	maxHumanized         string
-	maxHumanizedSuffix   string
-	width                int
-	writer               io.Writer
-	theme                Theme
-	renderWithBlankState bool
-	description          string
-	iterationString      string
-	ignoreLength         bool // ignoreLength if max bytes not known
-
-	// whether the output is expected to contain color codes
-	colorCodes bool
-
-	// show rate of change in kB/sec or MB/sec
-	showBytes bool
-	// show the iterations per second
-	showIterationsPerSecond bool
-	showIterationsCount     bool
-
-	// whether the progress bar should show elapsed time.
-	// always enabled if predictTime is true.
-	elapsedTime bool
-
-	showElapsedTimeOnFinish bool
-
-	// whether the progress bar should attempt to predict the finishing
-	// time of the progress based on the start time and the average
-	// number of seconds between  increments.
-	predictTime bool
-
-	// minimum time to wait in between updates
-	throttleDuration time.Duration
-
-	// clear bar once finished
-	clearOnFinish bool
-
-	// spinnerType should be a number between 0-75
-	spinnerType int
-
-	// spinnerTypeOptionUsed remembers if the spinnerType was changed manually
-	spinnerTypeOptionUsed bool
-
-	// spinner represents the spinner as a slice of string
-	spinner []string
-
-	// fullWidth specifies whether to measure and set the bar to a specific width
-	fullWidth bool
-
-	// invisible doesn't render the bar at all, useful for debugging
-	invisible bool
-
-	onCompletion func()
-
-	// whether the render function should make use of ANSI codes to reduce console I/O
-	useANSICodes bool
-
-	// showDescriptionAtLineEnd specifies whether description should be written at line end instead of line start
-	showDescriptionAtLineEnd bool
-}
-
-// Theme defines the elements of the bar
-type Theme struct {
-	Saucer        string
-	AltSaucerHead string
-	SaucerHead    string
-	SaucerPadding string
-	BarStart      string
-	BarEnd        string
-}
-
-// Option is the type all options need to adhere to
-type Option func(p *ProgressBar)
-
-// OptionSetWidth sets the width of the bar
-func OptionSetWidth(s int) Option {
-	return func(p *ProgressBar) {
-		p.config.width = s
-	}
-}
-
-// OptionSpinnerType sets the type of spinner used for indeterminate bars
-func OptionSpinnerType(spinnerType int) Option {
-	return func(p *ProgressBar) {
-		p.config.spinnerTypeOptionUsed = true
-		p.config.spinnerType = spinnerType
-	}
-}
-
-// OptionSpinnerCustom sets the spinner used for indeterminate bars to the passed
-// slice of string
-func OptionSpinnerCustom(spinner []string) Option {
-	return func(p *ProgressBar) {
-		p.config.spinner = spinner
-	}
-}
-
-// OptionSetTheme sets the elements the bar is constructed of
-func OptionSetTheme(t Theme) Option {
-	return func(p *ProgressBar) {
-		p.config.theme = t
-	}
-}
-
-// OptionSetVisibility sets the visibility
-func OptionSetVisibility(visibility bool) Option {
-	return func(p *ProgressBar) {
-		p.config.invisible = !visibility
-	}
-}
-
-// OptionFullWidth sets the bar to be full width
-func OptionFullWidth() Option {
-	return func(p *ProgressBar) {
-		p.config.fullWidth = true
-	}
-}
-
-// OptionSetWriter sets the output writer (defaults to os.StdOut)
-func OptionSetWriter(w io.Writer) Option {
-	return func(p *ProgressBar) {
-		p.config.writer = w
-	}
-}
-
-// OptionSetRenderBlankState sets whether or not to render a 0% bar on construction
-func OptionSetRenderBlankState(r bool) Option {
-	return func(p *ProgressBar) {
-		p.config.renderWithBlankState = r
-	}
-}
-
-// OptionSetDescription sets the description of the bar to render in front of it
-func OptionSetDescription(description string) Option {
-	return func(p *ProgressBar) {
-		p.config.description = description
-	}
-}
-
-// OptionEnableColorCodes enables or disables support for color codes
-// using mitchellh/colorstring
-func OptionEnableColorCodes(colorCodes bool) Option {
-	return func(p *ProgressBar) {
-		p.config.colorCodes = colorCodes
-	}
-}
-
-// OptionSetElapsedTime will enable elapsed time. Always enabled if OptionSetPredictTime is true.
-func OptionSetElapsedTime(elapsedTime bool) Option {
-	return func(p *ProgressBar) {
-		p.config.elapsedTime = elapsedTime
-	}
-}
-
-// OptionSetPredictTime will also attempt to predict the time remaining.
-func OptionSetPredictTime(predictTime bool) Option {
-	return func(p *ProgressBar) {
-		p.config.predictTime = predictTime
-	}
-}
-
-// OptionShowCount will also print current count out of total
-func OptionShowCount() Option {
-	return func(p *ProgressBar) {
-		p.config.showIterationsCount = true
-	}
-}
-
-// OptionShowIts will also print the iterations/second
-func OptionShowIts() Option {
-	return func(p *ProgressBar) {
-		p.config.showIterationsPerSecond = true
-	}
-}
-
-// OptionShowElapsedOnFinish will keep the display of elapsed time on finish
-func OptionShowElapsedTimeOnFinish() Option {
-	return func(p *ProgressBar) {
-		p.config.showElapsedTimeOnFinish = true
-	}
-}
-
-// OptionSetItsString sets what's displayed for iterations a second. The default is "it" which would display: "it/s"
-func OptionSetItsString(iterationString string) Option {
-	return func(p *ProgressBar) {
-		p.config.iterationString = iterationString
-	}
-}
-
-// OptionThrottle will wait the specified duration before updating again. The default
-// duration is 0 seconds.
-func OptionThrottle(duration time.Duration) Option {
-	return func(p *ProgressBar) {
-		p.config.throttleDuration = duration
-	}
-}
-
-// OptionClearOnFinish will clear the bar once its finished
-func OptionClearOnFinish() Option {
-	return func(p *ProgressBar) {
-		p.config.clearOnFinish = true
-	}
-}
-
-// OptionOnCompletion will invoke cmpl function once its finished
-func OptionOnCompletion(cmpl func()) Option {
-	return func(p *ProgressBar) {
-		p.config.onCompletion = cmpl
-	}
-}
-
-// OptionShowBytes will update the progress bar
-// configuration settings to display/hide kBytes/Sec
-func OptionShowBytes(val bool) Option {
-	return func(p *ProgressBar) {
-		p.config.showBytes = val
-	}
-}
-
-// OptionUseANSICodes will use more optimized terminal i/o.
-//
-// Only useful in environments with support for ANSI escape sequences.
-func OptionUseANSICodes(val bool) Option {
-	return func(p *ProgressBar) {
-		p.config.useANSICodes = val
-	}
-}
-
-// OptionShowDescriptionAtLineEnd defines whether description should be written at line end instead of line start
-func OptionShowDescriptionAtLineEnd() Option {
-	return func(p *ProgressBar) {
-		p.config.showDescriptionAtLineEnd = true
-	}
-}
-
-var defaultTheme = Theme{Saucer: "█", SaucerPadding: " ", BarStart: "▕", BarEnd: "▏"}
-
-// NewOptions constructs a new instance of ProgressBar, with any options you specify
-func NewOptions(max int, options ...Option) *ProgressBar {
-	return NewOptions64(int64(max), options...)
-}
-
-// NewOptions64 constructs a new instance of ProgressBar, with any options you specify
-func NewOptions64(max int64, options ...Option) *ProgressBar {
-	b := ProgressBar{
-		state: getBasicState(),
-		config: config{
-			writer:           os.Stdout,
-			theme:            defaultTheme,
-			iterationString:  "it",
-			width:            40,
-			max:              max,
-			throttleDuration: 0 * time.Nanosecond,
-			elapsedTime:      true,
-			predictTime:      true,
-			spinnerType:      9,
-			invisible:        false,
-		},
-	}
-
-	for _, o := range options {
-		o(&b)
-	}
-
-	if b.config.spinnerType < 0 || b.config.spinnerType > 75 {
-		panic("invalid spinner type, must be between 0 and 75")
-	}
-
-	// ignoreLength if max bytes not known
-	if b.config.max == -1 {
-		b.config.ignoreLength = true
-		b.config.max = int64(b.config.width)
-		b.config.predictTime = false
-	}
-
-	b.config.maxHumanized, b.config.maxHumanizedSuffix = humanizeBytes(float64(b.config.max))
-
-	if b.config.renderWithBlankState {
-		b.RenderBlank()
-	}
-
-	return &b
-}
-
-func getBasicState() state {
-	now := time.Now()
-	return state{
-		startTime:   now,
-		lastShown:   now,
-		counterTime: now,
-	}
-}
-
-// New returns a new ProgressBar
-// with the specified maximum
-func New(max int) *ProgressBar {
-	return NewOptions(max)
-}
-
-// DefaultBytes provides a progressbar to measure byte
-// throughput with recommended defaults.
-// Set maxBytes to -1 to use as a spinner.
-func DefaultBytes(maxBytes int64, description ...string) *ProgressBar {
-	desc := ""
-	if len(description) > 0 {
-		desc = description[0]
-	}
-	return NewOptions64(
-		maxBytes,
-		OptionSetDescription(desc),
-		OptionSetWriter(os.Stderr),
-		OptionShowBytes(true),
-		OptionSetWidth(10),
-		OptionThrottle(65*time.Millisecond),
-		OptionShowCount(),
-		OptionOnCompletion(func() {
-			fmt.Fprint(os.Stderr, "\n")
-		}),
-		OptionSpinnerType(14),
-		OptionFullWidth(),
-		OptionSetRenderBlankState(true),
-	)
-}
-
-// DefaultBytesSilent is the same as DefaultBytes, but does not output anywhere.
-// String() can be used to get the output instead.
-func DefaultBytesSilent(maxBytes int64, description ...string) *ProgressBar {
-	// Mostly the same bar as DefaultBytes
-
-	desc := ""
-	if len(description) > 0 {
-		desc = description[0]
-	}
-	return NewOptions64(
-		maxBytes,
-		OptionSetDescription(desc),
-		OptionSetWriter(io.Discard),
-		OptionShowBytes(true),
-		OptionSetWidth(10),
-		OptionThrottle(65*time.Millisecond),
-		OptionShowCount(),
-		OptionSpinnerType(14),
-		OptionFullWidth(),
-	)
-}
-
-// Default provides a progressbar with recommended defaults.
-// Set max to -1 to use as a spinner.
-func Default(max int64, description ...string) *ProgressBar {
-	desc := ""
-	if len(description) > 0 {
-		desc = description[0]
-	}
-	return NewOptions64(
-		max,
-		OptionSetDescription(desc),
-		OptionSetWriter(os.Stderr),
-		OptionSetWidth(10),
-		OptionThrottle(65*time.Millisecond),
-		OptionShowCount(),
-		OptionShowIts(),
-		OptionOnCompletion(func() {
-			fmt.Fprint(os.Stderr, "\n")
-		}),
-		OptionSpinnerType(14),
-		OptionFullWidth(),
-		OptionSetRenderBlankState(true),
-	)
-}
-
-// DefaultSilent is the same as Default, but does not output anywhere.
-// String() can be used to get the output instead.
-func DefaultSilent(max int64, description ...string) *ProgressBar {
-	// Mostly the same bar as Default
-
-	desc := ""
-	if len(description) > 0 {
-		desc = description[0]
-	}
-	return NewOptions64(
-		max,
-		OptionSetDescription(desc),
-		OptionSetWriter(io.Discard),
-		OptionSetWidth(10),
-		OptionThrottle(65*time.Millisecond),
-		OptionShowCount(),
-		OptionShowIts(),
-		OptionSpinnerType(14),
-		OptionFullWidth(),
-	)
-}
-
-// String returns the current rendered version of the progress bar.
-// It will never return an empty string while the progress bar is running.
-func (p *ProgressBar) String() string {
-	return p.state.rendered
-}
-
-// RenderBlank renders the current bar state, you can use this to render a 0% state
-func (p *ProgressBar) RenderBlank() error {
-	if p.config.invisible {
-		return nil
-	}
-	if p.state.currentNum == 0 {
-		p.state.lastShown = time.Time{}
-	}
-	return p.render()
-}
-
-// Reset will reset the clock that is used
-// to calculate current time and the time left.
-func (p *ProgressBar) Reset() {
-	p.lock.Lock()
-	defer p.lock.Unlock()
-
-	p.state = getBasicState()
-}
-
-// Finish will fill the bar to full
-func (p *ProgressBar) Finish() error {
-	p.lock.Lock()
-	p.state.currentNum = p.config.max
-	p.lock.Unlock()
-	return p.Add(0)
-}
-
-// Exit will exit the bar to keep current state
-func (p *ProgressBar) Exit() error {
-	p.lock.Lock()
-	defer p.lock.Unlock()
-
-	p.state.exit = true
-	if p.config.onCompletion != nil {
-		p.config.onCompletion()
-	}
-	return nil
-}
-
-// Add will add the specified amount to the progressbar
-func (p *ProgressBar) Add(num int) error {
-	return p.Add64(int64(num))
-}
-
-// Set will set the bar to a current number
-func (p *ProgressBar) Set(num int) error {
-	return p.Set64(int64(num))
-}
-
-// Set64 will set the bar to a current number
-func (p *ProgressBar) Set64(num int64) error {
-	p.lock.Lock()
-	toAdd := num - int64(p.state.currentBytes)
-	p.lock.Unlock()
-	return p.Add64(toAdd)
-}
-
-// Add64 will add the specified amount to the progressbar
-func (p *ProgressBar) Add64(num int64) error {
-	if p.config.invisible {
-		return nil
-	}
-	p.lock.Lock()
-	defer p.lock.Unlock()
-
-	if p.state.exit {
-		return nil
-	}
-
-	// error out since OptionSpinnerCustom will always override a manually set spinnerType
-	if p.config.spinnerTypeOptionUsed && len(p.config.spinner) > 0 {
-		return errors.New("OptionSpinnerType and OptionSpinnerCustom cannot be used together")
-	}
-
-	if p.config.max == 0 {
-		return errors.New("max must be greater than 0")
-	}
-
-	if p.state.currentNum < p.config.max {
-		if p.config.ignoreLength {
-			p.state.currentNum = (p.state.currentNum + num) % p.config.max
-		} else {
-			p.state.currentNum += num
-		}
-	}
-
-	p.state.currentBytes += float64(num)
-
-	// reset the countdown timer every second to take rolling average
-	p.state.counterNumSinceLast += num
-	if time.Since(p.state.counterTime).Seconds() > 0.5 {
-		p.state.counterLastTenRates = append(p.state.counterLastTenRates, float64(p.state.counterNumSinceLast)/time.Since(p.state.counterTime).Seconds())
-		if len(p.state.counterLastTenRates) > 10 {
-			p.state.counterLastTenRates = p.state.counterLastTenRates[1:]
-		}
-		p.state.counterTime = time.Now()
-		p.state.counterNumSinceLast = 0
-	}
-
-	percent := float64(p.state.currentNum) / float64(p.config.max)
-	p.state.currentSaucerSize = int(percent * float64(p.config.width))
-	p.state.currentPercent = int(percent * 100)
-	updateBar := p.state.currentPercent != p.state.lastPercent && p.state.currentPercent > 0
-
-	p.state.lastPercent = p.state.currentPercent
-	if p.state.currentNum > p.config.max {
-		return errors.New("current number exceeds max")
-	}
-
-	// always update if show bytes/second or its/second
-	if updateBar || p.config.showIterationsPerSecond || p.config.showIterationsCount {
-		return p.render()
-	}
-
-	return nil
-}
-
-// Clear erases the progress bar from the current line
-func (p *ProgressBar) Clear() error {
-	return clearProgressBar(p.config, p.state)
-}
-
-// Describe will change the description shown before the progress, which
-// can be changed on the fly (as for a slow running process).
-func (p *ProgressBar) Describe(description string) {
-	p.lock.Lock()
-	defer p.lock.Unlock()
-	p.config.description = description
-	if p.config.invisible {
-		return
-	}
-	p.render()
-}
-
-// New64 returns a new ProgressBar
-// with the specified maximum
-func New64(max int64) *ProgressBar {
-	return NewOptions64(max)
-}
-
-// GetMax returns the max of a bar
-func (p *ProgressBar) GetMax() int {
-	return int(p.config.max)
-}
-
-// GetMax64 returns the current max
-func (p *ProgressBar) GetMax64() int64 {
-	return p.config.max
-}
-
-// ChangeMax takes in a int
-// and changes the max value
-// of the progress bar
-func (p *ProgressBar) ChangeMax(newMax int) {
-	p.ChangeMax64(int64(newMax))
-}
-
-// ChangeMax64 is basically
-// the same as ChangeMax,
-// but takes in a int64
-// to avoid casting
-func (p *ProgressBar) ChangeMax64(newMax int64) {
-	p.config.max = newMax
-
-	if p.config.showBytes {
-		p.config.maxHumanized, p.config.maxHumanizedSuffix = humanizeBytes(float64(p.config.max))
-	}
-
-	p.Add(0) // re-render
-}
-
-// IsFinished returns true if progress bar is completed
-func (p *ProgressBar) IsFinished() bool {
-	return p.state.finished
-}
-
-// render renders the progress bar, updating the maximum
-// rendered line width. this function is not thread-safe,
-// so it must be called with an acquired lock.
-func (p *ProgressBar) render() error {
-	// make sure that the rendering is not happening too quickly
-	// but always show if the currentNum reaches the max
-	if time.Since(p.state.lastShown).Nanoseconds() < p.config.throttleDuration.Nanoseconds() &&
-		p.state.currentNum < p.config.max {
-		return nil
-	}
-
-	if !p.config.useANSICodes {
-		// first, clear the existing progress bar
-		err := clearProgressBar(p.config, p.state)
-		if err != nil {
-			return err
-		}
-	}
-
-	// check if the progress bar is finished
-	if !p.state.finished && p.state.currentNum >= p.config.max {
-		p.state.finished = true
-		if !p.config.clearOnFinish {
-			renderProgressBar(p.config, &p.state)
-		}
-		if p.config.onCompletion != nil {
-			p.config.onCompletion()
-		}
-	}
-	if p.state.finished {
-		// when using ANSI codes we don't pre-clean the current line
-		if p.config.useANSICodes && p.config.clearOnFinish {
-			err := clearProgressBar(p.config, p.state)
-			if err != nil {
-				return err
-			}
-		}
-		return nil
-	}
-
-	// then, re-render the current progress bar
-	w, err := renderProgressBar(p.config, &p.state)
-	if err != nil {
-		return err
-	}
-
-	if w > p.state.maxLineWidth {
-		p.state.maxLineWidth = w
-	}
-
-	p.state.lastShown = time.Now()
-
-	return nil
-}
-
-// State returns the current state
-func (p *ProgressBar) State() State {
-	p.lock.Lock()
-	defer p.lock.Unlock()
-	s := State{}
-	s.CurrentPercent = float64(p.state.currentNum) / float64(p.config.max)
-	s.CurrentBytes = p.state.currentBytes
-	s.SecondsSince = time.Since(p.state.startTime).Seconds()
-	if p.state.currentNum > 0 {
-		s.SecondsLeft = s.SecondsSince / float64(p.state.currentNum) * (float64(p.config.max) - float64(p.state.currentNum))
-	}
-	s.KBsPerSecond = float64(p.state.currentBytes) / 1000.0 / s.SecondsSince
-	return s
-}
-
-// regex matching ansi escape codes
-var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
-
-func getStringWidth(c config, str string, colorize bool) int {
-	if c.colorCodes {
-		// convert any color codes in the progress bar into the respective ANSI codes
-		str = colorstring.Color(str)
-	}
-
-	// the width of the string, if printed to the console
-	// does not include the carriage return character
-	cleanString := strings.Replace(str, "\r", "", -1)
-
-	if c.colorCodes {
-		// the ANSI codes for the colors do not take up space in the console output,
-		// so they do not count towards the output string width
-		cleanString = ansiRegex.ReplaceAllString(cleanString, "")
-	}
-
-	// get the amount of runes in the string instead of the
-	// character count of the string, as some runes span multiple characters.
-	// see https://stackoverflow.com/a/12668840/2733724
-	stringWidth := runewidth.StringWidth(cleanString)
-	return stringWidth
-}
-
-func renderProgressBar(c config, s *state) (int, error) {
-	var sb strings.Builder
-
-	averageRate := average(s.counterLastTenRates)
-	if len(s.counterLastTenRates) == 0 || s.finished {
-		// if no average samples, or if finished,
-		// then average rate should be the total rate
-		if t := time.Since(s.startTime).Seconds(); t > 0 {
-			averageRate = s.currentBytes / t
-		} else {
-			averageRate = 0
-		}
-	}
-
-	// show iteration count in "current/total" iterations format
-	if c.showIterationsCount {
-		if sb.Len() == 0 {
-			sb.WriteString("(")
-		} else {
-			sb.WriteString(", ")
-		}
-		if !c.ignoreLength {
-			if c.showBytes {
-				currentHumanize, currentSuffix := humanizeBytes(s.currentBytes)
-				if currentSuffix == c.maxHumanizedSuffix {
-					sb.WriteString(fmt.Sprintf("%s/%s%s",
-						currentHumanize, c.maxHumanized, c.maxHumanizedSuffix))
-				} else {
-					sb.WriteString(fmt.Sprintf("%s%s/%s%s",
-						currentHumanize, currentSuffix, c.maxHumanized, c.maxHumanizedSuffix))
-				}
-			} else {
-				sb.WriteString(fmt.Sprintf("%.0f/%d", s.currentBytes, c.max))
-			}
-		} else {
-			if c.showBytes {
-				currentHumanize, currentSuffix := humanizeBytes(s.currentBytes)
-				sb.WriteString(fmt.Sprintf("%s%s", currentHumanize, currentSuffix))
-			} else {
-				sb.WriteString(fmt.Sprintf("%.0f/%s", s.currentBytes, "-"))
-			}
-		}
-	}
-
-	// show rolling average rate
-	if c.showBytes && averageRate > 0 && !math.IsInf(averageRate, 1) {
-		if sb.Len() == 0 {
-			sb.WriteString("(")
-		} else {
-			sb.WriteString(", ")
-		}
-		currentHumanize, currentSuffix := humanizeBytes(averageRate)
-		sb.WriteString(fmt.Sprintf("%s%s/s", currentHumanize, currentSuffix))
-	}
-
-	// show iterations rate
-	if c.showIterationsPerSecond {
-		if sb.Len() == 0 {
-			sb.WriteString("(")
-		} else {
-			sb.WriteString(", ")
-		}
-		if averageRate > 1 {
-			sb.WriteString(fmt.Sprintf("%0.0f %s/s", averageRate, c.iterationString))
-		} else if averageRate*60 > 1 {
-			sb.WriteString(fmt.Sprintf("%0.0f %s/min", 60*averageRate, c.iterationString))
-		} else {
-			sb.WriteString(fmt.Sprintf("%0.0f %s/hr", 3600*averageRate, c.iterationString))
-		}
-	}
-	if sb.Len() > 0 {
-		sb.WriteString(")")
-	}
-
-	leftBrac, rightBrac, saucer, saucerHead := "", "", "", ""
-
-	// show time prediction in "current/total" seconds format
-	switch {
-	case c.predictTime:
-		rightBracNum := (time.Duration((1/averageRate)*(float64(c.max)-float64(s.currentNum))) * time.Second)
-		if rightBracNum.Seconds() < 0 {
-			rightBracNum = 0 * time.Second
-		}
-		rightBrac = rightBracNum.String()
-		fallthrough
-	case c.elapsedTime:
-		leftBrac = (time.Duration(time.Since(s.startTime).Seconds()) * time.Second).String()
-	}
-
-	if c.fullWidth && !c.ignoreLength {
-		width, err := termWidth()
-		if err != nil {
-			width = 80
-		}
-
-		amend := 1 // an extra space at eol
-		switch {
-		case leftBrac != "" && rightBrac != "":
-			amend = 4 // space, square brackets and colon
-		case leftBrac != "" && rightBrac == "":
-			amend = 4 // space and square brackets and another space
-		case leftBrac == "" && rightBrac != "":
-			amend = 3 // space and square brackets
-		}
-		if c.showDescriptionAtLineEnd {
-			amend += 1 // another space
-		}
-
-		c.width = width - getStringWidth(c, c.description, true) - 10 - amend - sb.Len() - len(leftBrac) - len(rightBrac)
-		s.currentSaucerSize = int(float64(s.currentPercent) / 100.0 * float64(c.width))
-	}
-	if s.currentSaucerSize > 0 {
-		if c.ignoreLength {
-			saucer = strings.Repeat(c.theme.SaucerPadding, s.currentSaucerSize-1)
-		} else {
-			saucer = strings.Repeat(c.theme.Saucer, s.currentSaucerSize-1)
-		}
-
-		// Check if an alternate saucer head is set for animation
-		if c.theme.AltSaucerHead != "" && s.isAltSaucerHead {
-			saucerHead = c.theme.AltSaucerHead
-			s.isAltSaucerHead = false
-		} else if c.theme.SaucerHead == "" || s.currentSaucerSize == c.width {
-			// use the saucer for the saucer head if it hasn't been set
-			// to preserve backwards compatibility
-			saucerHead = c.theme.Saucer
-		} else {
-			saucerHead = c.theme.SaucerHead
-			s.isAltSaucerHead = true
-		}
-	}
-
-	/*
-		Progress Bar format
-		Description % |------        |  (kb/s) (iteration count) (iteration rate) (predict time)
-
-		or if showDescriptionAtLineEnd is enabled
-		% |------        |  (kb/s) (iteration count) (iteration rate) (predict time) Description
-	*/
-
-	repeatAmount := c.width - s.currentSaucerSize
-	if repeatAmount < 0 {
-		repeatAmount = 0
-	}
-
-	str := ""
-
-	if c.ignoreLength {
-		selectedSpinner := spinners[c.spinnerType]
-		if len(c.spinner) > 0 {
-			selectedSpinner = c.spinner
-		}
-		spinner := selectedSpinner[int(math.Round(math.Mod(float64(time.Since(s.startTime).Milliseconds()/100), float64(len(selectedSpinner)))))]
-		if c.elapsedTime {
-			if c.showDescriptionAtLineEnd {
-				str = fmt.Sprintf("\r%s %s [%s] %s ",
-					spinner,
-					sb.String(),
-					leftBrac,
-					c.description)
-			} else {
-				str = fmt.Sprintf("\r%s %s %s [%s] ",
-					spinner,
-					c.description,
-					sb.String(),
-					leftBrac)
-			}
-		} else {
-			if c.showDescriptionAtLineEnd {
-				str = fmt.Sprintf("\r%s %s %s ",
-					spinner,
-					sb.String(),
-					c.description)
-			} else {
-				str = fmt.Sprintf("\r%s %s %s ",
-					spinner,
-					c.description,
-					sb.String())
-			}
-		}
-	} else if rightBrac == "" {
-		str = fmt.Sprintf("%4d%% %s%s%s%s%s %s",
-			s.currentPercent,
-			c.theme.BarStart,
-			saucer,
-			saucerHead,
-			strings.Repeat(c.theme.SaucerPadding, repeatAmount),
-			c.theme.BarEnd,
-			sb.String())
-
-		if s.currentPercent == 100 && c.showElapsedTimeOnFinish {
-			str = fmt.Sprintf("%s [%s]", str, leftBrac)
-		}
-
-		if c.showDescriptionAtLineEnd {
-			str = fmt.Sprintf("\r%s %s ", str, c.description)
-		} else {
-			str = fmt.Sprintf("\r%s%s ", c.description, str)
-		}
-	} else {
-		if s.currentPercent == 100 {
-			str = fmt.Sprintf("%4d%% %s%s%s%s%s %s",
-				s.currentPercent,
-				c.theme.BarStart,
-				saucer,
-				saucerHead,
-				strings.Repeat(c.theme.SaucerPadding, repeatAmount),
-				c.theme.BarEnd,
-				sb.String())
-
-			if c.showElapsedTimeOnFinish {
-				str = fmt.Sprintf("%s [%s]", str, leftBrac)
-			}
-
-			if c.showDescriptionAtLineEnd {
-				str = fmt.Sprintf("\r%s %s", str, c.description)
-			} else {
-				str = fmt.Sprintf("\r%s%s", c.description, str)
-			}
-		} else {
-			str = fmt.Sprintf("%4d%% %s%s%s%s%s %s [%s:%s]",
-				s.currentPercent,
-				c.theme.BarStart,
-				saucer,
-				saucerHead,
-				strings.Repeat(c.theme.SaucerPadding, repeatAmount),
-				c.theme.BarEnd,
-				sb.String(),
-				leftBrac,
-				rightBrac)
-
-			if c.showDescriptionAtLineEnd {
-				str = fmt.Sprintf("\r%s %s", str, c.description)
-			} else {
-				str = fmt.Sprintf("\r%s%s", c.description, str)
-			}
-		}
-	}
-
-	if c.colorCodes {
-		// convert any color codes in the progress bar into the respective ANSI codes
-		str = colorstring.Color(str)
-	}
-
-	s.rendered = str
-
-	return getStringWidth(c, str, false), writeString(c, str)
-}
-
-func clearProgressBar(c config, s state) error {
-	if s.maxLineWidth == 0 {
-		return nil
-	}
-	if c.useANSICodes {
-		// write the "clear current line" ANSI escape sequence
-		return writeString(c, "\033[2K\r")
-	}
-	// fill the empty content
-	// to overwrite the progress bar and jump
-	// back to the beginning of the line
-	str := fmt.Sprintf("\r%s\r", strings.Repeat(" ", s.maxLineWidth))
-	return writeString(c, str)
-	// the following does not show correctly if the previous line is longer than subsequent line
-	// return writeString(c, "\r")
-}
-
-func writeString(c config, str string) error {
-	if _, err := io.WriteString(c.writer, str); err != nil {
-		return err
-	}
-
-	if f, ok := c.writer.(*os.File); ok {
-		// ignore any errors in Sync(), as stdout
-		// can't be synced on some operating systems
-		// like Debian 9 (Stretch)
-		f.Sync()
-	}
-
-	return nil
-}
-
-// Reader is the progressbar io.Reader struct
-type Reader struct {
-	io.Reader
-	bar *ProgressBar
-}
-
-// NewReader return a new Reader with a given progress bar.
-func NewReader(r io.Reader, bar *ProgressBar) Reader {
-	return Reader{
-		Reader: r,
-		bar:    bar,
-	}
-}
-
-// Read will read the data and add the number of bytes to the progressbar
-func (r *Reader) Read(p []byte) (n int, err error) {
-	n, err = r.Reader.Read(p)
-	r.bar.Add(n)
-	return
-}
-
-// Close the reader when it implements io.Closer
-func (r *Reader) Close() (err error) {
-	if closer, ok := r.Reader.(io.Closer); ok {
-		return closer.Close()
-	}
-	r.bar.Finish()
-	return
-}
-
-// Write implement io.Writer
-func (p *ProgressBar) Write(b []byte) (n int, err error) {
-	n = len(b)
-	p.Add(n)
-	return
-}
-
-// Read implement io.Reader
-func (p *ProgressBar) Read(b []byte) (n int, err error) {
-	n = len(b)
-	p.Add(n)
-	return
-}
-
-func (p *ProgressBar) Close() (err error) {
-	p.Finish()
-	return
-}
-
-func average(xs []float64) float64 {
-	total := 0.0
-	for _, v := range xs {
-		total += v
-	}
-	return total / float64(len(xs))
-}
-
-func humanizeBytes(s float64) (string, string) {
-	sizes := []string{" B", " kB", " MB", " GB", " TB", " PB", " EB"}
-	base := 1000.0
-	if s < 10 {
-		return fmt.Sprintf("%2.0f", s), sizes[0]
-	}
-	e := math.Floor(logn(float64(s), base))
-	suffix := sizes[int(e)]
-	val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
-	f := "%.0f"
-	if val < 10 {
-		f = "%.1f"
-	}
-
-	return fmt.Sprintf(f, val), suffix
-}
-
-func logn(n, b float64) float64 {
-	return math.Log(n) / math.Log(b)
-}
-
-// termWidth function returns the visible width of the current terminal
-// and can be redefined for testing
-var termWidth = func() (width int, err error) {
-	width, _, err = term.GetSize(int(os.Stdout.Fd()))
-	if err == nil {
-		return width, nil
-	}
-
-	return 0, err
-}

+ 0 - 80
progressbar/spinners.go

@@ -1,80 +0,0 @@
-package progressbar
-
-var spinners = map[int][]string{
-	0:  {"←", "↖", "↑", "↗", "→", "↘", "↓", "↙"},
-	1:  {"▁", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▁"},
-	2:  {"▖", "▘", "▝", "▗"},
-	3:  {"┤", "┘", "┴", "└", "├", "┌", "┬", "┐"},
-	4:  {"◢", "◣", "◤", "◥"},
-	5:  {"◰", "◳", "◲", "◱"},
-	6:  {"◴", "◷", "◶", "◵"},
-	7:  {"◐", "◓", "◑", "◒"},
-	8:  {".", "o", "O", "@", "*"},
-	9:  {"|", "/", "-", "\\"},
-	10: {"◡◡", "⊙⊙", "◠◠"},
-	11: {"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"},
-	12: {">))'>", " >))'>", "  >))'>", "   >))'>", "    >))'>", "   <'((<", "  <'((<", " <'((<"},
-	13: {"⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"},
-	14: {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
-	15: {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"},
-	16: {"▉", "▊", "▋", "▌", "▍", "▎", "▏", "▎", "▍", "▌", "▋", "▊", "▉"},
-	17: {"■", "□", "▪", "▫"},
-	18: {"←", "↑", "→", "↓"},
-	19: {"╫", "╪"},
-	20: {"⇐", "⇖", "⇑", "⇗", "⇒", "⇘", "⇓", "⇙"},
-	21: {"⠁", "⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠠", "⠠", "⠤", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈", "⠈"},
-	22: {"⠈", "⠉", "⠋", "⠓", "⠒", "⠐", "⠐", "⠒", "⠖", "⠦", "⠤", "⠠", "⠠", "⠤", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈"},
-	23: {"⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠴", "⠲", "⠒", "⠂", "⠂", "⠒", "⠚", "⠙", "⠉", "⠁"},
-	24: {"⠋", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋"},
-	25: {"ヲ", "ァ", "ィ", "ゥ", "ェ", "ォ", "ャ", "ュ", "ョ", "ッ", "ア", "イ", "ウ", "エ", "オ", "カ", "キ", "ク", "ケ", "コ", "サ", "シ", "ス", "セ", "ソ", "タ", "チ", "ツ", "テ", "ト", "ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ヘ", "ホ", "マ", "ミ", "ム", "メ", "モ", "ヤ", "ユ", "ヨ", "ラ", "リ", "ル", "レ", "ロ", "ワ", "ン"},
-	26: {".", "..", "..."},
-	27: {"▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▉", "▊", "▋", "▌", "▍", "▎", "▏", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█", "▇", "▆", "▅", "▄", "▃", "▂", "▁"},
-	28: {".", "o", "O", "°", "O", "o", "."},
-	29: {"+", "x"},
-	30: {"v", "<", "^", ">"},
-	31: {">>--->", " >>--->", "  >>--->", "   >>--->", "    >>--->", "    <---<<", "   <---<<", "  <---<<", " <---<<", "<---<<"},
-	32: {"|", "||", "|||", "||||", "|||||", "|||||||", "||||||||", "|||||||", "||||||", "|||||", "||||", "|||", "||", "|"},
-	33: {"[          ]", "[=         ]", "[==        ]", "[===       ]", "[====      ]", "[=====     ]", "[======    ]", "[=======   ]", "[========  ]", "[========= ]", "[==========]"},
-	34: {"(*---------)", "(-*--------)", "(--*-------)", "(---*------)", "(----*-----)", "(-----*----)", "(------*---)", "(-------*--)", "(--------*-)", "(---------*)"},
-	35: {"█▒▒▒▒▒▒▒▒▒", "███▒▒▒▒▒▒▒", "█████▒▒▒▒▒", "███████▒▒▒", "██████████"},
-	36: {"[                    ]", "[=>                  ]", "[===>                ]", "[=====>              ]", "[======>             ]", "[========>           ]", "[==========>         ]", "[============>       ]", "[==============>     ]", "[================>   ]", "[==================> ]", "[===================>]"},
-	37: {"ဝ", "၀"},
-	38: {"▌", "▀", "▐▄"},
-	39: {"🌍", "🌎", "🌏"},
-	40: {"◜", "◝", "◞", "◟"},
-	41: {"⬒", "⬔", "⬓", "⬕"},
-	42: {"⬖", "⬘", "⬗", "⬙"},
-	43: {"[>>>          >]", "[]>>>>        []", "[]  >>>>      []", "[]    >>>>    []", "[]      >>>>  []", "[]        >>>>[]", "[>>          >>]"},
-	44: {"♠", "♣", "♥", "♦"},
-	45: {"➞", "➟", "➠", "➡", "➠", "➟"},
-	46: {"  |  ", ` \   `, "_    ", ` \   `, "  |  ", "   / ", "    _", "   / "},
-	47: {"  . . . .", ".   . . .", ". .   . .", ". . .   .", ". . . .  ", ". . . . ."},
-	48: {" |     ", "  /    ", "   _   ", `    \  `, "     | ", `    \  `, "   _   ", "  /    "},
-	49: {"⎺", "⎻", "⎼", "⎽", "⎼", "⎻"},
-	50: {"▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸"},
-	51: {"[    ]", "[   =]", "[  ==]", "[ ===]", "[====]", "[=== ]", "[==  ]", "[=   ]"},
-	52: {"( ●    )", "(  ●   )", "(   ●  )", "(    ● )", "(     ●)", "(    ● )", "(   ●  )", "(  ●   )", "( ●    )"},
-	53: {"✶", "✸", "✹", "✺", "✹", "✷"},
-	54: {"▐|\\____________▌", "▐_|\\___________▌", "▐__|\\__________▌", "▐___|\\_________▌", "▐____|\\________▌", "▐_____|\\_______▌", "▐______|\\______▌", "▐_______|\\_____▌", "▐________|\\____▌", "▐_________|\\___▌", "▐__________|\\__▌", "▐___________|\\_▌", "▐____________|\\▌", "▐____________/|▌", "▐___________/|_▌", "▐__________/|__▌", "▐_________/|___▌", "▐________/|____▌", "▐_______/|_____▌", "▐______/|______▌", "▐_____/|_______▌", "▐____/|________▌", "▐___/|_________▌", "▐__/|__________▌", "▐_/|___________▌", "▐/|____________▌"},
-	55: {"▐⠂       ▌", "▐⠈       ▌", "▐ ⠂      ▌", "▐ ⠠      ▌", "▐  ⡀     ▌", "▐  ⠠     ▌", "▐   ⠂    ▌", "▐   ⠈    ▌", "▐    ⠂   ▌", "▐    ⠠   ▌", "▐     ⡀  ▌", "▐     ⠠  ▌", "▐      ⠂ ▌", "▐      ⠈ ▌", "▐       ⠂▌", "▐       ⠠▌", "▐       ⡀▌", "▐      ⠠ ▌", "▐      ⠂ ▌", "▐     ⠈  ▌", "▐     ⠂  ▌", "▐    ⠠   ▌", "▐    ⡀   ▌", "▐   ⠠    ▌", "▐   ⠂    ▌", "▐  ⠈     ▌", "▐  ⠂     ▌", "▐ ⠠      ▌", "▐ ⡀      ▌", "▐⠠       ▌"},
-	56: {"¿", "?"},
-	57: {"⢹", "⢺", "⢼", "⣸", "⣇", "⡧", "⡗", "⡏"},
-	58: {"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"},
-	59: {".  ", ".. ", "...", " ..", "  .", "   "},
-	60: {".", "o", "O", "°", "O", "o", "."},
-	61: {"▓", "▒", "░"},
-	62: {"▌", "▀", "▐", "▄"},
-	63: {"⊶", "⊷"},
-	64: {"▪", "▫"},
-	65: {"□", "■"},
-	66: {"▮", "▯"},
-	67: {"-", "=", "≡"},
-	68: {"d", "q", "p", "b"},
-	69: {"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"},
-	70: {"🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "},
-	71: {"☗", "☖"},
-	72: {"⧇", "⧆"},
-	73: {"◉", "◎"},
-	74: {"㊂", "㊀", "㊁"},
-	75: {"⦾", "⦿"},
-}

+ 2 - 0
scripts/build_docker.sh

@@ -10,6 +10,8 @@ docker buildx build \
     --platform=linux/arm64,linux/amd64 \
     --platform=linux/arm64,linux/amd64 \
     --build-arg=VERSION \
     --build-arg=VERSION \
     --build-arg=GOFLAGS \
     --build-arg=GOFLAGS \
+    --cache-from type=local,src=.cache \
+    --cache-to type=local,dest=.cache \
     -f Dockerfile \
     -f Dockerfile \
     -t ollama \
     -t ollama \
     .
     .

+ 1 - 0
scripts/push_docker.sh

@@ -10,6 +10,7 @@ docker buildx build \
     --platform=linux/arm64,linux/amd64 \
     --platform=linux/arm64,linux/amd64 \
     --build-arg=VERSION \
     --build-arg=VERSION \
     --build-arg=GOFLAGS \
     --build-arg=GOFLAGS \
+    --cache-from type=local,src=.cache \
     -f Dockerfile \
     -f Dockerfile \
     -t ollama/ollama -t ollama/ollama:$VERSION \
     -t ollama/ollama -t ollama/ollama:$VERSION \
     .
     .

+ 9 - 10
server/download.go

@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"log"
 	"log"
+	"math"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"os"
 	"os"
@@ -53,8 +54,8 @@ type blobDownloadPart struct {
 
 
 const (
 const (
 	numDownloadParts          = 64
 	numDownloadParts          = 64
-	minDownloadPartSize int64 = 32 * 1000 * 1000
-	maxDownloadPartSize int64 = 256 * 1000 * 1000
+	minDownloadPartSize int64 = 100 * format.MegaByte
+	maxDownloadPartSize int64 = 1000 * format.MegaByte
 )
 )
 
 
 func (p *blobDownloadPart) Name() string {
 func (p *blobDownloadPart) Name() string {
@@ -147,7 +148,6 @@ func (b *blobDownload) run(ctx context.Context, requestURL *url.URL, opts *Regis
 			continue
 			continue
 		}
 		}
 
 
-		i := i
 		g.Go(func() error {
 		g.Go(func() error {
 			var err error
 			var err error
 			for try := 0; try < maxRetries; try++ {
 			for try := 0; try < maxRetries; try++ {
@@ -158,12 +158,11 @@ func (b *blobDownload) run(ctx context.Context, requestURL *url.URL, opts *Regis
 					// return immediately if the context is canceled or the device is out of space
 					// return immediately if the context is canceled or the device is out of space
 					return err
 					return err
 				case err != nil:
 				case err != nil:
-					log.Printf("%s part %d attempt %d failed: %v, retrying", b.Digest[7:19], i, try, err)
+					sleep := time.Second * time.Duration(math.Pow(2, float64(try)))
+					log.Printf("%s part %d attempt %d failed: %v, retrying in %s", b.Digest[7:19], part.N, try, err, sleep)
+					time.Sleep(sleep)
 					continue
 					continue
 				default:
 				default:
-					if try > 0 {
-						log.Printf("%s part %d completed after %d retries", b.Digest[7:19], i, try)
-					}
 					return nil
 					return nil
 				}
 				}
 			}
 			}
@@ -285,7 +284,7 @@ func (b *blobDownload) Wait(ctx context.Context, fn func(api.ProgressResponse))
 		}
 		}
 
 
 		fn(api.ProgressResponse{
 		fn(api.ProgressResponse{
-			Status:    fmt.Sprintf("downloading %s", b.Digest),
+			Status:    fmt.Sprintf("pulling %s", b.Digest[7:19]),
 			Digest:    b.Digest,
 			Digest:    b.Digest,
 			Total:     b.Total,
 			Total:     b.Total,
 			Completed: b.Completed.Load(),
 			Completed: b.Completed.Load(),
@@ -304,7 +303,7 @@ type downloadOpts struct {
 	fn      func(api.ProgressResponse)
 	fn      func(api.ProgressResponse)
 }
 }
 
 
-const maxRetries = 3
+const maxRetries = 6
 
 
 var errMaxRetriesExceeded = errors.New("max retries exceeded")
 var errMaxRetriesExceeded = errors.New("max retries exceeded")
 
 
@@ -322,7 +321,7 @@ func downloadBlob(ctx context.Context, opts downloadOpts) error {
 		return err
 		return err
 	default:
 	default:
 		opts.fn(api.ProgressResponse{
 		opts.fn(api.ProgressResponse{
-			Status:    fmt.Sprintf("downloading %s", opts.digest),
+			Status:    fmt.Sprintf("pulling %s", opts.digest[7:19]),
 			Digest:    opts.digest,
 			Digest:    opts.digest,
 			Total:     fi.Size(),
 			Total:     fi.Size(),
 			Completed: fi.Size(),
 			Completed: fi.Size(),

+ 32 - 46
server/images.go

@@ -228,26 +228,6 @@ func GetModel(name string) (*Model, error) {
 	return model, nil
 	return model, nil
 }
 }
 
 
-func filenameWithPath(path, f string) (string, error) {
-	// if filePath starts with ~/, replace it with the user's home directory.
-	if strings.HasPrefix(f, fmt.Sprintf("~%s", string(os.PathSeparator))) {
-		parts := strings.Split(f, string(os.PathSeparator))
-		home, err := os.UserHomeDir()
-		if err != nil {
-			return "", fmt.Errorf("failed to open file: %v", err)
-		}
-
-		f = filepath.Join(home, filepath.Join(parts[1:]...))
-	}
-
-	// if filePath is not an absolute path, make it relative to the modelfile path
-	if !filepath.IsAbs(f) {
-		f = filepath.Join(filepath.Dir(path), f)
-	}
-
-	return f, nil
-}
-
 func realpath(p string) string {
 func realpath(p string) string {
 	abspath, err := filepath.Abs(p)
 	abspath, err := filepath.Abs(p)
 	if err != nil {
 	if err != nil {
@@ -1146,43 +1126,49 @@ func GetSHA256Digest(r io.Reader) (string, int64) {
 var errUnauthorized = fmt.Errorf("unauthorized")
 var errUnauthorized = fmt.Errorf("unauthorized")
 
 
 func makeRequestWithRetry(ctx context.Context, method string, requestURL *url.URL, headers http.Header, body io.ReadSeeker, regOpts *RegistryOptions) (*http.Response, error) {
 func makeRequestWithRetry(ctx context.Context, method string, requestURL *url.URL, headers http.Header, body io.ReadSeeker, regOpts *RegistryOptions) (*http.Response, error) {
-	lastErr := errMaxRetriesExceeded
-	for try := 0; try < maxRetries; try++ {
-		resp, err := makeRequest(ctx, method, requestURL, headers, body, regOpts)
+	resp, err := makeRequest(ctx, method, requestURL, headers, body, regOpts)
+	if err != nil {
+		if !errors.Is(err, context.Canceled) {
+			log.Printf("request failed: %v", err)
+		}
+
+		return nil, err
+	}
+
+	switch {
+	case resp.StatusCode == http.StatusUnauthorized:
+		// Handle authentication error with one retry
+		auth := resp.Header.Get("www-authenticate")
+		authRedir := ParseAuthRedirectString(auth)
+		token, err := getAuthToken(ctx, authRedir)
 		if err != nil {
 		if err != nil {
-			log.Printf("couldn't start upload: %v", err)
 			return nil, err
 			return nil, err
 		}
 		}
-
-		switch {
-		case resp.StatusCode == http.StatusUnauthorized:
-			auth := resp.Header.Get("www-authenticate")
-			authRedir := ParseAuthRedirectString(auth)
-			token, err := getAuthToken(ctx, authRedir)
+		regOpts.Token = token
+		if body != nil {
+			_, err = body.Seek(0, io.SeekStart)
 			if err != nil {
 			if err != nil {
 				return nil, err
 				return nil, err
 			}
 			}
+		}
 
 
-			regOpts.Token = token
-			if body != nil {
-				body.Seek(0, io.SeekStart)
-			}
-			lastErr = errUnauthorized
-		case resp.StatusCode == http.StatusNotFound:
-			return nil, os.ErrNotExist
-		case resp.StatusCode >= http.StatusBadRequest:
-			body, err := io.ReadAll(resp.Body)
-			if err != nil {
-				return nil, fmt.Errorf("%d: %s", resp.StatusCode, err)
-			}
+		resp, err := makeRequest(ctx, method, requestURL, headers, body, regOpts)
+		if resp.StatusCode == http.StatusUnauthorized {
+			return nil, errUnauthorized
+		}
 
 
-			return nil, fmt.Errorf("%d: %s", resp.StatusCode, body)
-		default:
-			return resp, nil
+		return resp, err
+	case resp.StatusCode == http.StatusNotFound:
+		return nil, os.ErrNotExist
+	case resp.StatusCode >= http.StatusBadRequest:
+		responseBody, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return nil, fmt.Errorf("%d: %s", resp.StatusCode, err)
 		}
 		}
+		return nil, fmt.Errorf("%d: %s", resp.StatusCode, responseBody)
 	}
 	}
 
 
-	return nil, lastErr
+	return resp, nil
 }
 }
 
 
 func makeRequest(ctx context.Context, method string, requestURL *url.URL, headers http.Header, body io.Reader, regOpts *RegistryOptions) (*http.Response, error) {
 func makeRequest(ctx context.Context, method string, requestURL *url.URL, headers http.Header, body io.Reader, regOpts *RegistryOptions) (*http.Response, error) {

+ 8 - 8
server/routes.go

@@ -666,8 +666,14 @@ func HeadBlobHandler(c *gin.Context) {
 }
 }
 
 
 func CreateBlobHandler(c *gin.Context) {
 func CreateBlobHandler(c *gin.Context) {
+	targetPath, err := GetBlobsPath(c.Param("digest"))
+	if err != nil {
+		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+		return
+	}
+
 	hash := sha256.New()
 	hash := sha256.New()
-	temp, err := os.CreateTemp("", c.Param("digest"))
+	temp, err := os.CreateTemp(filepath.Dir(targetPath), c.Param("digest")+"-")
 	if err != nil {
 	if err != nil {
 		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
 		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
 		return
 		return
@@ -690,12 +696,6 @@ func CreateBlobHandler(c *gin.Context) {
 		return
 		return
 	}
 	}
 
 
-	targetPath, err := GetBlobsPath(c.Param("digest"))
-	if err != nil {
-		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
-		return
-	}
-
 	if err := os.Rename(temp.Name(), targetPath); err != nil {
 	if err := os.Rename(temp.Name(), targetPath); err != nil {
 		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
 		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
 		return
 		return
@@ -794,7 +794,7 @@ func Serve(ln net.Listener, allowOrigins []string) error {
 	if runtime.GOOS == "linux" {
 	if runtime.GOOS == "linux" {
 		// check compatibility to log warnings
 		// check compatibility to log warnings
 		if _, err := llm.CheckVRAM(); err != nil {
 		if _, err := llm.CheckVRAM(); err != nil {
-			log.Printf("Warning: GPU support may not be enabled, check you have installed GPU drivers: %v", err)
+			log.Printf(err.Error())
 		}
 		}
 	}
 	}
 
 

+ 50 - 30
server/upload.go

@@ -5,9 +5,9 @@ import (
 	"crypto/md5"
 	"crypto/md5"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
-	"hash"
 	"io"
 	"io"
 	"log"
 	"log"
+	"math"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"os"
 	"os"
@@ -35,6 +35,8 @@ type blobUpload struct {
 
 
 	context.CancelFunc
 	context.CancelFunc
 
 
+	file *os.File
+
 	done       bool
 	done       bool
 	err        error
 	err        error
 	references atomic.Int32
 	references atomic.Int32
@@ -42,8 +44,8 @@ type blobUpload struct {
 
 
 const (
 const (
 	numUploadParts          = 64
 	numUploadParts          = 64
-	minUploadPartSize int64 = 95 * 1000 * 1000
-	maxUploadPartSize int64 = 1000 * 1000 * 1000
+	minUploadPartSize int64 = 100 * format.MegaByte
+	maxUploadPartSize int64 = 1000 * format.MegaByte
 )
 )
 
 
 func (b *blobUpload) Prepare(ctx context.Context, requestURL *url.URL, opts *RegistryOptions) error {
 func (b *blobUpload) Prepare(ctx context.Context, requestURL *url.URL, opts *RegistryOptions) error {
@@ -128,12 +130,12 @@ func (b *blobUpload) Run(ctx context.Context, opts *RegistryOptions) {
 		return
 		return
 	}
 	}
 
 
-	f, err := os.Open(p)
+	b.file, err = os.Open(p)
 	if err != nil {
 	if err != nil {
 		b.err = err
 		b.err = err
 		return
 		return
 	}
 	}
-	defer f.Close()
+	defer b.file.Close()
 
 
 	g, inner := errgroup.WithContext(ctx)
 	g, inner := errgroup.WithContext(ctx)
 	g.SetLimit(numUploadParts)
 	g.SetLimit(numUploadParts)
@@ -145,7 +147,6 @@ func (b *blobUpload) Run(ctx context.Context, opts *RegistryOptions) {
 			g.Go(func() error {
 			g.Go(func() error {
 				var err error
 				var err error
 				for try := 0; try < maxRetries; try++ {
 				for try := 0; try < maxRetries; try++ {
-					part.ReadSeeker = io.NewSectionReader(f, part.Offset, part.Size)
 					err = b.uploadChunk(inner, http.MethodPatch, requestURL, part, opts)
 					err = b.uploadChunk(inner, http.MethodPatch, requestURL, part, opts)
 					switch {
 					switch {
 					case errors.Is(err, context.Canceled):
 					case errors.Is(err, context.Canceled):
@@ -153,7 +154,10 @@ func (b *blobUpload) Run(ctx context.Context, opts *RegistryOptions) {
 					case errors.Is(err, errMaxRetriesExceeded):
 					case errors.Is(err, errMaxRetriesExceeded):
 						return err
 						return err
 					case err != nil:
 					case err != nil:
-						log.Printf("%s part %d attempt %d failed: %v, retrying", b.Digest[7:19], part.N, try, err)
+						part.Reset()
+						sleep := time.Second * time.Duration(math.Pow(2, float64(try)))
+						log.Printf("%s part %d attempt %d failed: %v, retrying in %s", b.Digest[7:19], part.N, try, err, sleep)
+						time.Sleep(sleep)
 						continue
 						continue
 					}
 					}
 
 
@@ -173,8 +177,16 @@ func (b *blobUpload) Run(ctx context.Context, opts *RegistryOptions) {
 	requestURL := <-b.nextURL
 	requestURL := <-b.nextURL
 
 
 	var sb strings.Builder
 	var sb strings.Builder
+
+	// calculate md5 checksum and add it to the commit request
 	for _, part := range b.Parts {
 	for _, part := range b.Parts {
-		sb.Write(part.Sum(nil))
+		hash := md5.New()
+		if _, err := io.Copy(hash, io.NewSectionReader(b.file, part.Offset, part.Size)); err != nil {
+			b.err = err
+			return
+		}
+
+		sb.Write(hash.Sum(nil))
 	}
 	}
 
 
 	md5sum := md5.Sum([]byte(sb.String()))
 	md5sum := md5.Sum([]byte(sb.String()))
@@ -188,29 +200,39 @@ func (b *blobUpload) Run(ctx context.Context, opts *RegistryOptions) {
 	headers.Set("Content-Type", "application/octet-stream")
 	headers.Set("Content-Type", "application/octet-stream")
 	headers.Set("Content-Length", "0")
 	headers.Set("Content-Length", "0")
 
 
-	resp, err := makeRequestWithRetry(ctx, http.MethodPut, requestURL, headers, nil, opts)
-	if err != nil {
-		b.err = err
+	for try := 0; try < maxRetries; try++ {
+		resp, err := makeRequestWithRetry(ctx, http.MethodPut, requestURL, headers, nil, opts)
+		if err != nil {
+			b.err = err
+			if errors.Is(err, context.Canceled) {
+				return
+			}
+
+			sleep := time.Second * time.Duration(math.Pow(2, float64(try)))
+			log.Printf("%s complete upload attempt %d failed: %v, retrying in %s", b.Digest[7:19], try, err, sleep)
+			time.Sleep(sleep)
+			continue
+		}
+		defer resp.Body.Close()
+
+		b.err = nil
+		b.done = true
 		return
 		return
 	}
 	}
-	defer resp.Body.Close()
-
-	b.done = true
 }
 }
 
 
 func (b *blobUpload) uploadChunk(ctx context.Context, method string, requestURL *url.URL, part *blobUploadPart, opts *RegistryOptions) error {
 func (b *blobUpload) uploadChunk(ctx context.Context, method string, requestURL *url.URL, part *blobUploadPart, opts *RegistryOptions) error {
-	part.Reset()
-
 	headers := make(http.Header)
 	headers := make(http.Header)
 	headers.Set("Content-Type", "application/octet-stream")
 	headers.Set("Content-Type", "application/octet-stream")
 	headers.Set("Content-Length", fmt.Sprintf("%d", part.Size))
 	headers.Set("Content-Length", fmt.Sprintf("%d", part.Size))
-	headers.Set("X-Redirect-Uploads", "1")
 
 
 	if method == http.MethodPatch {
 	if method == http.MethodPatch {
+		headers.Set("X-Redirect-Uploads", "1")
 		headers.Set("Content-Range", fmt.Sprintf("%d-%d", part.Offset, part.Offset+part.Size-1))
 		headers.Set("Content-Range", fmt.Sprintf("%d-%d", part.Offset, part.Offset+part.Size-1))
 	}
 	}
 
 
-	resp, err := makeRequest(ctx, method, requestURL, headers, io.TeeReader(part.ReadSeeker, io.MultiWriter(part, part.Hash)), opts)
+	sr := io.NewSectionReader(b.file, part.Offset, part.Size)
+	resp, err := makeRequest(ctx, method, requestURL, headers, io.TeeReader(sr, part), opts)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -235,6 +257,7 @@ func (b *blobUpload) uploadChunk(ctx context.Context, method string, requestURL
 			return err
 			return err
 		}
 		}
 
 
+		// retry uploading to the redirect URL
 		for try := 0; try < maxRetries; try++ {
 		for try := 0; try < maxRetries; try++ {
 			err = b.uploadChunk(ctx, http.MethodPut, redirectURL, part, nil)
 			err = b.uploadChunk(ctx, http.MethodPut, redirectURL, part, nil)
 			switch {
 			switch {
@@ -243,7 +266,10 @@ func (b *blobUpload) uploadChunk(ctx context.Context, method string, requestURL
 			case errors.Is(err, errMaxRetriesExceeded):
 			case errors.Is(err, errMaxRetriesExceeded):
 				return err
 				return err
 			case err != nil:
 			case err != nil:
-				log.Printf("%s part %d attempt %d failed: %v, retrying", b.Digest[7:19], part.N, try, err)
+				part.Reset()
+				sleep := time.Second * time.Duration(math.Pow(2, float64(try)))
+				log.Printf("%s part %d attempt %d failed: %v, retrying in %s", b.Digest[7:19], part.N, try, err, sleep)
+				time.Sleep(sleep)
 				continue
 				continue
 			}
 			}
 
 
@@ -301,7 +327,7 @@ func (b *blobUpload) Wait(ctx context.Context, fn func(api.ProgressResponse)) er
 		}
 		}
 
 
 		fn(api.ProgressResponse{
 		fn(api.ProgressResponse{
-			Status:    fmt.Sprintf("uploading %s", b.Digest),
+			Status:    fmt.Sprintf("pushing %s", b.Digest[7:19]),
 			Digest:    b.Digest,
 			Digest:    b.Digest,
 			Total:     b.Total,
 			Total:     b.Total,
 			Completed: b.Completed.Load(),
 			Completed: b.Completed.Load(),
@@ -315,14 +341,10 @@ func (b *blobUpload) Wait(ctx context.Context, fn func(api.ProgressResponse)) er
 
 
 type blobUploadPart struct {
 type blobUploadPart struct {
 	// N is the part number
 	// N is the part number
-	N      int
-	Offset int64
-	Size   int64
-	hash.Hash
-
+	N       int
+	Offset  int64
+	Size    int64
 	written int64
 	written int64
-
-	io.ReadSeeker
 	*blobUpload
 	*blobUpload
 }
 }
 
 
@@ -334,10 +356,8 @@ func (p *blobUploadPart) Write(b []byte) (n int, err error) {
 }
 }
 
 
 func (p *blobUploadPart) Reset() {
 func (p *blobUploadPart) Reset() {
-	p.Seek(0, io.SeekStart)
 	p.Completed.Add(-int64(p.written))
 	p.Completed.Add(-int64(p.written))
 	p.written = 0
 	p.written = 0
-	p.Hash = md5.New()
 }
 }
 
 
 func uploadBlob(ctx context.Context, mp ModelPath, layer *Layer, opts *RegistryOptions, fn func(api.ProgressResponse)) error {
 func uploadBlob(ctx context.Context, mp ModelPath, layer *Layer, opts *RegistryOptions, fn func(api.ProgressResponse)) error {
@@ -352,7 +372,7 @@ func uploadBlob(ctx context.Context, mp ModelPath, layer *Layer, opts *RegistryO
 	default:
 	default:
 		defer resp.Body.Close()
 		defer resp.Body.Close()
 		fn(api.ProgressResponse{
 		fn(api.ProgressResponse{
-			Status:    fmt.Sprintf("uploading %s", layer.Digest),
+			Status:    fmt.Sprintf("pushing %s", layer.Digest[7:19]),
 			Digest:    layer.Digest,
 			Digest:    layer.Digest,
 			Total:     layer.Size,
 			Total:     layer.Size,
 			Completed: layer.Size,
 			Completed: layer.Size,