CONTRIBUTING.md
Rama main · réplica de lectura del repositorio original.
# Contributing
Thanks for looking. This repo has a deliberately narrow goal: run
**Qwen3.8-27B well** on a single consumer GPU and serve it to Cursor. It
is not trying to become a general model launcher — Ollama and LM Studio
already do that. Contributions that make *this* model faster, or that make
the setup work on hardware it currently fails on, are the ones that fit.
## The most useful thing you can send
**A hardware report.** The context-window table in the README is
*computed* from the sizing formula in [`NOTES.md`](NOTES.md), and only the
RTX 5090 row has been validated by hand. If you ran this on a 3090, a
4090, a 7900 XTX or an Apple Silicon machine, open a
[hardware report](../../issues/new?template=hardware_report.yml) with the
window that actually loaded. That is how the table stops being an
estimate.
## Ground rules for tuned values
Several constants here are load-bearing and were derived from the
architecture rather than guessed:
- the `0.85` allocation-slack factor and `34,816` bytes/token in
`scripts/windows/probe_hardware.ps1`
- `q8_0` KV cache precision (**not** `q4_0` — see the recurrent-layer note
in the README)
- `--parallel 1`, and the 4096/1024 batch sizes
If you change one, say what you measured. `kiln bench`
(`benchmark.{bat,sh}`) and `kiln quality` (`quality.{bat,sh}`) exist so
that these can be checked rather than argued about. A PR that changes a number with a reason beats one that
changes it with a preference.
## Layout, and the one rule that matters
```
kiln.bat / kiln.sh the front door
scripts/windows/ everything cmd.exe runs
scripts/unix/ everything macOS and Linux run
scripts/*.py shared Python: hardware.py, opencode.py, pi_agent.py, ...
assets/ the wordmark
```
**Every script sets its working directory to the repo root, not to its
own directory.** `models/`, `llama-bin/`, `api_key.txt` and `server.log`
all live at the root and are referenced relatively, so a script that
skips this resolves them into `scripts/windows/` and silently does the
wrong thing.
```bat
cd /d "%~dp0..\.."
```
```bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR/../.."
```
Because the working directory is the repo root, a shell script must
source its libraries through `$SCRIPT_DIR`, not `./`:
```bash
. "$SCRIPT_DIR/lib_variants.sh"
```
Batch files call siblings with `%~dp0`, which is already relative to the
calling script, so those need no adjustment.
## Shell and batch conventions
Both script families are maintained in parallel — a change to
`scripts/unix/start_linux.sh` usually needs the matching change in
`scripts/windows/start.bat`.
- **Line endings are enforced by CI.** `.sh` must be LF, `.bat` and `.ps1`
must be CRLF. `.gitattributes` pins this regardless of your
`core.autocrlf`; do not fight it.
- **cmd.exe has traps** that have already bitten this repo — unescaped
parentheses inside `( )` blocks, `::` comments in blocks, `for /f`
splitting on commas, `timeout /t` failing under redirected stdin. They
are all written up in [`NOTES.md`](NOTES.md). Read that section before
editing a `.bat`.
- Shell scripts must pass `bash -n` and
`shellcheck --severity=error`.
- Call sibling scripts as `call "%~dp0foo.bat"`, never bare `call foo.bat`.
## Adding a model variant
`MODELS` in `scripts/hardware.py` is the only place that knows what a
build *is*: repo, filename prefix, Cursor alias, quant ladder with byte
sizes, MTP head, vision projector, sampling, draft length and minimum
llama.cpp build. `lib_variants.bat` and `lib_variants.sh` read it, and
everything else is variant-blind. A new variant is an entry there,
plus its place in `DISK_ORDER`, and in `FAMILY_ORDER` or `SIBLINGS` if
kiln may pick or suggest it. The Gemma 4 entries are the template for
a new architecture. NOTES.md says how their sizing was measured.
`.github/scripts/test_hardware.py` checks that the new entry's files
do not collide with an existing one.
## What never gets committed
Weights, `llama-bin/`, logs, and `api_key.txt`. `.gitignore` covers them
and CI fails if any get tracked anyway.
## Cutting a release
`kiln self-update` moves people to the newest `vX.Y.Z` tag, not to the
tip of `main`. A merge to `main` reaches only the people who opted in
with `--main`. Everyone else gets it at the next tag:
1. In `CHANGELOG.md`, rename `[Unreleased]` to the new version and
today's date, and start an empty `[Unreleased]` above it.
2. Commit that to `main` and wait for CI to pass.
3. Tag that commit and push the tag:
```bash
git tag -a v1.2.0 -m "v1.2.0"
git push origin v1.2.0
```
Publishing a GitHub release for the tag is optional. Self-update only
reads the tag.
- **Tag commits on `main`.** Users fast-forward their `main` to the
tag, which only works if the tag is on its history.
- **Never move a tag once it is pushed.** Anyone who already updated to
it is left on a commit the new tag does not descend from, and their
next update refuses. Ship the fix as the next patch version instead.
- **Only exact `vMAJOR.MINOR.PATCH` names count.** `v1.2.0-rc1` or
`test` is ignored, so a tag like that never reaches anyone.
Bump the patch number for fixes and the minor number for new commands
or changed defaults. Bump the major number when an update means
re-downloading weights or relearning commands.
## Running the checks locally
```bash
bash -n $(git ls-files '*.sh')
shellcheck --severity=error $(git ls-files '*.sh')
python3 .github/scripts/test_opencode.py
python3 .github/scripts/test_pi.py
python3 .github/scripts/test_nvidia_smi.py
python3 .github/scripts/test_selfupdate.py
python3 .github/scripts/test_control.py
python3 .github/scripts/test_tunnel.py
python3 .github/scripts/test_bench.py
python3 .github/scripts/test_path.py
python3 .github/scripts/test_tui.py # app tests need Textual installed
```
`kiln tui` runs kiln commands rather than reimplementing them. A new
verb reaches it through `build_args` in `scripts/control.py`, the one
place a request becomes a command line. Keep everything in it a literal
or a registry key.
Open an issue before a large change so you do not build something that
does not fit the scope above.