Skip to main content

pichak

Fine-tuning hyperparameters derived from your machine, with the measurement printed next to every number.

pip install pichak
from pichak import derive

plan = derive(
    data="train.jsonl",
    tokenizer=tok,
    step_fn=lambda b: one_real_forward_and_backward(b),
    named_weights=model.named_parameters(),
)
print(plan.report())
  seq_len            512
                     token p95 416 over 512 real rows, rounded up to 512; p99 is
                     657 and max 943, so 5% of rows lose their tail at this length

  micro_batch        3
                     a doubling ramp with a real forward+backward at each rung:
                     1=ok, 2=ok, 4=ok, 8=SPILL. 8 SPILLED to system memory — it did
                     not raise, it got 1.6x slower per sample, which is the Windows
                     failure mode, so 4 is the largest that fit, peaking at 5.05GB
                     of 5.05GB; kept 3 to leave 12% for optimizer state

  learning_rate      0.000282
                     1/32 of the scale at which one Adam step would rewrite rather
                     than perturb. ||W||_F/sqrt(numel) over 26 matrices; the
                     smallest binds and it is b.1.f.2.weight at 0.00902

  NOT MEASURED (1): target_tokens=65536

Why not just use a good default

Because you cannot tell a good default from a bad one when it fails.

micro_batch=4 tells you nothing about whether 4 came from a measurement on your card, a heuristic in a blog post, or a number someone picked in 2023 for a different model. So when it OOMs at step 40 you bisect instead of reading.

Every number pichak returns carries the sentence that produced it. Anything it could not measure is collected under plan.constants() and printed together, so a constant can never quietly pass for a derivation.

plan.micro_batch          # 3
plan.why("micro_batch")   # the ramp, rung by rung, and which one failed
plan.constants()          # {'target_tokens': 65536}

It catches the failure that does not raise

On Windows, a batch that exceeds VRAM does not throw. The driver pages the excess to system memory and the step simply crawls. Measured on a GTX 1060 6GB with a 6-layer model at sequence 512:

micro_batch peak seconds/step
watching only for OOM 14 18.24GB on a 6GB card 47.5
watching per-sample time too 3 5.05GB 2.0

Both "work". The first is 23x slower and nothing in the logs says why. On a healthy ramp, seconds-per-sample is flat or improves as the batch grows — bigger batches amortise fixed costs. A sudden jump means the batch did not really fit.

What it derives, and from what

measurement
seq_len tokenised p95 over 512 real rows of your corpus
loss_policy whether the rows have a separable completion to mask against
micro_batch a doubling ramp with a real forward+backward at each rung
seconds_per_step the wall time of that rung
grad_accum micro-batch x seq_len against your token target
lora_rank largest rung whose optimizer state fits — fp32 master, two Adam moments, and one step's gradients, which is the copy count people forget
learning_rate the model's own Frobenius norms: the step size at which one Adam update would rewrite a weight rather than perturb it

Everything is optional. Pass only a corpus and a tokenizer and you get a sequence length; pass a step_fn and you get the batch. It will not invent a number it could not measure — a plan with three derived values and an honest gap is more useful than one with ten you cannot tell apart.

The learning rate, since it is the surprising one

An Adam update moves a parameter by roughly lr, whatever the gradient's scale. So there is a learning rate at which one step rewrites a weight matrix instead of nudging it: where lr * sqrt(numel(W)) reaches ||W||_F. Every sane learning rate is a fraction of that, and the fraction is what schedules argue about.

Measured, at the ranks behind this library:

rewrite/2    2.26e-3   destroyed the model twice (CE 1.74 -> 10.4, 1.11 -> 19.3)
rewrite/32   1.41e-4   the first scale that actually learned
rewrite/45             where a hand-tuned LoRA run at 2e-4 landed

pichak opens at rewrite/32 — the top of the range that works. Opening hot costs the model; opening cold costs time. With a penalty that asymmetric you start at the bottom of what is known.

This is not tuned for your dataset. It is the scale at which updates are the right size for these weights. It replaces "I copied 2e-4 from a post about a different model", which is the actual alternative.

Measurement tools, usable on their own

pichak gpu                    # virtualisation, launch latency, transfer bandwidth
pichak disk "models/*.safetensors"   # queue-depth sweep, page cache bypassed
pichak corpus train.jsonl mistralai/Mistral-Small-24B-Instruct-2501
from pichak.measure import disk_queue_depth, transfer_bandwidth, virtualisation

These exist because the numbers people quote are rarely the numbers their machine gives:

  • A buffered disk benchmark reported 4079 MB/s on a drive rated 2100 — that was the page cache. Unbuffered, the same drive peaks at queue depth 3 and gets slower past it.
  • A rented A6000 measured D2H 457 MB/s against H2D 1502, and pinned memory — the standard fix — bought nothing. Nothing about PCIe explains that; it was a fabric, and it decided where hidden states could live.
  • One rented machine had 84.8us kernel launches against a normal 2-5. A workload issuing 400,000 launches per step would have spent nine hours on overhead and looked slower than a 2016 card.

Install

pip install pichak            # the plan, the disk sweep — no dependencies
pip install pichak[torch]     # the ramp, the learning rate, the GPU measurements
pip install pichak[hf]        # + transformers, for the corpus CLI

Python 3.9+. The core has no dependencies at all; torch is only needed for the parts that touch a GPU.

Where the numbers come from

Every measurement quoted here was made while fine-tuning a 24B model on a 6GB GTX 1060 and on a rented A6000 held down to the same 6GB. The raw logs — including the six runs that failed before one worked — are at flap-findings, along with the five hypotheses that sounded right and were wrong.

Licence

MIT. By Oleksandr Pichak.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pichak-0.1.0.tar.gz (25.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pichak-0.1.0-py3-none-any.whl (27.9 kB view details)

Uploaded Python 3

File details

Details for the file pichak-0.1.0.tar.gz.

File metadata

  • Download URL: pichak-0.1.0.tar.gz
  • Upload date:
  • Size: 25.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for pichak-0.1.0.tar.gz
Algorithm Hash digest
SHA256 7ebcf40c6d37bbbb1c1233e3712658857e07601db5975d2a41fe16460a860f52
MD5 f15e24c321cf9769db7445e3edd430df
BLAKE2b-256 e3fbf4ce3cea083e2e675ba2b7885942d61381bb5640fc32822365c6c08161d5

See more details on using hashes here.

File details

Details for the file pichak-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pichak-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 27.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for pichak-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 191bb4ec520c1bec2753d15edf0fd5b9c8e378b08a8488e0a83ee8946d4a8f25
MD5 de0e24fd92dc6ee80a6f5a6b444d5467
BLAKE2b-256 7ce03916c4d6501cc3cbeb96b74812424fe091819dccbf7f09138e9407936d24

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page