A Mica program can be told a few things when it starts, on its own command line, ahead of its own arguments:
./server +mica carriers=2 -mica --port 8080Everything between +mica and -mica belongs to the runtime. It is read before
any unit initializes and before the program’s first statement, and none of its
words reach the program: ArgCount and Arg see --port 8080 and nothing else.
What a program may be told is decided when it is built, not when it starts. A program tested with two carriers and shipped with that promise keeps two carriers wherever it is started. No environment variable takes part: every choice that changes a running program stands on the line that started it, where anyone can read it.
The flags
| Flag | Effect |
|---|---|
info | prints the program’s manifest, which says what the build decided, beside the configuration the start resolved from it and from the other flags in the group, as one line of JSON on standard output, and ends the start with exit status 0; nothing of the program runs |
check | prints everything info prints and every fact the start weighed on the way, as one line of JSON on standard output, and ends the start with exit status 0: the dry run before a long job. A group naming both info and check answers once, with check’s document |
carriers=<n> | runs the program’s tasks on n carriers, from 1 to 1024, in place of the build’s count; only a program built with --tasking multicore has carriers to choose |
processes=<n> | lets at most n worker processes, from 1 to 1024, run the tasks the program places on processes at once, in place of the build’s --processes count or the processors the program may use; see how many worker processes run at once |
gpu=<n> | runs the program’s device work on card n of the machine, counted from 0 as NVIDIA’s driver numbers the cards, in place of the first; check lists the cards by these numbers; see which card a program uses |
products=<file> | runs each float32, bfloat16 and float16 matrix product on a GPU by the routine measured fastest on the card for its shape, and keeps each choice in file; a later run naming the same file reads the choices instead of measuring again, so the runs print the same digits; see products measured on the card |
rtp=listen | starts the runtime protocol’s agent: the running program answers a client, such as the editor, over a Unix domain socket of its own at the default path, /run/user/<uid>/mica/<pid>.sock, or /tmp/mica-<uid>/<pid>.sock where the system has no runtime directory for the user |
rtp=suspend | starts the agent and holds the program before its first statement until a client sends runtime/continue; the program names its socket on standard error while it waits |
rtp=<path> | starts the agent on the socket at path, which holds a /, so a socket in the working directory is rtp=./agent.sock; combines with rtp=suspend |
stdout=line | writes each line of standard output through as soon as the program writes it. Without it, output to a pipe or a file waits until about 4 KiB have gathered or the program ends, so a notebook cell, a CI log or a supervising process sees nothing for a while. It costs one write to the operating system per line: a million short lines into a pipe took 0.63 s with the flag and 0.22 s without it |
info answers after every other flag in the group was read, so
+mica carriers=2 info -mica shows the pool the program would start with:
{"mica_runtime":1,"manifest":{"mica_manifest":1,...,"tasking":{"flavor":"multicore","carriers":4,"pinned":false,"processes":null},...},"configuration":{"carriers":{"effective":2,"source":"flag","oversubscribed":false},"processes":{"effective":16,"source":"processors","oversubscribed":false},"stdout":"default","products":null}}manifest is the program’s manifest, the
same document mica --inspect reads from the program file without starting
it. configuration is what only a start can know:
| Member | Meaning |
|---|---|
carriers.effective | the carrier count the pool sizes to |
carriers.source | where that count comes from: flag (the carriers flag), build (the build’s --tasking count, and the one carrier of a single build), or, for a multicore build without a count, the fact that limits the processors the program may use: processors, affinity or quota |
carriers.oversubscribed | true when the pool has more carriers than the processors the program may use, so some of them run in turn rather than at once |
processes.effective | how many worker processes may run tasks placed on processes at once |
processes.source | where that count comes from: flag (the processes flag), build (the build’s --processes count), or the fact that limits the processors the program may use: processors, affinity or quota |
processes.oversubscribed | true when that count is larger than the processors the program may use |
stdout | line when the stdout=line flag was given, default otherwise: line by line on a terminal, in blocks to a pipe or a file |
A program that places code on a GPU answers one more member, device, about
the card it would run on:
"device":{"provider":"nvidia","card":0,"name":"NVIDIA GeForce RTX 4090","generation":"sm_89","precompiled":true}| Member | Meaning |
|---|---|
device.provider | the provider that found a card, nvidia, or null when none did |
device.card | the card’s number: the one a gpu=<n> flag named, or 0, the first |
device.name | the card’s name, as NVIDIA’s driver states it, or null |
device.generation | the card’s generation, as sm_89, or null |
device.precompiled | true when every kernel the program runs, the device floor’s and its own, loads precompiled on this card, so NVIDIA’s runtime compiler never starts; false when some kernel compiles at the start, or no card was found |
device.reason | present when no card was found: each provider’s own words for why |
products | the record a products=<file> flag named, null otherwise |
check adds one member, check, with the facts behind the count:
taskset -c 0,1 ./server +mica check -mica{"mica_runtime":1,"manifest":{...},"configuration":{"carriers":{"effective":2,"source":"affinity","oversubscribed":false},"processes":{"effective":2,"source":"affinity","oversubscribed":false},"stdout":"default","products":null},"check":{"processors":{"online":16,"affinity":2,"quota":{"readable":true,"cgroup":null,"microseconds":null,"period":null,"carriers":null},"usable":{"carriers":2,"source":"affinity"}}}}| Member | Meaning |
|---|---|
processors.online | the processors online on the machine |
processors.affinity | the processors the program’s affinity mask allows, or null when the mask cannot be read |
processors.quota.readable | whether the cgroup hierarchy holding the program’s CPU controller could be read, v1 or v2; false when neither can be |
processors.quota.cgroup | the cgroup whose quota is the tightest (cpu.max on cgroup v2, cpu.cfs_quota_us over cpu.cfs_period_us on v1), or null when no quota applies |
processors.quota.microseconds, processors.quota.period | that quota: the processor time granted per period, both in microseconds |
processors.quota.carriers | the quota as whole processors, rounded up |
processors.usable | the carriers the program may usefully run, and the fact that limits them |
cards | present where the program places code on a GPU: every card the machine offers, each with the number gpu=<n> names it by, its name and its generation, or null when no provider can count the cards, which device.reason explains |
usable matters beside a count the build or a flag named: a program built with
--tasking multicore,8 and started in a container limited to two processors
still starts eight carriers, because the build’s count is a promise, and
check shows that six of them can never run at once, with
"oversubscribed":true.
How many carriers a pool starts
A program built with --tasking multicore and no count sizes its carrier pool
when it starts, to the least of three facts the kernel states:
- the processors online on the machine;
- the processors its affinity mask allows, as
tasksetor a container’s--cpuset-cpusrestricts it; - the CPU quota of its cgroup, as a container’s
--cpusor a Kubernetes CPU limit sets it, rounded up to whole processors. The tightest quota on the path from the program’s cgroup up to the cgroup root applies.
A program in a container limited to one and a half processors on a sixteen-processor machine starts two carriers, not sixteen. The quota is read from the cgroup hierarchy that holds the CPU controller: cgroup v2, or cgroup v1 on older hosts and hybrid layouts where the controller still sits there.
Any count from 1 to 1024 runs. On a sixteen-processor machine, a thousand tasks ran on 1024 carriers in 0.3 seconds with about 100 MB of memory, and gave the same results as on one carrier. Carriers beyond the processors spend processor time looking for work rather than doing it, so the processors the program may use are the healthy count, which is what a pool without a count starts with.
Which card a program uses
A program that places code on a GPU runs its device work on one card: the first, unless the start names another.
./train +mica check -mica # lists the machine's cards
./train +mica gpu=1 -mica # trains on the secondcheck lists every card with the number the flag names it by, as NVIDIA’s
driver numbers them:
"cards":[{"card":0,"name":"NVIDIA GeForce RTX 4090","generation":"sm_89"},{"card":1,"name":"NVIDIA GeForce RTX 3090 Ti","generation":"sm_86"}]The number is checked against the machine’s cards before the program’s first
statement, so a number with no card behind it is refused at the start rather
than at the program’s first device use, which may come late in a run. Which
card a program uses is a fact of the machine it runs on, not of its build, so
every policy that reads a group admits the flag, sealed included.
How many worker processes run at once
A task placed on a process runs in a worker process of its own. However many tasks a program places, at most so many workers run at once; a task placed beyond the count waits until a worker ends, and then runs. A thousand placements on a two-processor machine run two at a time rather than starting a thousand processes together.
The count is, in order:
- the
processes=<n>flag, where the build’s policy admits it; - the build’s
--processes <n>; - the processors the program may use when it starts, the same count a carrier pool without a count starts with.
A placement that waits holds no carrier: the program’s other tasks run on while it waits, and a cancelled scope ends the wait without starting a worker. A stream on a process takes no worker’s place, because its consumer paces it: a program may read a stream from a worker and place tasks on processes for what it reads, whatever the count.
Tasks placed on processes that wait for each other, such as a server and its
client placed side by side, need a count at least as large as the tasks that
wait together, which --processes states for every machine the program runs on.
Where the group stands
The group is read only at the very head of the arguments. A +mica anywhere
later is an ordinary argument of the program, so a runtime never takes words
that belong to the program. A program whose first argument must be the word
+mica is started with an empty group in front of it:
./tool +mica -mica +mica # the program receives one argument: +micaThe run mode passes the group through unchanged:
mica --run tool.mica +mica info -mica.
The build’s policy
--runtime-flags states what the built program accepts:
| Policy | Accepts | Default for |
|---|---|---|
open | every flag the build did not pin | the debug tier (--optimize debug, and --run unless it names a faster tier) |
sealed | only info, check, gpu=<n> and stdout=line | every release tier (O1, O2, release,...) |
sealed,rtp | info, check, gpu=<n>, stdout=line and the rtp= flags: a shipped program an editor or a monitoring tool may attach to when it is started to allow it | never a default |
none | nothing: the runtime reads no group, and +mica ... -mica are ordinary arguments | never a default |
A shipped release binary therefore answers questions about itself and changes
only where its build said so. stdout=line belongs with the questions: it
decides when the lines arrive, not what the program does, so a release service
under a supervisor or in CI shows its log as it writes it. gpu=<n> does too:
the card is the machine’s, so a release program trains on the card the one
starting it chooses.
Products measured on the card
A matrix product on a GPU runs by a routine of NVIDIA’s cuBLAS, which picks
one by its own ranking. That choice is right for most shapes on most cards,
and leaves time on some. With products=<file>, the first product of each
shape times the routines cuBLAS’s own cuBLASLt ranks for it, and then searches
past them through every configuration of the algorithms cuBLASLt lists for
the product’s types, on the card the program runs on, and keeps the fastest;
every later product of the shape runs by it. Products of float32, bfloat16
and float16 are measured; a float64 product keeps cuBLAS’s own choice.
cuBLAS’s own routine takes part in every measurement, and a measured routine
replaces it only when it is clearly faster, so a record never makes a program
slower. On an RTX 3090 Ti, the ranked routines took nanoGPT’s bfloat16
training step from 25.9 ms with cuBLAS’s own choice to 24.4 ms, and the
search past them to 23.8 ms.
The search costs its time once, in the run that writes the record: about twenty seconds over nanoGPT’s dozen product shapes, about forty over GPT-2 small’s. Every later run reads the record and measures nothing.
The routine decides in which order a product’s sums are added, so it decides the last digits of the answer. That is why the choice is kept: the record holds one line for each shape, and a run that names the same file reads its choices instead of measuring again. The run that measured and every run that reads its record print the same digits:
./train +mica products=train.products -mica # measures, and keeps the choices
./train +mica products=train.products -mica # reads them: the same digitsA record’s first line names the card’s architecture and the library’s
version it was measured with. A record made for another card or another
version is begun again, and the program says so on standard error. Which
routines may be chosen follows the build: a program built without
fp_half_intermediates chooses only among routines that add a product’s
partial sums in float32, as cuBLAS’s own choice does.
The flag changes which digits a build prints, so a build admits it only under
the open policy, the debug default.
Pinned values
A value spelled as fixed in the build cannot be changed at start, whatever the policy:
mica --tasking multicore,2,fixed ...,fixed stands behind a count and nowhere else.
Refusals
A flag the build does not allow ends the start before anything of the program has run: one line on standard error, and exit status 2.
| Situation | The line |
|---|---|
| the group has no end | mica runtime flags: the runtime flag group opened by '+mica' is not closed by '-mica' |
| an unknown flag | mica runtime flags: unknown runtime flag 'turbo'; the flags are 'info', 'check', 'carriers=N', 'processes=N', 'gpu=N', 'stdout=line', 'products=FILE' and 'rtp=...' |
| a record without a file | mica runtime flags: 'products=' is refused: the record of measured products is named by a file, 'products=FILE' |
| an unknown buffering | mica runtime flags: 'stdout=full' is refused: standard output's buffering is chosen with 'stdout=line' |
| a count out of range | mica runtime flags: 'carriers=0' is refused: the carrier count must be an integer between 1 and 1024 |
| a process count out of range | mica runtime flags: 'processes=0' is refused: the process count must be an integer between 1 and 1024 |
| a number with no card behind it | mica runtime flags: 'gpu=2' is refused: this machine has 2 cards, numbered 0 to 1 |
| a word that is no card number | mica runtime flags: 'gpu=-1' is refused: a card is named by its number, counted from 0, as 'gpu=0' |
| no provider counting the cards | mica runtime flags: 'gpu=0' is refused: no GPU provider counts this machine's cards: nvidia: libcuda.so.1 not found |
| a card named for a program without device code | mica runtime flags: 'gpu=0' is refused: this program places no code on a GPU |
| a sealed build | mica runtime flags: 'carriers=3' is refused: this program was built with '--runtime-flags sealed', which admits only 'info', 'check', 'gpu=N' and 'stdout=line' |
| a build sealed with the protocol | mica runtime flags: 'carriers=3' is refused: this program was built with '--runtime-flags sealed,rtp', which admits only 'info', 'check', 'gpu=N', 'stdout=line' and 'rtp=...' |
| the protocol in a sealed build | mica runtime flags: 'rtp=listen' is refused: this program was built with '--runtime-flags sealed', which does not admit the runtime protocol; a sealed build admits it with '--runtime-flags sealed,rtp' |
| an unknown protocol word | mica runtime flags: 'rtp=fast' is refused: the runtime protocol is asked for with 'rtp=listen', 'rtp=suspend' or 'rtp=<socket path>', a path holding a '/' |
| a second socket path | mica runtime flags: 'rtp=./b.sock' is refused: the runtime protocol listens on one socket, and 'rtp=./a.sock' named it already |
| a socket that cannot be opened | mica runtime flags: the runtime protocol could not start on './agent.sock': another process listens on the socket path, or the path is not a socket; the other reasons are named on the runtime protocol’s page |
| a pinned count | mica runtime flags: 'carriers=3' is refused: this program's build pinned 2 carriers ('--tasking multicore,2,fixed') |
| a single-carrier build | mica runtime flags: 'carriers=3' is refused: this program was built for one carrier ('--tasking single') |