★ Reading this for free? Get 20 structured AI courses + per-chapter AI tutor — the first chapter of every course free, no card.Start free in 30 seconds
Ollama

Ollama Pull Stuck or Slow? Fix Failed Downloads

August 23, 2026
13 min read
Local AI Master Research Team

Want to go deeper than this article?

Free account unlocks the first chapter of all 25 courses — RAG, agents, MCP, voice AI, MLOps, real GitHub repos.

📚AI Learning Path

Ollama’s running. Here’s what to build with it. Go from “ollama run” to RAG apps, agents, and fine-tuned models — structured and hands-on. First chapter free.

Start free
Or own it for life — Lifetime $149, pay once

Run the same ollama pull again — it resumes, it does not start over. Ollama writes each blob to a -partial file with one small JSON file per chunk beside it, and Prepare() in server/download.go globs those -partial-* files at the start of every pull, restores each chunk's byte count, and skips chunks that already finished. The thing that turns a resume into a fresh 40GB download is not the pull, it is the server: on startup Ollama prunes any file in blobs/ whose name will not parse as a digest, which includes every -partial file, once it is more than an hour old. If you are coming back to an abandoned download tomorrow, start the daemon with OLLAMA_NOPRUNE=1.

That is the answer to the question most people are actually asking. The rest of this page is the part that saves you the second and third attempt: which of the half-dozen error strings you got, what each one is really telling you, and which of them are worth retrying versus which mean your network will never finish this download unattended. If you are still at the install stage rather than the debugging stage, start with our complete Ollama guide instead.

All constants, error strings and log lines below were read from ollama/ollama server/download.go on main, cross-checked against the log output pasted into the linked issues. The current release at the time of writing is v0.32.15 (19 August 2026). These values have changed before and will change again — the file on your own build is the authority, not this page.

Does re-running ollama pull resume, or start over?

It resumes, at chunk granularity. Understanding which chunk it resumes from is what tells you whether a flaky connection will ever converge.

A blob is not downloaded as one stream. Prepare() splits it into parts using three constants:

numDownloadParts          = 16
minDownloadPartSize int64 = 100 * format.MegaByte
maxDownloadPartSize int64 = 1000 * format.MegaByte

The part size is Total / 16, then clamped into that 100MB–1000MB window. You can verify the formula against a real log without owning anything: the server log pasted into issue #17484 contains

msg="downloading 8440f2a076f1 in 19 1 GB part(s)"
msg="downloading eacf610d1ee4 in 10 100 MB part(s)"

for an 18 GB blob and a 927 MB blob. 18,000 / 16 = 1,125 MB, above the 1,000 MB ceiling, so it clamps to 1 GB parts — 19 of them. 927 / 16 = 58 MB, below the 100 MB floor, so it clamps up to 100 MB parts — 10 of them. The formula and the log agree.

All 16-plus parts are fetched concurrently (g.SetLimit(numDownloadParts)), each with its own HTTP Range: bytes=start-end request. That is why a slow link feels worse than a single-stream download: sixteen streams are competing for the same pipe.

What is on disk while a pull is running

Path (under your models directory)What it is
blobs/sha256-<digest>A finished, verified blob
blobs/sha256-<digest>-partialThe download in progress. It is preallocated to the blob's full size (file.Truncate(b.Total) plus setSparse), so ls -l reports the final size within seconds — that number is not progress
blobs/sha256-<digest>-partial-<N>One tiny JSON file per chunk holding that chunk's offset, size and completed bytes. This is the resume state. Delete these and you restart

What survives an interruption, and what does not

This is the table that explains the behaviour people find maddening. The two branches come from downloadChunk, which commits a chunk's progress for exactly two error types and rolls it back for everything else.

How the download stoppedDoes that chunk keep its progress?Why
You pressed Ctrl-CYescontext.Canceled is one of two errors the copy path commits and writes out
Remote closed the stream mid-body (unexpected EOF)Yesio.ErrUnexpectedEOF is the other. The source comment says so: "return nil or context.Canceled or UnexpectedEOF (resumable)"
Stall watchdog fired (part stalled)YesThe watchdog cancels the chunk context, which surfaces as context.Canceled
Connection reset, DNS failure, timeoutNo — that chunk rewindsThe generic branch runs b.Completed.Add(-n) and returns before the progress is written to the part file
You killed the daemon, or lost powerOnly up to the last completed chunk attemptThe part file is written when a chunk attempt ends, not continuously
You restarted the daemon more than an hour laterNo — the files are goneStartup prune. See the section on protecting partial files

The fourth row is the one worth internalising. On a connection that drops rather than stalls, every reset throws away the bytes that chunk had accumulated since its last successful attempt — which on a 1 GB part can be hundreds of megabytes.

Reading articles is good. Building is better.

Free account = 20+ free chapters across 25 courses, with a per-chapter AI tutor. No card. Cancel anytime if you ever upgrade.

Which error is it? The full error-string map

The single most common mistake is treating Error: max retries exceeded as the diagnosis. It is not. It is a wrapper — fmt.Errorf("%w: %w", errMaxRetriesExceeded, err) — and the real error is the text after the colon. Six failed attempts on one chunk produce it, with maxRetries = 6 and a sleep of 2^try seconds between attempts: 1s, 2s, 4s, 8s, 16s, 32s.

Error string you seeWhere it comes fromWhat it actually meansWhat to do
Error: max retries exceeded: <anything>errMaxRetriesExceeded in server/download.goOne chunk failed six times. Read the part after the colon — that is the real faultDiagnose the suffix, using the rows below
Error: context deadline exceededA Go context timeout — in a pull, usually the 30-second budget on resolving the blob's direct URLA registry request hung long enough to burn the whole resolution windowRe-run the pull. If it lands at the end of an otherwise complete download, see the section below
max retries exceeded: unexpected EOFRemote closed the connection mid-body, six timesResumable, and the progress was kept each time. Almost always the network, not OllamaRe-run the pull. Repeat until it converges — it will, because progress is preserved
max retries exceeded: ... read: connection reset by peerTCP RST from something between you and the CDNA middlebox, VPN, corporate firewall or ISP is killing long-lived TLS streamsRetry off the VPN; if it repeats, this is the one case where progress genuinely rewinds
dial tcp: lookup <32-hex>.r2.cloudflarestorage.com: no such hostDNS failure on the redirect target, not the registryThe manifest fetch worked; the blob lives on a signed CDN URL at a different hostname your resolver cannot answer forFix DNS or the proxy — not a bandwidth problem
dial tcp: lookup registry.ollama.ai on 10.x.x.x:53: no such hostYour internal resolver cannot resolve the registry at allThe daemon is not going through the proxy you configured in your shellSet HTTPS_PROXY for the server process
maximum redirects exceeded (10) for directURLerrMaxRedirectsExceededSomething is intercepting and re-redirecting the CDN handoff — captive portal, TLS-inspecting proxyGet off the intercepting network, or install the proxy CA as a system certificate
<digest> part N stalled; retrying. If this persists, press ctrl-c to exit, then 'ollama pull' to find a faster connection.The 30-second stall watchdog, logged at INFONot an error. One chunk received zero bytes for 30 seconds and is being reopenedNothing, unless it loops forever — then see the stall section
no space left on devicesyscall.ENOSPCOut of disk. One of only two errors that abort immediately with no retryFree space, or move the models directory — see OLLAMA_MODELS not working
unexpected status code <n>The registry returned something that is neither 200 nor 307A gateway, WAF or proxy answered instead of the registryLook at the number: 403 and 407 mean proxy auth; 5xx means try later
Error: pull model manifest: <error>images.go — the manifest fetch, before any blob startsYou never got as far as downloading weights. Auth, DNS or TLS, not bandwidthTest with curl -I https://registry.ollama.ai/v2/ from the same machine

Two of those deserve emphasis. unexpected EOF is the good failure — it keeps your progress, so brute-force retrying works. connection reset by peer is the bad one, because that path rewinds the chunk, which is how people end up watching a 40GB download oscillate for a day.

Why does the progress bar jump backwards?

Because on a generic chunk error, Ollama subtracts the bytes it had already counted. The rollback is explicit in the source:

n, err := io.CopyN(w, io.TeeReader(resp.Body, part), part.Size-part.Completed.Load())
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, io.ErrUnexpectedEOF) {
    // rollback progress
    b.Completed.Add(-n)
    return err
}

b.Completed is what the progress bar reads. So a chunk that got 800MB in and then hit a reset silently gives those 800MB back, and the percentage falls. Sixteen chunks doing that at random on a bad link produces the sawtooth in issue #17329, "Bug: Download keep resetting when the wifi is slow" — still open, with the reporter noting the useful detail that the server log shows nothing at all when it happens.

There is a proposed fix. PR #17389 would add bandwidth sampling, scale the stall timeout by measured speed, drop to four parallel parts on slow links, and require three consecutive stall cycles before retrying. Note the tense: it is open and unmerged, so none of that is in your build. Do not configure around it.

What you can do today is reduce the number of things that can go wrong at once, which mostly means not downloading over Wi-Fi, and not downloading through a VPN.

What does Error: context deadline exceeded actually mean?

It is a Go context timeout, and in ollama pull it is almost always the 30-second budget for resolving a blob's direct download URL — not the download itself.

Before fetching a blob, Ollama asks the registry for the real (signed, CDN-hosted) URL. That resolution runs inside a single wrapping context:

ctx, cancel := context.WithTimeout(ctx, 30*time.Second)

with a backoff-and-retry loop inside it, using the same context. The consequence, spelled out in PR #17551: "If the first registry request hangs until that context expires, the backoff sees an already-canceled context and returns immediately, so no retry actually occurs." One slow request eats the entire retry budget.

That explains the shape of issue #17484, which is what most people are searching when they land here: two large blobs complete at 100%, and then the pull dies on a 479-byte one.

pulling 8440f2a076f1: 100% ...  18 GB
pulling eacf610d1ee4: 100% ...  927 MB
Error: context deadline exceeded

The last blob was 479 bytes. Bandwidth had nothing to do with it. PR #17551 proposed bounding each individual request to 10 seconds while keeping the 30-second overall budget — it was closed without being merged on 17 August 2026, and main still has the shared 30-second context. Issue #17484 is still open.

What to do: re-run the pull. Because the big blobs completed, the resume is cheap — you are retrying a metadata fetch, not 18GB. If it fails repeatedly at the same point, the registry path is being slowed by something local (DNS, proxy, TLS inspection) rather than by your download speed.

Reading articles is good. Building is better.

Free account = 20+ free chapters across 25 courses, with a per-chapter AI tutor. No card. Cancel anytime if you ever upgrade.

Why does it stall at the same percentage every time?

Ollama has a per-chunk watchdog that fires after 30 seconds with no bytes written, and it logs a message people usually mistake for an error:

<digest> part 7 stalled; retrying. If this persists, press ctrl-c to exit,
then 'ollama pull' to find a faster connection.

That is an INFO-level log, and it is the system working. The important detail is in the retry loop: a stalled chunk is handled with try-- before continue, so a stall does not consume the six-attempt budget. A chunk can stall and reopen forever without ever producing max retries exceeded. That is why a genuinely dead connection presents as an infinite hang rather than a clean failure.

downloadStallTimeout is a package-level var set to 30 seconds. It is not exposed as an environment variable, so there is no supported way to raise it on a slow link — the only lever the tracker offers is the unmerged PR above. The historical version of this complaint is issue #1736, "Download slows to a crawl at 99%", which is the same watchdog interacting with a chunk that opens but never streams.

If you are stuck in that loop: Ctrl-C, wait, and pull again. Your progress is preserved (Ctrl-C is the good stop), and a new pull re-resolves the CDN URL, which frequently lands you on a different edge node.

Why does DNS fail on a random Cloudflare hostname?

Because the blob does not come from registry.ollama.ai. The registry answers with a redirect to a signed, time-limited storage URL on a different hostname, and server/download.go deliberately stops following at the first cross-host redirect and uses that URL directly:

// if the hostname is the same, allow the redirect
if req.URL.Hostname() == requestURL.Hostname() {
    return nil
}
// stop at the first redirect that is not the same hostname
return http.ErrUseLastResponse

So a pull touches two hostnames, and your network has to be able to reach both. The classic failure, reported verbatim in issue #10151, is:

Error: max retries exceeded: Get "https://dd20bb891979d25aebc8bec07b2b3bbc.r2.cloudflarestorage.com/ollama/docker/registry/v2/blobs/sha256/dd/...":
dial tcp: lookup dd20bb891979d25aebc8bec07b2b3bbc.r2.cloudflarestorage.com: no such host

The manifest downloaded fine. The blob host did not resolve. Things that produce that, in the order worth checking:

  1. A resolver that only answers for internal names. Confirm with a plain lookup of the failing hostname from the same machine. If nslookup fails too, it is not an Ollama bug.
  2. A DNS-level blocker. Pi-hole, NextDNS and corporate filtering commonly have broad rules covering object-storage hostnames.
  3. IPv6 that resolves but does not route. If your resolver returns AAAA records for a path with no working IPv6 egress, connections hang rather than fail cleanly. Test by disabling IPv6 on the interface, or prefer IPv4 at the OS level, and pull again.
  4. A proxy configured for the shell but not the daemon. This is common enough that it gets its own section below. Issue #15708 is the open report of exactly this pattern: the manifest resolves through the proxy, the blob download then fails with no such host.

How do I pull through a corporate proxy?

The proxy variable has to reach the Ollama server process. Exporting it in the terminal where you type ollama pull usually does nothing, because the CLI is a thin HTTP client — the daemon does the downloading.

The official FAQ is specific about which variable: "Use HTTPS_PROXY to redirect outbound requests through the proxy. Ensure the proxy certificate is installed as a system certificate." And it warns against the obvious-looking alternative: "Avoid setting HTTP_PROXY. Ollama does not use HTTP for model pulls, only HTTPS. Setting HTTP_PROXY may interrupt client connections to the server."

Where to set it depends entirely on how the server was started:

How the server runsWhere HTTPS_PROXY must go
Linux, installed with the official scriptThe systemd unit — sudo systemctl edit ollama.service, add Environment="HTTPS_PROXY=http://proxy:port" under [Service], then systemctl daemon-reload && systemctl restart ollama. A shell export never reaches it
macOS desktop applaunchctl setenv HTTPS_PROXY "http://proxy:port", then restart the Ollama application. .zshrc is not read by a launchd-started app
Windows desktop appQuit from the tray, set the variable under "Edit environment variables for your account", relaunch from the Start menu
ollama serve in a terminalExport in that terminal, before starting the server
Docker-e HTTPS_PROXY=https://proxy.example.com on docker run, per the FAQ

TLS-inspecting proxies need one more step: the proxy's CA has to be a system certificate, not just trusted by your browser. Go's HTTP client reads the system trust store, and a proxy that re-signs traffic without a trusted CA produces a TLS error rather than a clean proxy error.

The same "set it on the daemon, not the shell" rule is what breaks model relocation too — that is covered in detail in our guide to OLLAMA_MODELS not working per platform, and the full environment-variable surface is tabulated in our Ollama troubleshooting guide.

Where are the partial files, and how do I stop Ollama deleting them?

Ollama prunes unrecognised blob files at server startup, and every -partial file is unrecognised. This is the mechanism behind "I restarted my machine and my 40GB download started from zero."

PruneLayers() runs on startup (unless OLLAMA_NOPRUNE is set) and walks the blobs directory. For each file it converts - to : and tries to parse the result as a digest. sha256-abc123-partial becomes sha256:abc123:partial, which is not a valid digest, so the file is removed — the source comment is literally "remove invalid blobs (e.g. partial downloads)". The only thing standing between you and that deletion is a grace period:

const layerPruneGracePeriod = time.Hour
...
if time.Since(info.ModTime()) < layerPruneGracePeriod {
    continue
}

So: a partial download survives a server restart for one hour after its last write, and not a minute longer.

SituationWhat happens to your partials
Pull dies, you re-run it immediatelyFine — resumes
Pull dies, you restart the daemon within the hourFine — the grace period protects them
Pull dies, you come back tomorrow and start the daemon normallyDeleted. Full re-download
You start the daemon with OLLAMA_NOPRUNE=1Prune is skipped entirely; partials survive indefinitely

Set OLLAMA_NOPRUNE=1 the same way you would set the proxy above — on the server process, not the shell. It disables all blob pruning, including cleanup of genuinely orphaned layers, so turn it off again once the download lands.

The directories themselves, per the official FAQ:

OSModels directory
macOS~/.ollama/models
Linux (standard installer)/usr/share/ollama/.ollama/models
WindowsC:\Users\%username%\.ollama\models

Partials live in blobs/ inside those. If you have moved the directory and are not sure Ollama agrees with you about where it is, that is a different failure — see the OLLAMA_MODELS guide.

What actually helps on a slow or flaky connection?

Honestly, less than you would like. There is no download-speed setting, no configurable part count, and no way to raise the stall timeout. OLLAMA_MAX_TRANSFER_STREAMS exists (default 4) but its own description in envconfig/config.go scopes it to "safetensors model pulls/pushes" — it does not change numDownloadParts = 16 for registry blob downloads.

What is left, ranked by how much difference it makes:

  1. Wire, not Wi-Fi. Sixteen concurrent range requests over a marginal wireless link is the exact condition in #17329. This is the single biggest lever.
  2. Turn the VPN off for the pull. VPN concentrators are a common source of the connection reset by peer variant, which is the failure mode that rewinds progress.
  3. Just re-run it, repeatedly. With unexpected EOF and Ctrl-C both preserving progress, a loop of pull-fail-pull genuinely converges. Do it inside the one-hour prune window, or set OLLAMA_NOPRUNE=1 first.
  4. Pull a smaller quantisation. A Q4_K_M build is roughly 0.6GB per billion parameters, so a 70B is about 42GB and a 32B about 19GB — check what you actually need against our Ollama model RAM and VRAM table before committing to the biggest file on the page.
  5. Copy the blobs from a machine that already has them. If a colleague or a second machine has the model, moving blobs/ and manifests/ by hand is faster and more reliable than any retry strategy. Our guide to copying Ollama models to an offline PC covers the exact file layout, and air-gapped AI deployment covers doing it at scale.
  6. Pull overnight, unattended, and accept it may need a second run. Given progress preservation, two overnight runs beat one supervised day.

Honest limitations

  • Nothing here was reproduced on our own connection. Every constant and code excerpt comes from ollama/ollama on main; every error string and log line comes from the linked issue where a user pasted it. We do not publish download-speed benchmarks we did not measure, and there are none on this page.
  • Constants drift. maxRetries, numDownloadParts, downloadStallTimeout and layerPruneGracePeriod are compile-time values in a fast-moving repo. They are accurate for main at v0.32.15 (19 August 2026). Check server/download.go on your build before quoting them back at anyone.
  • #17484, #17329 and #15708 were open when this was written, and PRs #17551 and #17389 were unmerged. Click through before assuming a workaround is still needed — or still necessary.
  • Non-registry sources behave differently. Pulling hf.co/... GGUF models goes through the same downloader but a different host and auth path; #17484 is itself a Hugging Face pull. Errors that mention a hf.co URL are worth searching separately.
  • We are not going to tell you to edit /etc/hosts. Pinning a signed, rotating CDN hostname to an IP you found on a forum is how you get a download that fails signature checks later. Fix DNS properly or use the proxy.

FAQ

Does ollama pull resume a partial download, or start over?

It resumes. Each chunk's progress lives in a small JSON file named sha256-<digest>-partial-<N> next to the partial blob, and Prepare() reads those back at the start of every pull, skipping chunks that already completed. The exception is the startup prune: partial files older than one hour are deleted when the server starts, unless OLLAMA_NOPRUNE is set.

What does Error: max retries exceeded mean on its own?

That one chunk failed six consecutive times, with a doubling sleep between attempts. It is a wrapper, not a diagnosis — the real cause is the error text printed after the colon. unexpected EOF means the remote hung up (retry, your progress is kept); connection reset by peer means something actively killed the connection (that chunk's progress rewinds); no such host means DNS, not bandwidth.

Why does ollama pull fail with context deadline exceeded after the download finished?

Because resolving a blob's direct URL runs under a single 30-second context shared across its own retry loop, so one hung request can consume the entire budget — including on a tiny final blob after gigabytes have already landed. That is issue #17484, still open; the proposed fix in PR #17551 was closed without merging. Re-running the pull is cheap, because the completed blobs are kept.

Can I make Ollama download with fewer parallel connections?

Not for registry blobs. numDownloadParts = 16 is a compile-time constant with no environment override. OLLAMA_MAX_TRANSFER_STREAMS (default 4) applies to safetensors pulls and pushes, not to this path. The open PR #17389 proposes dropping to four parts automatically on slow links, but it is unmerged.

Is there a way to make the 30-second stall timeout longer?

No supported one. downloadStallTimeout is a package-level variable in server/download.go with no environment binding — it is a var rather than a const so tests can shorten it, not so users can raise it. When a chunk stalls, Ollama reopens it and decrements the retry counter, so stalls loop indefinitely rather than failing.

Why did my half-finished 40GB download disappear overnight?

Startup prune. Ollama removes files in blobs/ whose names do not parse as digests — which includes every -partial file — once they are more than an hour old, and it does this every time the server starts. Start the daemon with OLLAMA_NOPRUNE=1 when you intend to resume a stale download.

Is part stalled; retrying something I need to fix?

Not by itself. It is an INFO log meaning one chunk received nothing for 30 seconds and is being reopened, and the retry counter is decremented so it does not count against the six-attempt budget. It only matters if it repeats indefinitely, at which point the message's own advice applies: Ctrl-C, then pull again.

Sources

  • ollama/ollama — server/download.go (maxRetries, numDownloadParts, part sizing, downloadStallTimeout, the rollback branch, redirect handling, error variables)
  • ollama/ollama — server/images.go (PruneLayers, layerPruneGracePeriod, the pull model manifest wrapper)
  • Ollama FAQ (model storage paths, HTTPS_PROXY guidance, per-platform environment variables)
  • Issues: #17484 (context deadline exceeded, open), #17329 (progress resets on slow Wi-Fi, open), #15708 (no such host behind a proxy, open), #10151 (r2.cloudflarestorage.com DNS failure), #1036 (unexpected EOF on pull), #8167 (connection reset by peer), #2155 and #1736 (slow-connection behaviour)
  • Pull requests: #17551 (closed unmerged) and #17389 (open)
🎯
AI Learning Path

Ollama’s running. Here’s what to build with it.

Go from “ollama run” to RAG apps, agents, and fine-tuned models — structured and hands-on. First chapter free.

Or own it for life — Lifetime $149 $599, pay once
Once your hardware is sorted

Stop piecing Ollama together from blog posts

Ollama Mastery is 15 chapters end to end — install, model choice, Modelfiles, GPU offload, the API, and the 20 errors that actually happen. Plus 24 more courses.

$149 once unlocks everything, forever — about $0.27/chapter for life. Prefer to spread it out? Pro is $79/year (saves 27%) or $8.99/month.
Secure checkout by Lemon Squeezy — your card never touches this siteInstant access the moment you payFirst chapter of every course is free — try before you buy

Liked this? 20 full AI courses are waiting.

From fundamentals to RAG, agents, MCP servers, voice AI, and production deployment with real GitHub repos. First chapter free, every course.

Reading now
Join the discussion

Local AI Master Research Team

Creator of Local AI Master. I've built datasets with over 77,000 examples and trained AI models from scratch. Now I help people achieve AI independence through local AI mastery.

Build Real AI on Your Machine

RAG, agents, NLP, vision, and MLOps - chapters across 25 courses that take you from reading about AI to building AI.

Want structured AI education?

25 courses, 519+ chapters, from $9. Understand AI, don't just use it.

AI Learning Path
More on Ollama
See the full Best Ollama Models 2026 guide.

Comments (0)

No comments yet. Be the first to share your thoughts!

📅 Published: August 23, 2026🔄 Last Updated: August 23, 2026✓ Manually Reviewed

Ready to Go Beyond Tutorials?

20 structured courses with hands-on chapters - build RAG chatbots, AI agents, and ML pipelines on your own hardware.

🎯
AI Learning Path

Go from reading about AI to building with AI

20 structured courses. Hands-on projects. Runs on your machine. Start free.

Or own it for life — Lifetime $149 $599, pay once

Was this helpful?

LM

Written by the Local AI Master Team

The team behind Local AI Master

We build Local AI Master around practical, testable local AI workflows: model selection, hardware planning, RAG systems, agents, and MLOps. The goal is to turn scattered tutorials into a structured learning path you can follow on your own hardware.

✓ Local AI Curriculum✓ Hands-On Projects✓ Open Source Contributor
📚
Free · no account required

Grab the AI Starter Kit — career roadmap, cheat sheet, setup guide

No spam. Unsubscribe with one click.

🎯
AI Learning Path

Ollama’s running. Here’s what to build with it.

Go from “ollama run” to RAG apps, agents, and fine-tuned models — structured and hands-on. First chapter free.

Or own it for life — Lifetime $149 $599, pay once
Free Tools & Calculators