Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
OpenDAS
ollama
Commits
868e3b31
"vscode:/vscode.git/clone" did not exist on "4e320b8b90b8a698fc3c057a3f54cbabe59b543a"
Commit
868e3b31
authored
Jul 25, 2023
by
Bruce MacDonald
Browse files
allow for concurrent pulls of the same files
parent
09d8bf67
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
231 additions
and
6 deletions
+231
-6
server/download.go
server/download.go
+217
-0
server/images.go
server/images.go
+5
-4
server/routes.go
server/routes.go
+9
-2
No files found.
server/download.go
0 → 100644
View file @
868e3b31
package
server
import
(
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"strconv"
"sync"
"time"
"github.com/jmorganca/ollama/api"
)
type
FileDownload
struct
{
Digest
string
FilePath
string
Total
int64
Completed
int64
}
var
inProgress
sync
.
Map
// map of digests currently being downloaded to their current download progress
// downloadBlob downloads a blob from the registry and stores it in the blobs directory
func
downloadBlob
(
ctx
context
.
Context
,
mp
ModelPath
,
digest
string
,
regOpts
*
RegistryOptions
,
fn
func
(
api
.
ProgressResponse
))
error
{
fp
,
err
:=
GetBlobsPath
(
digest
)
if
err
!=
nil
{
return
err
}
if
fi
,
_
:=
os
.
Stat
(
fp
);
fi
!=
nil
{
// we already have the file, so return
fn
(
api
.
ProgressResponse
{
Digest
:
digest
,
Total
:
int
(
fi
.
Size
()),
Completed
:
int
(
fi
.
Size
()),
})
return
nil
}
fileDownload
:=
&
FileDownload
{
Digest
:
digest
,
FilePath
:
fp
,
Total
:
1
,
// dummy value to indicate that we don't know the total size yet
Completed
:
0
,
}
_
,
downloading
:=
inProgress
.
LoadOrStore
(
digest
,
fileDownload
)
if
downloading
{
// this is another client requesting the server to download the same blob concurrently
return
monitorDownload
(
ctx
,
mp
,
regOpts
,
fileDownload
,
fn
)
}
resp
,
err
:=
requestDownload
(
ctx
,
mp
,
regOpts
,
fileDownload
)
if
err
!=
nil
{
return
err
}
return
doDownload
(
ctx
,
fileDownload
,
resp
,
fn
)
}
var
downloadMu
sync
.
Mutex
// mutex to check to resume a download while monitoring
// monitorDownload monitors the download progress of a blob and resumes it if it is interrupted
func
monitorDownload
(
ctx
context
.
Context
,
mp
ModelPath
,
regOpts
*
RegistryOptions
,
f
*
FileDownload
,
fn
func
(
api
.
ProgressResponse
))
error
{
tick
:=
time
.
NewTicker
(
time
.
Second
)
for
range
tick
.
C
{
downloadMu
.
Lock
()
val
,
downloading
:=
inProgress
.
Load
(
f
.
Digest
)
if
!
downloading
{
// check once again if the download is complete
if
fi
,
_
:=
os
.
Stat
(
f
.
FilePath
);
fi
!=
nil
{
downloadMu
.
Unlock
()
// successfull download while monitoring
fn
(
api
.
ProgressResponse
{
Digest
:
f
.
Digest
,
Total
:
int
(
fi
.
Size
()),
Completed
:
int
(
fi
.
Size
()),
})
return
nil
}
// resume the download
resp
,
err
:=
requestDownload
(
ctx
,
mp
,
regOpts
,
f
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"resume: %w"
,
err
)
}
inProgress
.
Store
(
f
.
Digest
,
f
)
downloadMu
.
Unlock
()
return
doDownload
(
ctx
,
f
,
resp
,
fn
)
}
downloadMu
.
Unlock
()
f
,
ok
:=
val
.
(
*
FileDownload
)
if
!
ok
{
return
fmt
.
Errorf
(
"invalid type for in progress download: %T"
,
val
)
}
fn
(
api
.
ProgressResponse
{
Status
:
fmt
.
Sprintf
(
"downloading %s"
,
f
.
Digest
),
Digest
:
f
.
Digest
,
Total
:
int
(
f
.
Total
),
Completed
:
int
(
f
.
Completed
),
})
}
return
nil
}
var
chunkSize
=
1024
*
1024
// 1 MiB in bytes
// requestDownload requests a blob from the registry and returns the response
func
requestDownload
(
ctx
context
.
Context
,
mp
ModelPath
,
regOpts
*
RegistryOptions
,
f
*
FileDownload
)
(
*
http
.
Response
,
error
)
{
var
size
int64
fi
,
err
:=
os
.
Stat
(
f
.
FilePath
+
"-partial"
)
switch
{
case
errors
.
Is
(
err
,
os
.
ErrNotExist
)
:
// noop, file doesn't exist so create it
case
err
!=
nil
:
return
nil
,
fmt
.
Errorf
(
"stat: %w"
,
err
)
default
:
size
=
fi
.
Size
()
// Ensure the size is divisible by the chunk size by removing excess bytes
size
-=
size
%
int64
(
chunkSize
)
err
:=
os
.
Truncate
(
f
.
FilePath
+
"-partial"
,
size
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"truncate: %w"
,
err
)
}
}
url
:=
fmt
.
Sprintf
(
"%s/v2/%s/blobs/%s"
,
mp
.
Registry
,
mp
.
GetNamespaceRepository
(),
f
.
Digest
)
headers
:=
map
[
string
]
string
{
"Range"
:
fmt
.
Sprintf
(
"bytes=%d-"
,
size
),
}
resp
,
err
:=
makeRequest
(
"GET"
,
url
,
headers
,
nil
,
regOpts
)
if
err
!=
nil
{
log
.
Printf
(
"couldn't download blob: %v"
,
err
)
return
nil
,
err
}
// resp MUST be closed by doDownload, which should follow this function
if
resp
.
StatusCode
!=
http
.
StatusOK
&&
resp
.
StatusCode
!=
http
.
StatusPartialContent
{
body
,
_
:=
io
.
ReadAll
(
resp
.
Body
)
return
nil
,
fmt
.
Errorf
(
"on download registry responded with code %d: %v"
,
resp
.
StatusCode
,
string
(
body
))
}
err
=
os
.
MkdirAll
(
path
.
Dir
(
f
.
FilePath
),
0
o700
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"make blobs directory: %w"
,
err
)
}
remaining
,
_
:=
strconv
.
ParseInt
(
resp
.
Header
.
Get
(
"Content-Length"
),
10
,
64
)
f
.
Completed
=
size
f
.
Total
=
remaining
+
f
.
Completed
inProgress
.
Store
(
f
.
Digest
,
f
)
return
resp
,
nil
}
// doDownload downloads a blob from the registry and stores it in the blobs directory
func
doDownload
(
ctx
context
.
Context
,
f
*
FileDownload
,
resp
*
http
.
Response
,
fn
func
(
api
.
ProgressResponse
))
error
{
defer
resp
.
Body
.
Close
()
out
,
err
:=
os
.
OpenFile
(
f
.
FilePath
+
"-partial"
,
os
.
O_CREATE
|
os
.
O_APPEND
|
os
.
O_WRONLY
,
0
o644
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"open file: %w"
,
err
)
}
defer
out
.
Close
()
outerLoop
:
for
{
select
{
case
<-
ctx
.
Done
()
:
// handle client request cancellation
inProgress
.
Delete
(
f
.
Digest
)
return
nil
default
:
fn
(
api
.
ProgressResponse
{
Status
:
fmt
.
Sprintf
(
"downloading %s"
,
f
.
Digest
),
Digest
:
f
.
Digest
,
Total
:
int
(
f
.
Total
),
Completed
:
int
(
f
.
Completed
),
})
if
f
.
Completed
>=
f
.
Total
{
if
err
:=
out
.
Close
();
err
!=
nil
{
return
err
}
if
err
:=
os
.
Rename
(
f
.
FilePath
+
"-partial"
,
f
.
FilePath
);
err
!=
nil
{
fn
(
api
.
ProgressResponse
{
Status
:
fmt
.
Sprintf
(
"error renaming file: %v"
,
err
),
Digest
:
f
.
Digest
,
Total
:
int
(
f
.
Total
),
Completed
:
int
(
f
.
Completed
),
})
return
err
}
break
outerLoop
}
}
n
,
err
:=
io
.
CopyN
(
out
,
resp
.
Body
,
int64
(
chunkSize
))
if
err
!=
nil
&&
!
errors
.
Is
(
err
,
io
.
EOF
)
{
return
err
}
f
.
Completed
+=
n
inProgress
.
Store
(
f
.
Digest
,
f
)
}
inProgress
.
Delete
(
f
.
Digest
)
log
.
Printf
(
"success getting %s
\n
"
,
f
.
Digest
)
return
nil
}
server/images.go
View file @
868e3b31
...
...
@@ -3,6 +3,7 @@ package server
import
(
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
...
...
@@ -232,7 +233,7 @@ func filenameWithPath(path, f string) (string, error) {
return
f
,
nil
}
func
CreateModel
(
name
string
,
path
string
,
fn
func
(
resp
api
.
ProgressResponse
))
error
{
func
CreateModel
(
ctx
context
.
Context
,
name
string
,
path
string
,
fn
func
(
resp
api
.
ProgressResponse
))
error
{
mf
,
err
:=
os
.
Open
(
path
)
if
err
!=
nil
{
fn
(
api
.
ProgressResponse
{
Status
:
fmt
.
Sprintf
(
"couldn't open modelfile '%s'"
,
path
)})
...
...
@@ -265,7 +266,7 @@ func CreateModel(name string, path string, fn func(resp api.ProgressResponse)) e
// the model file does not exist, try pulling it
if
errors
.
Is
(
err
,
os
.
ErrNotExist
)
{
fn
(
api
.
ProgressResponse
{
Status
:
"pulling model file"
})
if
err
:=
PullModel
(
c
.
Args
,
&
RegistryOptions
{},
fn
);
err
!=
nil
{
if
err
:=
PullModel
(
ctx
,
c
.
Args
,
&
RegistryOptions
{},
fn
);
err
!=
nil
{
return
err
}
mf
,
err
=
GetManifest
(
ParseModelPath
(
c
.
Args
))
...
...
@@ -900,7 +901,7 @@ func PushModel(name string, regOpts *RegistryOptions, fn func(api.ProgressRespon
return
nil
}
func
PullModel
(
name
string
,
regOpts
*
RegistryOptions
,
fn
func
(
api
.
ProgressResponse
))
error
{
func
PullModel
(
ctx
context
.
Context
,
name
string
,
regOpts
*
RegistryOptions
,
fn
func
(
api
.
ProgressResponse
))
error
{
mp
:=
ParseModelPath
(
name
)
fn
(
api
.
ProgressResponse
{
Status
:
"pulling manifest"
})
...
...
@@ -915,7 +916,7 @@ func PullModel(name string, regOpts *RegistryOptions, fn func(api.ProgressRespon
layers
=
append
(
layers
,
&
manifest
.
Config
)
for
_
,
layer
:=
range
layers
{
if
err
:=
downloadBlob
(
mp
,
layer
.
Digest
,
regOpts
,
fn
);
err
!=
nil
{
if
err
:=
downloadBlob
(
ctx
,
mp
,
layer
.
Digest
,
regOpts
,
fn
);
err
!=
nil
{
return
err
}
}
...
...
server/routes.go
View file @
868e3b31
package
server
import
(
"context"
"encoding/json"
"errors"
"fmt"
...
...
@@ -200,7 +201,10 @@ func PullModelHandler(c *gin.Context) {
Password
:
req
.
Password
,
}
if
err
:=
PullModel
(
req
.
Name
,
regOpts
,
fn
);
err
!=
nil
{
ctx
,
cancel
:=
context
.
WithCancel
(
c
.
Request
.
Context
())
defer
cancel
()
if
err
:=
PullModel
(
ctx
,
req
.
Name
,
regOpts
,
fn
);
err
!=
nil
{
ch
<-
gin
.
H
{
"error"
:
err
.
Error
()}
}
}()
...
...
@@ -250,7 +254,10 @@ func CreateModelHandler(c *gin.Context) {
ch
<-
resp
}
if
err
:=
CreateModel
(
req
.
Name
,
req
.
Path
,
fn
);
err
!=
nil
{
ctx
,
cancel
:=
context
.
WithCancel
(
c
.
Request
.
Context
())
defer
cancel
()
if
err
:=
CreateModel
(
ctx
,
req
.
Name
,
req
.
Path
,
fn
);
err
!=
nil
{
ch
<-
gin
.
H
{
"error"
:
err
.
Error
()}
}
}()
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment