Bauen
Erweiterungen erstellen
Scellis zieht die Eigentumsgrenze an einer ungewohnten Stelle: Der Maintainer besitzt die Grammatik, den Checker und das Referenz-Orakel — sonst nichts. Jede Instanz gehört dir. Alles, was der Maintainer berechnen kann, erstellst du selbst aus der Oberfläche heraus — ohne Engine-Patch, ohne Freigabe-Warteschlange. Das Credo hinter dem Design ist deutlich: „Wenn ein Nutzer ohne mich keinen Laplace-Operator erstellen kann, gehört das Substrat mir — nicht ihm.“ Diese Seite ist der praktische Rundgang durch diesen Anspruch: was erstellbar ist, wie eine erstellte Op konkret aussieht, wie ein Kernel Vertrauen verdient — und wie dein Code auf allem aufbaut, was schon im Katalog liegt.
Die erstellbare Oberfläche#
Die erstellbare Oberfläche ist das eigene Vokabular der Plattform, Ende zu Ende — keine seitlich angeschraubte Plugin-Nische:
- Berechnung: Blöcke — Composite-Blöcke, deren Körper ein Graph als Daten ist, und Funktionsblöcke in der Sandbox — dazu Ops mit deklariertem Shape- und Typverhalten sowie GPU-Kernel, gepaart mit CPU-Referenzen.
- Training: Gradientenregeln, Optimizer, Scheduler, Losses, Metriken, PEFT-Adapter-Arten und die Aggregationsstrategien, auf denen kollaboratives Training läuft.
- Oberfläche und Daten: Viewer, Connectors, Format-Codecs, Modelle, Datensätze — und die Packs, die all das ausliefern.
Was der Maintainer behält, sind genau drei Grammatiken: eine Shape-/Signatursprache, in der Ops ihre Verträge deklarieren; eine Buffer-Binding-ABI für Storage-Buffer, Uniforms und Workgroup-Shared-Scratch; und eine Backward-(VJP-)Repräsentation. Alles andere ist eine Instanz über diesen Grammatiken — und weil die Engine die Grammatiken generisch interpretiert, kann sie beim Dispatch einen Built-in nicht von einer Nutzer-Entität unterscheiden. Der Unterschied ist ein Herkunfts-Chip im Katalog, nie ein eigener Codepfad.
Anatomie einer erstellten Op#
Du erstellst in einem Formular, das das Studio aus dem Op-Schema rendert — und der Copilot kann denselben Datensatz entwerfen, denn er schreibt über dieselbe API. Darunter hat der Datensatz eine Form:
- name: user/ada/mish # namespaced — never shadows a builtin
version: 1
summary: "Mish activation: out = a * tanh(softplus(a))"
taxonomy: webir.op.elementwise
inputs: [{ name: a, dtype: [f32], shape: ["..."] }]
outputs: [{ name: out, dtype: [f32], shape: ["..."] }]
tolerance: { class: tight } # justified against the op's semantics
determinism: { debug: deterministic, prod: deterministic_within_tolerance }
gradients: { kind: elementwise, vjp_rule: "grad_out * mish'(a)" }
kernels: { forward: [{ id: user/ada/mish_f32, when: { dtype: f32 } }] }Jedes Feld ist ein geprüfter Vertrag, keine Dokumentation. Die Id ist namensraumgebunden (user/<owner>/<name>), sodass eine Nutzer-Op neben einem Built-in steht, nie an seiner Stelle. Die Shape-Strings sind eine interpretierte Signatur — eine Op, deren Ausgabegröße von Datenwerten abhängt, deklariert genau das, mit einer Worst-Case-Schranke, damit Speicher planbar bleibt. Die Toleranzklasse muss zur Semantik der Op passen: Ein Matmul kann nicht loose deklarieren, um der Konformität auszuweichen. Und Determinismus wird pro Stufe deklariert — debug, prod, fast — nie still per Default gesetzt.
Der Kernel und seine Referenz#
Erstellte Kernel sind kein eingeschränkter Dialekt. Die Binding-ABI deckt lesende und schreibende Storage-Buffer, Uniforms und Workgroup-Shared-Scratch ab — ein gekacheltes Matmul, eine Reduktion oder ein Stencil ist also erstellbar, nicht nur eine elementweise Abbildung. Ein minimaler Forward-Kernel zur Op oben:
@group(0) @binding(0) var<storage, read> in0 : array<f32>;
@group(0) @binding(1) var<storage, read_write> out : array<f32>;
@group(0) @binding(2) var<uniform> p : Params;
@compute @workgroup_size(256, 1, 1)
fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
let i = gid.x;
if (i >= p.outNumel) { return; }
let x = in0[i];
out[i] = x * tanh(log(1.0 + exp(x))); // must match the reference within "tight"
}Bei der Paarungsregel beginnt das Vertrauen. Neben dem WGSL lieferst du eine Referenzimplementierung — in einer geschlossenen skalaren Grammatik oder als Sandbox-JS mit niedrigerer Vertrauensobergrenze. Der Konformitätsläufer generiert Testfälle, führt beide aus und verlangt Übereinstimmung innerhalb der deklarierten Toleranzklasse. Das Bestehen prägt ein hash-gebundenes ConformanceReceipt, das mit dem Kernel reist und im Katalog sichtbar ist — dasselbe Regime, das auf der Korrektheits-Seite jeden Built-in prüft, über derselben IR-Leiter.
Ein ehrliches Detail: Das Receipt ist profilgebunden. Es belegt die Paarung auf genau der GPU-Umgebung, auf der es lief; auf einer anderen Geräteklasse läuft die Prüfung lokal erneut. Receipts reisen nie als importiertes Vertrauen — das würde mehr behaupten, als getestet wurde.
Hinweis
Konformität schaltet vertrauenswürdige Aktivierung und Teilen frei — nie deine eigene Schleife. Deinen eigenen, noch unkonformen Kernel kannst du auf deinem Gerät jederzeit ausführen: schreiben, laufen lassen, prüfen, korrigieren, wieder laufen lassen.
Auch Gradienten und Reducer verdienen Vertrauen#
Ein deklarierter Backward ist ein gewöhnlicher komponierter Op-Graph — dieselbe Ausdruckskraft wie ein Forward — und er wird per Gradient-Check mit finiten Differenzen gegen deinen eigenen Forward geprüft, inklusive einer verpflichtenden Negativkontrolle. Offen gesagt: Der Gradient-Check ist eine starke notwendige Bedingung, kein formaler Beweis. Ein nutzerdeklarierter Reducer geht weiter — seine behauptete Algebra (Assoziativität, Kommutativität) wird vor der Aktivierung property-getestet, denn ein falsch deklarierter Kombinierer würde per Baum-Reduktion still zu falschen Zahlen führen.
Den Katalog wiederverwenden wie eine Bibliothek#
Ein erstellter Körper startet beim ganzen Katalog, nicht bei null. Das Design-Mandat lautet „wie pandas importieren und einen DataFrame benutzen“ — aus jedem erstellten Körper heraus rufst du andere Entitäten über eine einzige, kontrollierte Oberfläche auf:
// Inside any authored body: the governed call surface.
const y = ctx.op("core/softmax", x, { axis: -1 }); // a builtin, resolved by name
const z = ctx.call("user/kim/zscore@3", y); // another author's block
const s = ctx.tensor.matmul(z, w); // the tensor-algebra surface
// Every reference resolves through YOUR pinned uses closure —
// never against "whatever happens to be newest".Die Buchführung ist nicht dein Job. Abhängigkeitskanten werden automatisch aus deinen Aufrufstellen abgeleitet und auf exakt die Versionen gepinnt, die beim Erstellen aufgelöst wurden; das Lockfile hält ihre Hashes fest. Danach löst sich jeder Laufzeitaufruf durch diesen Pin auf — die Aufrufe und die deklarierte Closure sind dieselbe Menge: Was du aufrufst, ist exakt, was du gepinnt hast. Eine erstellte Entität ist damit per Konstruktion reproduzierbar und offline sicher ausführbar; ein Abhängigkeitszyklus oder ein unauflösbarer Pin wird laut abgelehnt, nie zu „das Neueste gewinnt“ aufgelöst.
Zwei Eigenschaften halten die Komposition ehrlich. Effekte sind transitiv über die Closure: Eine als pur deklarierte Funktion kann nicht leise einen Helfer mit Netzwerk-Effekt aufrufen, denn die Effekte des Aufgerufenen zählen für die ganze Kette, und die Einwilligung wird über die Vereinigung eingeholt. Und Vertrauen komponiert konservativ: Ein Aufgerufener läuft auf der strengsten Vertrauensstufe seiner Aufrufkette — nicht vertrauenswürdiger Code kann sich nicht über vertrauenswürdige Aufrufer reinwaschen.
Ein Check, eine Messlatte — und ehrliche Namen#
Jede erstellte Entität durchläuft dieselbe Prüfung auf ihrem vollständigen Datensatz — beim Erstellen und erneut bei jeder Bearbeitung. Es ist exakt die Prüfung, die auch jeden Built-in prüft: Fehlende oder unvollständige Angaben werden abgelehnt, und die Ablehnung benennt, was fehlt. Built-ins sind unveränderlich — einen zu „bearbeiten“ forkt eine erstklassige Kopie, die dir gehört, mit festgehaltener Herkunft; deine eigenen Entitäten bearbeitest du direkt, über denselben validierten Pfad.
Auch Überschreiben und Namen bleiben ehrlich. Jede Op, die einen erstellbaren Binding-Vertrag deklariert, ist überschreibbar — die Override-Menge ist eine Katalogabfrage, keine fest verdrahtete Liste — und auf deinem Gerät gewinnt der Kernel deines Workspaces vor dem eines installierten Packs, der vor dem eingebauten Kern gewinnt. Aber eine Nutzer-Op kann sich nie als Built-in ausgeben: Erstellte Ids leben unter user/<owner>/<name>, und ein blanker Built-in-Name löst immer zur Core-Op auf. Überschreiben ist eine lokale Dispatch-Präferenz; Identitätsübernahme ist strukturell ausgeschlossen.
Veröffentlichen ist ein eigener Schritt#
Vertrauensstufen — unverified, community, verified, official — werden pro Artefakt verdient und sind orthogonal zur Autorschaft: Nutzer-Entitäten können verified erreichen. Ist eine Entität bereit, dein Gerät zu verlassen, packst du sie, signierst sie im Browser und bestehst die Publikations-Gates: ein aufgelöstes Lockfile, Konformitätsbelege wo anwendbar, vollständige Metadaten und eine explizite Lizenz. Dieser Weg — und was Käufer und Installierende auf der anderen Seite sehen — ist Thema von Packs und Veröffentlichen. Ist dein Werk eine Trainingskomponente, taucht sie jetzt in den Model-View-Pickern auf wie jede mitgelieferte — siehe Training im Browser.