Skip to content

Multi-chain generate_quantities writes the constrained parameters into the csv #3403

Description

@avehtari

The fix and issue text were assisted by Claude

Summary:

gq_writer has two write_gq_values overloads. The std::vector one
drops the first num_constrained_params_ values before writing; the
Eigen one does not. Only the multi-chain path of standalone_generate
uses the Eigen overload, so generate_quantities num_chains=N writes
each row with the constrained parameters prepended, while the header
lists only the generated quantities.

src/stan/services/util/gq_writer.hpp, Eigen overload:

model.write_array(rng, draw, values, false, true, &ss);
...
sample_writer_(values);                     // parameters + generated quantities

versus the std::vector overload:

std::vector<double> gq_values(values.begin() + num_constrained_params_,
                              values.end());
sample_writer_(gq_values);                  // generated quantities only

The result is a CSV whose rows have num_params more fields than the
header has names, so any positional reader mis-assigns every column.

Only generate_quantities num_chains=N with N > 1 in a single process
is affected, which in practice means the CmdStan command line. CmdStanR
and CmdStanPy both launch one process per chain, so each process runs
with num_chains = 1 and is delegated to the single-chain overload;
RStan's standalone_gqs() calls that overload directly. All three
therefore take the std::vector path and are unaffected.

Reproducible Steps:

gq_rng.stan:

parameters {
  real theta;
}
model {
  theta ~ std_normal();
}
generated quantities {
  real u = uniform_rng(0, 1);
}
./gq_rng sample num_warmup=200 num_samples=4 output file=fit.csv random seed=1
cp fit.csv fit_1.csv; cp fit.csv fit_2.csv
./gq_rng generate_quantities fitted_params=fit.csv num_chains=2 output file=gqmc.csv random seed=999

Current Output:

gqmc_1.csv — one column name, two values per row; the leading value is
theta, a parameter:

u
-1.871546, 0.89641628
-0.60661605, 0.63008851
-0.36586517, 0.17265139
-0.96383407, 0.36561701

Expected Output:

u
0.89641628
0.63008851
0.17265139
0.36561701

Additional Information:

Fix is to slice in the Eigen overload as the std::vector one does:

plain_type_t<EigVec> gq_values
    = values.tail(values.size() - num_constrained_params_);
sample_writer_(gq_values);

Note this changes the output of generate_quantities num_chains=N by
removing columns, so it deserves a changelog entry for anyone whose
parser adapted to the current behaviour.

Minor, separate: the two paths also disagree on the separator. The
Eigen writer emits ", " where the std::vector writer emits ",".
Not addressed here.

The regression test asserts that each row of a two-chain run has as
many comma-separated fields as the header has names.
It fails on develop and passes with the fix. PR #3402 changes the rng seed
behavior which changes the output, and after #3402 is merged, the test for this
needs to be updated.

Current Version:

v2.39.0 (also reproduced on develop: cmdstan b6c5aa2, stan
d39d26ba7).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions