From 79e0f449f144c43e9c5701e44d723b118513bb9b Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Mon, 27 Jul 2026 14:07:40 -0700 Subject: [PATCH 01/30] =?UTF-8?q?llvm=20-exe:=20actually=20print=20the=20e?= =?UTF-8?q?xception=20=E2=80=94=20jit=5Frun=5Fmain=5Fguarded=20never=20flu?= =?UTF-8?q?shed=20its=20printer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot caught this on #3581 after it merged. jit_run_main_guarded built a TextPrinter and streamed "EXCEPTION: " into it, then returned without calling output(). TextWriter's destructor only frees largeBuffer; TextPrinter::output() is the sole thing in the class that printf+fflush's. So the print half of e2b818643 was dead code: a standalone -exe panic set the right exit status and said nothing. Why e2b818643's own check missed it: it validated by observation and shipped no fixture, so nothing automated ever asserted the text. The exit code was correct throughout, which is the half that was easy to see. Proven on this build, not argued: a `panic("boom from the exe entry")` fixture compiled with -jit -exe and run beside its runtime DLLs now prints EXCEPTION: boom from the exe entry and exits 1. The message is verbatim the literal in this function, which is what attributes the output to this line rather than to some other path on the panic rail. Still missing, and named here so it is not lost again: a regression test. An -exe fixture in CI is awkward (it needs the JIT plus a working linker, and clang-cl link discovery is already flaky on Windows locally), so the honest home for this is a tests-cpp case driving jit_run_main_guarded with a panicking context and capturing stdout. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019ruKZPG8G7SPEUyZtvBzcT --- src/builtin/module_jit.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/builtin/module_jit.cpp b/src/builtin/module_jit.cpp index 9615d7df2c..64856ecd31 100644 --- a/src/builtin/module_jit.cpp +++ b/src/builtin/module_jit.cpp @@ -1653,6 +1653,7 @@ DAS_API int32_t jit_run_main_guarded ( das::Context * ctx, void * mainFn, int32_ if ( !ok ) { das::TextPrinter tp; tp << "EXCEPTION: " << (ctx->getException() ? ctx->getException() : "unknown") << "\n"; + tp.output(); // TextWriter's dtor only frees its buffer — output() is what prints return 1; } return rc; From 8cb3a8b4f8fa3db24dd2887f8a7344628561f3ac Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Mon, 27 Jul 2026 19:02:04 -0700 Subject: [PATCH 02/30] dasllama-convert + fio direct writer: GLM conversion 383s -> 145s, peak commit 150 -> 82 GB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three pieces, measured on GLM-4.5-Air Q4_K_M (78.6 GB image): - fio dwrite_* — cache-bypassing strictly-ascending sequential writer (open/append/band/commit/stat/close). Win32: FILE_FLAG_NO_BUFFERING + preallocate (ascending writes keep NTFS VDL zero-fill off the bill); POSIX: fallocate + fadvise(DONTNEED); macOS: F_NOCACHE + F_PREALLOCATE. The handle owns a sector-aligned bounce band, so callers append any pointer at any size; dwrite_band/commit hand the band out for zero-copy producers; dwrite_stat splits stage vs syscall time. save_image rides it: buffered long_fwrite was worth 275 MB/s on big images, the direct writer sustains 1664-1720 MB/s per plane (rested- drive GLM aggregate 760 MB/s; per-plane MB/s now logged >= 256 MB). Meta serializes first so the exact file size preallocates up front; the header patches through a normal handle after close. - win32 mmap shim: PAGE_WRITECOPY -> PAGE_READONLY. Copy-on-write sections charge commit for every page that COULD be privatized, so a 73 GB gguf mapping billed 73 GB of private bytes on top of the model (measured 149.9 GB peak for the GLM conversion; 81.9 GB after). Now matches POSIX PROT_READ and faults loudly on an accidental write. - utils/dasllama-convert — offline GGUF -> .dlim (clargs; -m/-o/-q/ -f planar|metal|vulkan/--force/--keep-stale-tmp). Emits the identity- hashed name the load path looks for, and sweeps stale *.dlim..tmp siblings by default (killed runs leave full-size ones; 177.5 GB of them found on this box). Output verified byte-identical (SHA256) to the previous writer at 2.4 GB and 82 GB; the load path maps the tool's image directly. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WQW3fFpEpq8YtqNZTVh7jJ --- include/daScript/simulate/aot_builtin_fio.h | 6 + modules/dasLLAMA/dasllama/dasllama_image.das | 216 +++++++--- src/builtin/module_builtin_fio.cpp | 402 ++++++++++++++++++- utils/dasllama-convert/main.das | 180 +++++++++ 4 files changed, 737 insertions(+), 67 deletions(-) create mode 100644 utils/dasllama-convert/main.das diff --git a/include/daScript/simulate/aot_builtin_fio.h b/include/daScript/simulate/aot_builtin_fio.h index 34bac16a69..acc7570cc1 100644 --- a/include/daScript/simulate/aot_builtin_fio.h +++ b/include/daScript/simulate/aot_builtin_fio.h @@ -89,6 +89,12 @@ namespace das { DAS_API void builtin_map_file ( const FILE* _f, const TBlock>>& blk, Context*, LineInfoArg * at ); DAS_API void * builtin_fmap_open ( const char * name, uint64_t * size, Context * context, LineInfoArg * at ); DAS_API void builtin_fmap_close ( void * data, uint64_t size, Context * context, LineInfoArg * at ); + DAS_API void * builtin_dwrite_open ( const char * name, uint64_t total_bytes, uint64_t band_bytes, Context * context, LineInfoArg * at ); + DAS_API bool builtin_dwrite_append ( void * h, void * data, uint64_t bytes, Context * context, LineInfoArg * at ); + DAS_API void * builtin_dwrite_band ( void * h, uint64_t * avail, Context * context, LineInfoArg * at ); + DAS_API bool builtin_dwrite_commit ( void * h, uint64_t bytes, Context * context, LineInfoArg * at ); + DAS_API uint64_t builtin_dwrite_stat ( void * h, int32_t which, Context * context, LineInfoArg * at ); + DAS_API bool builtin_dwrite_close ( void * h, Context * context, LineInfoArg * at ); DAS_API char * builtin_dirname ( const char * name, Context * context, LineInfoArg * at ); DAS_API char * builtin_basename ( const char * name, Context * context, LineInfoArg * at ); DAS_API bool builtin_fstat ( const FILE * f, FStat & fs, Context * context, LineInfoArg * at ); diff --git a/modules/dasLLAMA/dasllama/dasllama_image.das b/modules/dasLLAMA/dasllama/dasllama_image.das index 6c0076b519..3789d2ec6c 100644 --- a/modules/dasLLAMA/dasllama/dasllama_image.das +++ b/modules/dasLLAMA/dasllama/dasllama_image.das @@ -72,16 +72,84 @@ def private pad_to_page(cur : uint64) : uint64 { return (cur + page - 1ul) / page * page } -def private write_zeros(f : file; n : uint64) { - if (n == 0ul) { +let private IMAGE_WRITE_BAND = 16l * 1024l * 1024l // the direct writer's staging band +let private SECTION_TABLE_SLACK = 65536ul // preallocation headroom for the section blob + +// The image's byte sink: append-only and strictly ascending, which is what keeps NTFS from +// charging valid-data-length zero-fill on the preallocated file (measured: 3.4 s per 8 GB gap). +struct private ImgWriter { + h : void? = null + cur : uint64 = 0ul + ok : bool = true +} + +def private w_open(path : string; total : uint64) : ImgWriter { + let h = unsafe(dwrite_open(path, total, uint64(IMAGE_WRITE_BAND))) + return ImgWriter(h = h, ok = h != null) +} + +def private w_append(var w : ImgWriter; p : void?; nbytes : uint64) { + if (!w.ok || nbytes == 0ul) { + return + } + if (!unsafe(dwrite_append(w.h, p, nbytes))) { + w.ok = false // a dead save stays dead — don't keep pushing bytes at a full disk + return + } + w.cur += nbytes +} + +def private w_zeros(var w : ImgWriter; n : uint64) { + if (n == 0ul || !w.ok) { return } var z : array z |> resize(int(n)) - f |> fwrite(z) + unsafe { + w_append(w, addr(z[0]), n) + } delete z } +def private w_close(var w : ImgWriter) : bool { + if (w.h == null) { + return false + } + let flushed = unsafe(dwrite_close(w.h)) + w.h = null + return flushed && w.ok +} + +def private plane_end(cur : uint64; field : array) : uint64 { + if (empty(field)) { + return cur + } + return pad_to_page(cur) + uint64(long_length(field)) * uint64(typeinfo sizeof(field[0])) +} + +// The image's final size, known before a byte goes out: planes contribute their sizes, the meta +// blob is already serialized. The writer preallocates from this, so the file lands as one +// contiguous run. +def private image_total_bytes(var t; meta_bytes : uint64) : uint64 { + var cur = uint64(IMAGE_PAGE) + apply(t) $ [unused_argument(name)] (name : string; var field) { + static_if (typeinfo is_array(field)) { + static_if (!typeinfo is_string(field[0])) { + cur = plane_end(cur, field) + } + } static_elif (typeinfo safe_has_field < image_map > (field)) { + apply(field) $ [unused_argument(iname)] (iname : string; var ifield) { + static_if (typeinfo is_array(ifield)) { + static_if (!typeinfo is_string(ifield[0])) { + cur = plane_end(cur, ifield) + } + } + } + } + } + return pad_to_page(cur + meta_bytes) +} + def private store_u32(var b : array; at : int; v : uint) { unsafe { var p = unsafe(addr < uint? >(b[at])) @@ -321,31 +389,33 @@ def private image_post_load(var t : Model) { // ===== save ===== -// one plane: pad to a page, bulk-write, account. long_ forms: a plane past 2 GiB -// (voxtral-mini qblob = 3.6 G elements) does not fit the int32 length/byte-count rail -def private write_plane(f : file; var sections : array; var cur : uint64&; - var ok : bool&; name : string; var field : array) { - if (empty(field) || !ok) { // a dead save stays dead — don't keep pushing bytes at a full disk +// one plane: pad to a page, bulk-append, account. The writer advances w.cur by exactly what it +// accepted, so the section table cannot drift from the file. long_ forms: a plane past 2 GiB +// (voxtral-mini qblob = 3.6 G elements) does not fit the int32 rail. +def private write_plane(var w : ImgWriter; var sections : array; + name : string; var field : array) { + if (empty(field) || !w.ok) { return } let nbytes = uint64(long_length(field)) * uint64(typeinfo sizeof(field[0])) - let at = pad_to_page(cur) - write_zeros(f, at - cur) - let wrote = f |> long_fwrite(field) - if (uint64(wrote) != nbytes) { - // a short write is an I/O failure (ENOSPC in practice), not an accounting bug - to_log(LOG_ERROR, "dasLLAMA image: plane '{name}' short write {wrote} of {int64(nbytes)} bytes at offset {int64(at)} — disk full? image save aborted\n") - ok = false + let at = pad_to_page(w.cur) + w_zeros(w, at - w.cur) + let ts = ref_time_ticks() + unsafe { + w_append(w, addr(field[0]), nbytes) + } + // big planes get their own line: a uniform rate means the cost is systemic, a few slow ones + // mean stalls. Only >=256 MB, so small models stay quiet and this needs no env knob. + if (nbytes >= 256ul * 1024ul * 1024ul) { + let ms = int64(get_time_usec(ts)) / 1000l + let mb = int64(nbytes) >> 20l + to_log(LOG_INFO, "dasLLAMA image: plane {name} {mb} MB in {ms} ms = {ms > 0l ? mb * 1000l / ms : 0l} MB/s\n") + } + if (!w.ok) { + to_log(LOG_ERROR, "dasLLAMA image: plane '{name}' failed writing {int64(nbytes)} bytes at offset {int64(at)} — disk full? image save aborted\n") return } sections |> push(ImgSection(name = name, off = at, bytes = nbytes)) - cur = at + nbytes - // fwrite and the section table must agree byte-for-byte, or the mapped planes land at - // the wrong offsets — fail the save loudly, not the load - if (uint64(ftell(f)) != cur) { - to_log(LOG_ERROR, "dasLLAMA image: plane '{name}' wrote {ftell(f)} != accounted {int64(cur)} (nbytes {int64(nbytes)}) — image save aborted\n") - ok = false - } } //! Serialize a loaded weight-carrying struct as a prepared image: array planes page-aligned raw @@ -355,27 +425,30 @@ def save_image(var t; path : string; tag : string = "") : bool { // per-writer tmp name: concurrent savers (parallel test workers on one cache miss) must // not clobber each other's half-written file; last atomic rename wins with same content let tmp_path = "{path}.{ref_time_ticks()}.tmp" - var ok = true var fend = 0ul + var meta_off = 0ul + var meta_bytes = 0ul + var nsections = 0 var meta <- new MemSerializer() var warch = Archive(reading = false, stream = meta) var sections : array - fopen(tmp_path, "wb") $(f) { - if (f == null) { - ok = false - return - } - // page 0: header placeholder — patched after the walk (fseek back) - write_zeros(f, uint64(IMAGE_PAGE)) - var cur = uint64(IMAGE_PAGE) - let ts_meta = ref_time_ticks() - _::serialize_image_meta(warch, t) - to_log(LOG_INFO, "dasLLAMA image: meta serialize {get_time_usec(ts_meta) / 1000}ms\n") + // meta first: it needs nothing from the planes, and having it sized up front is what lets + // the writer preallocate the exact file (see image_total_bytes) + let ts_meta = ref_time_ticks() + _::serialize_image_meta(warch, t) + to_log(LOG_INFO, "dasLLAMA image: meta serialize {get_time_usec(ts_meta) / 1000}ms\n") + var scalar_blob <- meta->extractData() + unsafe { + delete meta + } + var w = w_open(tmp_path, image_total_bytes(t, uint64(long_length(scalar_blob)) + SECTION_TABLE_SLACK)) + if (w.ok) { + w_zeros(w, uint64(IMAGE_PAGE)) // page 0: header placeholder — patched after close let ts_planes = ref_time_ticks() apply(t) $(name : string; var field) { static_if (typeinfo is_array(field)) { static_if (!typeinfo is_string(field[0])) { // string arrays ride the meta blob - write_plane(f, sections, cur, ok, name, field) + write_plane(w, sections, name, field) } } static_elif (typeinfo safe_has_field < image_map > (field)) { // a nested weight-carrier on the image rail (marker: it declares image_map) — @@ -384,54 +457,76 @@ def save_image(var t; path : string; tag : string = "") : bool { apply(field) $(iname : string; var ifield) { static_if (typeinfo is_array(ifield)) { static_if (!typeinfo is_string(ifield[0])) { - write_plane(f, sections, cur, ok, "{name}.{iname}", ifield) + write_plane(w, sections, "{name}.{iname}", ifield) } } } } } - to_log(LOG_INFO, "dasLLAMA image: planes write {get_time_usec(ts_planes) / 1000}ms\n") + let planes_ms = int64(get_time_usec(ts_planes)) / 1000l + let planes_mb = int64(w.cur) >> 20l + to_log(LOG_INFO, "dasLLAMA image: planes write {planes_ms}ms ({planes_mb} MB at {planes_ms > 0l ? planes_mb * 1000l / planes_ms : 0l} MB/s)\n") + // the writer's own split — staging our bytes vs waiting on the device. Anything the two + // do not account for is time spent OUTSIDE the writer (the walk itself). + let copy_ms = int64(unsafe(dwrite_stat(w.h, 0))) / 1000000l + let write_ms = int64(unsafe(dwrite_stat(w.h, 1))) / 1000000l + let direct_mb = int64(unsafe(dwrite_stat(w.h, 2))) >> 20l + let bounce_mb = int64(unsafe(dwrite_stat(w.h, 3))) >> 20l + to_log(LOG_INFO, "dasLLAMA image: writer split: stage {copy_ms}ms ({bounce_mb} MB bounced), syscall {write_ms}ms ({direct_mb} MB direct), unaccounted {planes_ms - copy_ms - write_ms}ms\n") // meta = [sections][the walk's scalar stream], one blob at the tail var smeta <- new MemSerializer() var sarch = Archive(reading = false, stream = smeta) sarch |> serialize(sections) var meta_blob <- smeta->extractData() - var scalar_blob <- meta->extractData() meta_blob |> push_from(scalar_blob) - let meta_off = cur - let meta_bytes = uint64(long_length(meta_blob)) - f |> fwrite(meta_blob) - cur += meta_bytes + nsections = length(sections) + meta_off = w.cur + meta_bytes = uint64(long_length(meta_blob)) + if (meta_bytes > 0ul) { // never empty in practice — the section table always rides here + unsafe { + w_append(w, addr(meta_blob[0]), meta_bytes) + } + } // pad the file itself to a page multiple (the whole-file bytesNoCopy wrap needs it) - fend = pad_to_page(cur) - write_zeros(f, fend - cur) - // patch the header + fend = pad_to_page(w.cur) + w_zeros(w, fend - w.cur) + delete meta_blob + unsafe { + delete smeta + } + delete sections + } + delete scalar_blob + var ok = w_close(w) + if (ok) { + // the header carries counts only the finished walk knows (section count, meta offset and + // size), and the direct writer is append-only — so patch page 0 through an ordinary + // handle now that the streaming one is closed var hdr : array hdr |> resize(int(IMAGE_HEADER_BYTES)) store_u32(hdr, 0, IMAGE_MAGIC) store_u32(hdr, 4, uint(IMAGE_VERSION)) store_u32(hdr, 8, uint(IMAGE_PAGE)) - store_u32(hdr, 12, uint(length(sections))) + store_u32(hdr, 12, uint(nsections)) store_u64(hdr, 16, meta_off) store_u64(hdr, 24, meta_bytes) store_u64(hdr, 32, fend) store_u64(hdr, 40, hash(image_identity(tag))) - fseek(f, 0l, seek_set) - f |> fwrite(hdr) - delete hdr - delete meta_blob - delete scalar_blob - unsafe { - delete smeta + ok = false + fopen(tmp_path, "r+b") $(hf) { + if (hf != null) { + fseek(hf, 0l, seek_set) + ok = int64(hf |> fwrite(hdr)) == IMAGE_HEADER_BYTES + } } - delete sections - } - unsafe { - delete meta + if (!ok) { + to_log(LOG_ERROR, "dasLLAMA image: '{tmp_path}' header patch failed — image save aborted\n") + } + delete hdr } if (ok) { - // buffered-tail backstop: the small tail writes can sit in the stdio buffer and hit - // ENOSPC only at fclose, which reports nothing here — verify what landed on disk + // last backstop: the writer reports its own failures, but an ENOSPC that only surfaces + // at close would still leave a short file — verify what actually landed on disk var st : FStat if (!stat(tmp_path, st) || st.size != fend) { to_log(LOG_ERROR, "dasLLAMA image: '{tmp_path}' is {int64(st.size)} bytes on disk, expected {int64(fend)} — disk full? image save aborted\n") @@ -439,7 +534,8 @@ def save_image(var t; path : string; tag : string = "") : bool { } } if (!ok) { - if (!remove(tmp_path)) { + // a failed OPEN never created the file, so only warn about a tmp that is actually there + if (stat(tmp_path).is_valid && !remove(tmp_path)) { to_log(LOG_WARNING, "dasLLAMA image: orphaned tmp '{tmp_path}' could not be removed — clean it up by hand\n") } return false diff --git a/src/builtin/module_builtin_fio.cpp b/src/builtin/module_builtin_fio.cpp index a7c0c4005f..e496332943 100644 --- a/src/builtin/module_builtin_fio.cpp +++ b/src/builtin/module_builtin_fio.cpp @@ -43,6 +43,18 @@ MAKE_TYPE_FACTORY(clock, das::Time)// use MAKE_TYPE_FACTORY out of namespace. So #define getchar_wrapper getchar #endif +// The direct sequential writer (dwrite_*): cache-bypassing, strictly-ascending append, used by +// bulk producers that already know their total size — measured 2060 MB/s vs 275 MB/s for the same +// volume through buffered fwrite, and it leaves the page cache to whatever the producer is READING. +// Implemented per-platform at the bottom of this file (same split as the mmap shim above); the +// handle owns an aligned bounce buffer, so callers append any pointer at any size. +void * das_dwrite_open ( const char * path, uint64_t total_bytes, uint64_t band_bytes ); +bool das_dwrite_append ( void * h, const void * data, uint64_t bytes ); +void * das_dwrite_band ( void * h, uint64_t * avail ); +bool das_dwrite_commit ( void * h, uint64_t bytes ); +uint64_t das_dwrite_stat ( void * h, int which ); +bool das_dwrite_close ( void * h ); + namespace das { struct TimeAnnotation : ManagedValueAnnotation