@mcmcjs/julia 0.6.1 → 0.7.1

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/README.md CHANGED
@@ -1,11 +1,17 @@
1
1
  # @mcmcjs/julia
2
2
 
3
- The bridge to the Julia ecosystem for [MCMC.js](https://github.com/mcmcjs/mcmcjs):
4
- detecting the installed Julia toolchain (juliaup and Julia), and (later)
5
- bootstrapping it and running inference via Turing.jl / JuliaBUGS.
3
+ The bridge to the Julia ecosystem for [MCMC.js](https://github.com/mcmcjs/mcmcjs).
4
+
5
+ It detects and bootstraps the Julia toolchain via juliaup, manages a pinned, per-user Julia project, and runs inference as a subprocess.
6
+
7
+ ## What it provides
8
+
9
+ - **Toolchain** — detect juliaup and Julia (`detectJulia`/`detectJuliaup`), install them (`planSetup`/`runSetup`), and manage versions (`list`/`add`/`remove`/`default`/`update`/`gc`) over juliaup channels.
10
+ - **Managed project** — provision an isolated Julia project with Turing.jl and JuliaBUGS (and friends) and precompile it (`ensureProject`).
11
+ - **Driver** — the `fit` and `predict` runner (`driver.jl`) that runs Turing or JuliaBUGS and writes the MCMCChains-JSON samples file plus a run record.
6
12
 
7
13
  > Early alpha: the API is not yet stable.
8
14
 
9
15
  ## License
10
16
 
11
- [MIT](./LICENSE) (c) [Shravan Goswami](https://shravangoswami.com)
17
+ [MIT](./LICENSE) © [Shravan Goswami](https://shravangoswami.com)
package/dist/fitlib.jl CHANGED
@@ -96,6 +96,21 @@ end
96
96
 
97
97
  to_namedtuple(data) = (; (Symbol(k) => v for (k, v) in data)...)
98
98
 
99
+ # Binds the data so a model can read a variable as a property (`data.y`) or by
100
+ # index (`data["y"]` / `data[:y]`), with `haskey`/`keys` supported, so models
101
+ # written in either idiom run unchanged. The underlying NamedTuple is reached
102
+ # via getfield to avoid colliding with a data variable also named `nt`.
103
+ struct ModelData
104
+ nt::NamedTuple
105
+ end
106
+ Base.getproperty(d::ModelData, k::Symbol) = getproperty(getfield(d, :nt), k)
107
+ Base.getindex(d::ModelData, k::Symbol) = getproperty(getfield(d, :nt), k)
108
+ Base.getindex(d::ModelData, k::AbstractString) = getproperty(getfield(d, :nt), Symbol(k))
109
+ Base.haskey(d::ModelData, k::Symbol) = haskey(getfield(d, :nt), k)
110
+ Base.haskey(d::ModelData, k::AbstractString) = haskey(getfield(d, :nt), Symbol(k))
111
+ Base.keys(d::ModelData) = keys(getfield(d, :nt))
112
+ Base.propertynames(d::ModelData) = propertynames(getfield(d, :nt))
113
+
99
114
  # JuliaBUGS requires dense arrays with a concrete numeric eltype, but JSON parses
100
115
  # them as Vector{Any} (and nested arrays as vectors of vectors). narrow promotes a
101
116
  # numeric array to a concrete eltype and stacks equal-sized nested arrays into a
@@ -140,14 +155,83 @@ function chains_from_wire(wire, targets)
140
155
  return MCMCChains.Chains(arr, syms, Dict(:parameters => syms); info = (; varname_to_symbol = vmap))
141
156
  end
142
157
 
158
+ # Expand one sampler transition to canonical scalar leaves (theta -> theta[1], ...),
159
+ # the same names the final samples file uses, so streamed batches reconstruct it.
160
+ function flatten_draw(transition)
161
+ out = Pair{String,Float64}[]
162
+ for (vn, val) in pairs(transition.params)
163
+ for (leaf, leafval) in Turing.DynamicPPL.varname_and_value_leaves(vn, val)
164
+ push!(out, string(leaf) => Float64(leafval))
165
+ end
166
+ end
167
+ return out
168
+ end
169
+
170
+ # A per-draw callback that emits {"mcmcjs":"draws",...} batches on PROGRESS_IO as
171
+ # sampling proceeds. AbstractMCMC passes the 1-based chain index as `chain_number`;
172
+ # each batch carries a per-chain monotonic seq, and a chain's batches concatenate
173
+ # (by leaf name) to the final samples file. Assumes the callback is not invoked
174
+ # concurrently across chains (true for the MCMCSerial path build_and_sample uses).
175
+ function draw_streamer(draws_per_chain::Int, batch_size::Int)
176
+ chain = Ref(-1)
177
+ seq = Ref(0)
178
+ names = String[]
179
+ buffer = Dict{String,Vector{Float64}}()
180
+ filled = Ref(0)
181
+ function emit(iteration)
182
+ filled[] == 0 && return
183
+ cols = Dict{String,Vector{Float64}}(n => copy(buffer[n]) for n in names)
184
+ println(
185
+ PROGRESS_IO[],
186
+ JSON.json(
187
+ Dict(
188
+ "mcmcjs" => "draws",
189
+ "chain" => chain[],
190
+ "seq" => seq[],
191
+ "iteration" => iteration,
192
+ "draws" => cols,
193
+ ),
194
+ ),
195
+ )
196
+ flush(PROGRESS_IO[])
197
+ seq[] += 1
198
+ for n in names
199
+ empty!(buffer[n])
200
+ end
201
+ filled[] = 0
202
+ end
203
+ return function (_rng, _model, _sampler, transition, _state, iteration; chain_number = 1, _kwargs...)
204
+ c = Int(chain_number) - 1
205
+ if c != chain[]
206
+ chain[] = c
207
+ seq[] = 0
208
+ end
209
+ leaves = flatten_draw(transition)
210
+ if isempty(names)
211
+ for (n, _) in leaves
212
+ push!(names, n)
213
+ buffer[n] = Float64[]
214
+ end
215
+ end
216
+ for (n, v) in leaves
217
+ push!(get!(buffer, n, Float64[]), v)
218
+ end
219
+ filled[] += 1
220
+ if filled[] >= batch_size || iteration >= draws_per_chain
221
+ emit(iteration)
222
+ end
223
+ end
224
+ end
225
+
143
226
  # Build the model and sample in one call so both run in the latest world age:
144
227
  # the model methods are defined by `include` at runtime, so a plain call from
145
228
  # this (older) world would fail with "method too new". invokelatest fixes that.
146
- # Returns Turing's default chain type, a FlexiChain.
147
- function build_and_sample(entry, data, sampler, draws, chains, rng)
229
+ # Returns Turing's default chain type, a FlexiChain. With a callback, draws stream.
230
+ function build_and_sample(entry, data, sampler, draws, chains, rng; callback = nothing)
148
231
  model = entry(data)
232
+ kw = callback === nothing ? NamedTuple() : (; callback)
149
233
  return Logging.with_logger(JSONProgressLogger()) do
150
- sample(rng, model, sampler, MCMCSerial(), draws, chains; progress = true)
234
+ sample(rng, model, sampler, MCMCSerial(), draws, chains; progress = true, kw...)
151
235
  end
152
236
  end
153
237
 
@@ -251,7 +335,7 @@ function handle_request(request)
251
335
 
252
336
  wire = if mode == "predict"
253
337
  backend == "turing" || error("predict is not yet supported for backend $backend")
254
- model = Base.invokelatest(entry, predict_namedtuple(request["data"]))
338
+ model = Base.invokelatest(entry, ModelData(predict_namedtuple(request["data"])))
255
339
  stage = "load_samples"
256
340
  rchn = chains_from_wire(JSON.parsefile(request["samples"]), request["predict"]["targets"])
257
341
  stage = "predict"
@@ -260,7 +344,7 @@ function handle_request(request)
260
344
  error("no variables predicted; check that [predict].targets name unconditioned outcomes")
261
345
  chains_to_wire(pp)
262
346
  else
263
- data = backend == "juliabugs" ? bugs_namedtuple(request["data"]) : to_namedtuple(request["data"])
347
+ data = backend == "juliabugs" ? bugs_namedtuple(request["data"]) : ModelData(to_namedtuple(request["data"]))
264
348
  sampler, warmup = build_sampler(request["sampler"], backend)
265
349
  draws = Int(request["sampler"]["draws"])
266
350
  chains = Int(get(request["sampler"], "chains", 4))
@@ -270,7 +354,11 @@ function handle_request(request)
270
354
  chn = Base.invokelatest(sample_bugs, model, sampler, warmup, draws, chains, rng)
271
355
  chains_to_wire(chn)
272
356
  else
273
- chn = Base.invokelatest(build_and_sample, entry, data, sampler, draws, chains, rng)
357
+ cb = get(request, "stream_draws", false) ?
358
+ draw_streamer(draws, Int(get(request, "draw_batch_size", 25))) : nothing
359
+ chn = Base.invokelatest(
360
+ build_and_sample, entry, data, sampler, draws, chains, rng; callback = cb,
361
+ )
274
362
  vnchain_to_wire(chn)
275
363
  end
276
364
  end