@mcmcjs/julia 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fitlib.jl CHANGED
@@ -17,6 +17,8 @@ using Pkg
17
17
  import JuliaBUGS
18
18
  import AdvancedHMC
19
19
  import ForwardDiff
20
+ # Loading Mooncake activates AbstractPPL's native extension for AutoMooncake.
21
+ import Mooncake
20
22
  import FlexiChains
21
23
  import DimensionalData
22
24
 
@@ -131,6 +133,9 @@ bugs_namedtuple(data) = (; (Symbol(k) => narrow(v) for (k, v) in data)...)
131
133
  to_missing(x) = x === nothing ? missing : (x isa AbstractVector ? map(to_missing, x) : x)
132
134
  predict_namedtuple(data) = (; (Symbol(k) => to_missing(v) for (k, v) in data)...)
133
135
 
136
+ # A blanked target compiles to a latent node; JuliaBUGS drops scalar missings.
137
+ bugs_predict_namedtuple(data) = (; (Symbol(k) => narrow(to_missing(v)) for (k, v) in data)...)
138
+
134
139
  # Rebuild a posterior Chains (latents only) from our MCMCChains-JSON wire object so
135
140
  # it can feed Turing.predict. The varname_to_symbol map is required for VarName
136
141
  # indexing; target outcome columns are excluded.
@@ -297,17 +302,164 @@ function vnchain_to_wire(chn)
297
302
  end
298
303
 
299
304
  # JuliaBUGS compiles the model at runtime, so building and sampling must run in
300
- # separate latest-world frames; merging them reintroduces a world-age error. The
301
- # result is already an MCMCChains.Chains, so it is not re-wrapped.
302
- function sample_bugs(model, sampler, warmup, draws, chains, rng)
305
+ # separate latest-world frames; merging them reintroduces a world-age error.
306
+ # Returns a FlexiChain like the Turing path; gen_chains recovers generated
307
+ # quantities (unobserved nodes with no observed descendants) after sampling.
308
+ # With a callback, draws stream (see bugs_draw_streamer).
309
+ function sample_bugs(model, sampler, warmup, draws, chains, rng; callback = nothing)
310
+ kw = callback === nothing ? NamedTuple() : (; callback)
303
311
  return Logging.with_logger(JSONProgressLogger()) do
304
312
  JuliaBUGS.AbstractMCMC.sample(
305
313
  rng, model, sampler, JuliaBUGS.AbstractMCMC.MCMCSerial(), draws, chains;
306
- chain_type = MCMCChains.Chains, n_adapts = warmup, discard_initial = warmup, progress = true,
314
+ chain_type = FlexiChains.VNChain, n_adapts = warmup, discard_initial = warmup,
315
+ progress = true, kw...,
307
316
  )
308
317
  end
309
318
  end
310
319
 
320
+ # The JuliaBUGS counterpart to draw_streamer. A JuliaBUGS transition is a raw
321
+ # AdvancedHMC.Transition whose params are an unconstrained vector, so (unlike the
322
+ # Turing path) draws cannot be read off it directly. Per batch we rebuild a small
323
+ # FlexiChain through the same gen_chains + vnchain_to_wire path the final file
324
+ # uses, so the streamed leaf names (parameters, generated quantities, and sampler
325
+ # stats) match the file exactly. Reconstruction runs on a copy of the model so it
326
+ # never perturbs the sampler's state. AbstractMCMC passes the 1-based chain index
327
+ # as chain_number; each chain's batches carry a per-chain monotonic seq.
328
+ # TODO: once the pinned JuliaBUGS carries TuringLang/JuliaBUGS.jl#517 (with
329
+ # AbstractMCMC#212 and AbstractPPL#178), a transition exposes named constrained
330
+ # params (a VarNamedTuple) plus generated quantities, so this collapses to a
331
+ # per-draw flatten like draw_streamer, dropping the deepcopy and reconstruction.
332
+ function bugs_draw_streamer(model, draws_per_chain::Int, batch_size::Int)
333
+ recon = deepcopy(model)
334
+ side_rng = StableRNG(0)
335
+ chain = Ref(-1)
336
+ seq = Ref(0)
337
+ θs = Vector{Vector{Float64}}()
338
+ lps = Float64[]
339
+ stats = Any[]
340
+ function emit(iteration)
341
+ isempty(θs) && return
342
+ stats_names = collect(keys(merge((; lp = lps[1]), stats[1])))
343
+ stats_values = [vcat(lps[i], collect(values(stats[i]))) for i in eachindex(θs)]
344
+ chn = JuliaBUGS.gen_chains(FlexiChains.VNChain, recon, θs, stats_names, stats_values; rng = side_rng)
345
+ wire = vnchain_to_wire(chn)
346
+ nIter, total, _ = wire["size"]
347
+ flat = wire["value_flat"]
348
+ params = wire["parameters"]
349
+ cols = Dict(params[p] => [flat[i + (p - 1) * nIter] for i in 1:nIter] for p in 1:total)
350
+ println(
351
+ PROGRESS_IO[],
352
+ JSON.json(
353
+ Dict(
354
+ "mcmcjs" => "draws",
355
+ "chain" => chain[],
356
+ "seq" => seq[],
357
+ "iteration" => iteration,
358
+ "draws" => cols,
359
+ ),
360
+ ),
361
+ )
362
+ flush(PROGRESS_IO[])
363
+ seq[] += 1
364
+ empty!(θs)
365
+ empty!(lps)
366
+ empty!(stats)
367
+ end
368
+ return function (_rng, _model, _sampler, transition, _state, iteration; chain_number = 1, _kwargs...)
369
+ c = Int(chain_number) - 1
370
+ if c != chain[]
371
+ chain[] = c
372
+ seq[] = 0
373
+ empty!(θs)
374
+ empty!(lps)
375
+ empty!(stats)
376
+ end
377
+ push!(θs, copy(transition.z.θ))
378
+ push!(lps, transition.z.ℓπ.value)
379
+ push!(stats, AdvancedHMC.stat(transition))
380
+ if length(θs) >= batch_size || iteration >= draws_per_chain
381
+ emit(iteration)
382
+ end
383
+ end
384
+ end
385
+
386
+ # JuliaBUGS posterior prediction: condition the (target-blanked) model on the
387
+ # posterior parameter columns, then evaluate!! ancestral-samples the remaining
388
+ # latents once per posterior draw. No sampler or gradient runs.
389
+ function predict_bugs(model, posterior, targets, rng)
390
+ model isa JuliaBUGS.BUGSModelWithGradient && (model = model.base_model)
391
+ APPL = JuliaBUGS.AbstractPPL
392
+ target_syms = Set(Symbol.(targets))
393
+ is_target(vn) = APPL.getsym(vn) in target_syms
394
+
395
+ sz = posterior["size"]
396
+ nIter, nCols, nChains = Int(sz[1]), Int(sz[2]), Int(sz[3])
397
+ flat = posterior["value_flat"]
398
+ col = Dict(n => i for (i, n) in enumerate(posterior["parameters"]))
399
+ posterior_names = Set(posterior["name_map"]["parameters"])
400
+ cell(i, p, c) = begin
401
+ v = flat[i + (p - 1) * nIter + (c - 1) * nIter * nCols]
402
+ v === nothing ? NaN : Float64(v)
403
+ end
404
+
405
+ # Nodes fully covered by posterior columns are conditioned; anything
406
+ # uncovered stays latent and is forward-sampled, like Turing.predict.
407
+ gd = model.graph_evaluation_data
408
+ conditioned = APPL.VarName[]
409
+ restore = Tuple{APPL.VarName,Int}[]
410
+ for (i, vn) in enumerate(gd.sorted_nodes)
411
+ (gd.is_stochastic_vals[i] && !gd.is_observed_vals[i] && !is_target(vn)) || continue
412
+ leaves = collect(APPL.varname_leaves(vn, APPL.getvalue(model.evaluation_env, vn)))
413
+ found = count(l -> string(l) in posterior_names, leaves)
414
+ if found == length(leaves)
415
+ push!(conditioned, vn)
416
+ append!(restore, [(l, col[string(l)]) for l in leaves])
417
+ elseif found > 0
418
+ error("the posterior samples cover $(vn) only partially; refit before predicting")
419
+ end
420
+ end
421
+ isempty(conditioned) &&
422
+ error("the posterior samples share no parameters with the model; were they produced by fitting this spec?")
423
+ cond = APPL.condition(model, conditioned)
424
+
425
+ any(is_target, cond.graph_evaluation_data.sorted_nodes) ||
426
+ error("no variables predicted; check that [predict].targets name model variables")
427
+
428
+ out_leaves = APPL.VarName[]
429
+ arr = Array{Float64,3}(undef, 0, 0, 0)
430
+ for c in 1:nChains, i in 1:nIter
431
+ env = cond.evaluation_env
432
+ for (leaf, p) in restore
433
+ env = JuliaBUGS.BangBang.setindex!!(env, cell(i, p, c), leaf)
434
+ end
435
+ cond = JuliaBUGS.BangBang.setproperty!!(cond, :evaluation_env, env)
436
+ drawn, _ = APPL.evaluate!!(rng, cond)
437
+ if isempty(out_leaves)
438
+ out_leaves = [
439
+ l for vn in cond.graph_evaluation_data.sorted_nodes if is_target(vn)
440
+ for l in APPL.varname_leaves(vn, APPL.getvalue(drawn, vn))
441
+ ]
442
+ arr = Array{Float64,3}(undef, nIter, length(out_leaves), nChains)
443
+ end
444
+ for (k, leaf) in enumerate(out_leaves)
445
+ arr[i, k, c] = Float64(APPL.getvalue(drawn, leaf))
446
+ end
447
+ end
448
+
449
+ pnames = string.(out_leaves)
450
+ n = length(pnames)
451
+ flat_out = Vector{Union{Float64,Nothing}}(undef, nIter * n * nChains)
452
+ for c in 1:nChains, p in 1:n, i in 1:nIter
453
+ flat_out[i + (p - 1) * nIter + (c - 1) * nIter * n] = arr[i, p, c]
454
+ end
455
+ return Dict(
456
+ "size" => [nIter, n, nChains],
457
+ "value_flat" => flat_out,
458
+ "parameters" => pnames,
459
+ "name_map" => Dict("parameters" => pnames, "internals" => String[]),
460
+ )
461
+ end
462
+
311
463
  # Build the exact wire object parseMCMCChainsJson consumes: value_flat is indexed
312
464
  # i + p*nIter + c*nIter*nParams (column-major over [iteration, parameter, chain]).
313
465
  function chains_to_wire(chn)
@@ -333,7 +485,7 @@ function provenance()
333
485
  packages = Dict{String,String}()
334
486
  for (_, info) in Pkg.dependencies()
335
487
  if info.name in
336
- ("Turing", "FlexiChains", "MCMCChains", "DynamicPPL", "AbstractMCMC", "JuliaBUGS", "AdvancedHMC", "ForwardDiff", "ADTypes") &&
488
+ ("Turing", "FlexiChains", "MCMCChains", "DynamicPPL", "AbstractMCMC", "JuliaBUGS", "AdvancedHMC", "ForwardDiff", "Mooncake", "ADTypes") &&
337
489
  info.version !== nothing
338
490
  packages[info.name] = string(info.version)
339
491
  end
@@ -372,15 +524,22 @@ function handle_request(request)
372
524
  entry = resolve_entry(Main, get(request["model"], "entry", nothing))
373
525
 
374
526
  wire = if mode == "predict"
375
- backend == "turing" || error("predict is not yet supported for backend $backend")
376
- model = Base.invokelatest(entry, ModelData(predict_namedtuple(request["data"])))
377
- stage = "load_samples"
378
- rchn = chains_from_wire(JSON.parsefile(request["samples"]), request["predict"]["targets"])
379
- stage = "predict"
380
- pp = Base.invokelatest(predict, rng, model, rchn)
381
- isempty(names(pp)) &&
382
- error("no variables predicted; check that [predict].targets name unconditioned outcomes")
383
- chains_to_wire(pp)
527
+ if backend == "juliabugs"
528
+ model = Base.invokelatest(entry, bugs_predict_namedtuple(request["data"]))
529
+ stage = "load_samples"
530
+ posterior = JSON.parsefile(request["samples"])
531
+ stage = "predict"
532
+ Base.invokelatest(predict_bugs, model, posterior, request["predict"]["targets"], rng)
533
+ else
534
+ model = Base.invokelatest(entry, ModelData(predict_namedtuple(request["data"])))
535
+ stage = "load_samples"
536
+ rchn = chains_from_wire(JSON.parsefile(request["samples"]), request["predict"]["targets"])
537
+ stage = "predict"
538
+ pp = Base.invokelatest(predict, rng, model, rchn)
539
+ isempty(names(pp)) &&
540
+ error("no variables predicted; check that [predict].targets name unconditioned outcomes")
541
+ chains_to_wire(pp)
542
+ end
384
543
  else
385
544
  data = backend == "juliabugs" ? bugs_namedtuple(request["data"]) : ModelData(to_namedtuple(request["data"]))
386
545
  sampler, warmup = build_sampler(request["sampler"], backend)
@@ -389,8 +548,14 @@ function handle_request(request)
389
548
  stage = "sample"
390
549
  if backend == "juliabugs"
391
550
  model = Base.invokelatest(entry, data)
392
- chn = Base.invokelatest(sample_bugs, model, sampler, warmup, draws, chains, rng)
393
- chains_to_wire(chn)
551
+ # Reconstruction reads the plain BUGSModel; entry may return a gradient wrapper.
552
+ base = hasproperty(model, :base_model) ? model.base_model : model
553
+ cb = get(request, "stream_draws", false) ?
554
+ bugs_draw_streamer(base, draws, Int(get(request, "draw_batch_size", 25))) : nothing
555
+ chn = Base.invokelatest(
556
+ sample_bugs, model, sampler, warmup, draws, chains, rng; callback = cb,
557
+ )
558
+ vnchain_to_wire(chn)
394
559
  else
395
560
  cb = get(request, "stream_draws", false) ?
396
561
  draw_streamer(draws, Int(get(request, "draw_batch_size", 25))) : nothing
package/dist/index.cjs CHANGED
@@ -268,6 +268,7 @@ async function runFit(spec, resolved, io) {
268
268
  ({ stdout, stderr, code, cancelled } = await io.spawn(resolved.command, args, {
269
269
  onProgress: io.onProgress,
270
270
  onDraws: io.onDraws,
271
+ onLog: io.onLog,
271
272
  signal: io.signal
272
273
  }));
273
274
  } finally {
@@ -397,6 +398,7 @@ var PACKAGES = [
397
398
  "JuliaBUGS",
398
399
  "AdvancedHMC",
399
400
  "ForwardDiff",
401
+ "Mooncake",
400
402
  "JSON",
401
403
  "StableRNGs",
402
404
  "StatsFuns"