Skip to content

Commit 0fb1a1a

Browse files
committed
Make thread_number / number_of_threads an opt-in pair like the instances pair
1 parent cda7b9a commit 0fb1a1a

17 files changed

Lines changed: 326 additions & 176 deletions

capgen-ng/ccpp_capgen_ng.py

Lines changed: 81 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -689,21 +689,44 @@ def _load_metadata_files(
689689
('group_name', 'character', 'drives per-group dispatch inside ccpp_physics_* (each suite_cap emits a select case on this name)'),
690690
('horizontal_loop_begin', 'integer', 'lower horizontal slice bound at scheme call sites'),
691691
('horizontal_loop_end', 'integer', 'upper horizontal slice bound at scheme call sites'),
692-
('thread_number', 'integer', 'current thread number (pass 1 if single-threaded)'),
693-
('number_of_threads', 'integer', 'total thread count (pass 1 if single-threaded)'),
694692
('number_of_physics_threads','integer', 'physics-internal thread budget (pass 1 if unused)'),
695693
('ccpp_error_code', 'integer', 'CCPP error flag'),
696694
('ccpp_error_message', 'character', 'CCPP error message'),
697695
]
698-
699-
# Optional control variables that must be declared as a *pair*. Hosts that
700-
# need a multi-instance API declare both ``instance_number`` (the index) and
701-
# ``number_of_instances`` (the bound). Hosts that don't may omit both; the
702-
# generator will emit a single-instance API and dimension all per-instance
703-
# arrays to length 1. Declaring exactly one is an error.
696+
# NOTE: the threading index/count (``thread_number`` / ``number_of_threads``)
697+
# is NOT required — it is a paired-optional control pair, fully symmetric with
698+
# (``instance_number`` / ``number_of_instances``); see
699+
# ``_PAIRED_OPTIONAL_CTRL_VARS`` below. ``number_of_physics_threads`` is a
700+
# separate, unpaired scheme-facing scalar that stays unconditionally required.
701+
702+
# Paired-optional control variables. Each entry is an (index, count) pair:
703+
# the host declares BOTH members (in ``type=control``) or NEITHER; declaring
704+
# exactly one is a hard error. Declaring a pair opts the host into that
705+
# multi-<X> API — the index flows as a per-call control dummy and the count
706+
# gives the bound. When a pair is absent the public API drops both args and
707+
# the framework uses literal ``1`` wherever the index would appear. A host
708+
# variable may be dimensioned by the count standard name only when its pair is
709+
# declared (otherwise the resolver's scalar-index collapse raises — it needs
710+
# the index variable in scope).
711+
#
712+
# The two pairs are fully symmetric (decision 2026-06-09):
713+
# * (instance_number, number_of_instances) — multi-instance API. The
714+
# framework reads ``number_of_instances`` at register/init to size its
715+
# own per-instance state (``ccpp_suite_data(:)``, ``ccpp_group_state(:)``).
716+
# * (thread_number, number_of_threads) — multi-threading API.
717+
# ``thread_number`` indexes host-owned per-thread containers;
718+
# ``number_of_threads`` is carried as a control dummy (the framework owns
719+
# no per-thread state yet, so its value is not consumed — kept for symmetry
720+
# with ``number_of_instances`` and future per-thread sizing).
721+
# (A chunk/block index is intentionally NOT a control pair: capgen-ng's
722+
# slice-based design passes the current chunk as a horizontal range via
723+
# horizontal_loop_begin/end, so no scheme ever indexes by chunk inside a call.)
724+
# Each entry: (index std_name, count std_name, index description, count description).
704725
_PAIRED_OPTIONAL_CTRL_VARS = [
705-
('instance_number', 'integer', 'current model instance index'),
706-
('number_of_instances', 'integer', 'total number of model instances'),
726+
('instance_number', 'number_of_instances',
727+
'current model instance index', 'total number of model instances'),
728+
('thread_number', 'number_of_threads',
729+
'current thread index', 'total thread count'),
707730
]
708731

709732

@@ -775,39 +798,58 @@ def _check_control_var(std_name, expected_type, description, required: bool) ->
775798
for std_name, expected_type, description in _REQUIRED_CTRL_VARS:
776799
_check_control_var(std_name, expected_type, description, required=True)
777800

778-
# Paired optional: both ``instance_number`` (the per-call index) and
779-
# ``number_of_instances`` (the bound, used at register time to size
780-
# the per-instance state arrays) live in ``type=control``. Symmetric
781-
# with the (thread_number, number_of_threads) pair. Either both
782-
# declared or neither.
783-
_check_control_var(
784-
'instance_number', 'integer',
785-
'current model instance index', required=False,
786-
)
787-
_check_control_var(
788-
'number_of_instances', 'integer',
789-
'total number of model instances', required=False,
790-
)
801+
# Paired-optional control pairs (see _PAIRED_OPTIONAL_CTRL_VARS): for each
802+
# (index, count) pair the host declares both members in a type=control
803+
# table or neither. Declaring exactly one is an error. Both pairs —
804+
# (instance_number, number_of_instances) and (thread_number,
805+
# number_of_threads) — are validated identically.
806+
for idx_name, cnt_name, idx_desc, cnt_desc in _PAIRED_OPTIONAL_CTRL_VARS:
807+
_check_control_var(idx_name, 'integer', idx_desc, required=False)
808+
_check_control_var(cnt_name, 'integer', cnt_desc, required=False)
809+
810+
idx_present = host_dict.get(idx_name) is not None
811+
cnt_present = host_dict.get(cnt_name) is not None
812+
if idx_present ^ cnt_present:
813+
present, missing = (
814+
(idx_name, cnt_name) if idx_present else (cnt_name, idx_name)
815+
)
816+
errors.append(
817+
"Host '{}' declares '{}' in a type=control table but is "
818+
"missing its paired variable '{}' (which must also be in a "
819+
"type=control table).\n"
820+
" '{}' and '{}' are a paired-optional control pair: declare "
821+
"both members to opt into that API, or neither.".format(
822+
host_name, present, missing, idx_name, cnt_name,
823+
)
824+
)
791825

792-
inst_present = host_dict.get('instance_number') is not None
793-
ninst_present = host_dict.get('number_of_instances') is not None
794-
if inst_present ^ ninst_present:
795-
present, missing = (
796-
('instance_number', 'number_of_instances')
797-
if inst_present
798-
else ('number_of_instances', 'instance_number')
799-
)
800-
errors.append(
801-
"Host '{}' declares '{}' (in a type=control table) but is "
802-
"missing the paired variable '{}' (which must also be in a "
803-
"type=control table).\n"
804-
" Declare both for a multi-instance API, or neither for a "
805-
"single-instance API.".format(host_name, present, missing)
806-
)
826+
# Control-table allowlist: a type=control table may declare ONLY the
827+
# framework's known control variables — the unconditionally required set
828+
# plus the members of the paired-optional pairs. Anything else in a
829+
# type=control table is a hard error. Host-specific quantities that
830+
# schemes consume belong in a type=host table; the subcycle loop variables
831+
# (ccpp_loop_counter / ccpp_loop_extent) are generator-owned locals the
832+
# host never declares.
833+
allowed_control = {name for name, _type, _desc in _REQUIRED_CTRL_VARS}
834+
for idx_name, cnt_name, _idesc, _cdesc in _PAIRED_OPTIONAL_CTRL_VARS:
835+
allowed_control.add(idx_name)
836+
allowed_control.add(cnt_name)
837+
for std_name, entry in sorted(host_dict.items()):
838+
if entry.is_control and std_name not in allowed_control:
839+
errors.append(
840+
"Variable '{}' is declared in a type=control table for host "
841+
"'{}' but is not a recognized framework control variable.\n"
842+
" A type=control table may declare only: {}.\n"
843+
" If '{}' is a host quantity that schemes consume, declare it "
844+
"in a type=host table instead.".format(
845+
std_name, host_name, ', '.join(sorted(allowed_control)),
846+
std_name,
847+
)
848+
)
807849

808850
if errors:
809851
raise CCPPError(
810-
"Host '{}' is missing required control variables:\n\n{}".format(
852+
"Host '{}' has invalid control-variable metadata:\n\n{}".format(
811853
host_name,
812854
'\n\n'.join("ERROR: " + e for e in errors),
813855
)

capgen-ng/generator/group_cap.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@
5757
'group_name',
5858
'horizontal_loop_begin',
5959
'horizontal_loop_end',
60-
'thread_number',
61-
'number_of_threads',
60+
'thread_number', # paired-optional (with number_of_threads); ordered here when declared
61+
'number_of_threads', # paired-optional; ordered here when declared
6262
'number_of_physics_threads',
6363
'ccpp_error_code',
6464
'ccpp_error_message',

capgen-ng/metadata/registered_dimensions.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,11 @@
140140
'number_of_instances': 'instance_number',
141141

142142
# Per-thread DDT containers (e.g. ``physics%Interstitial(thread_number)``)
143-
# — the host's openmp-thread index. ``thread_number`` is a required
144-
# control variable (see doc/migration.md §3.1), so any host using
145-
# this dim already has the paired index in scope.
143+
# — the host's openmp-thread index. (thread_number, number_of_threads) is
144+
# a paired-optional control pair (see ccpp_capgen_ng._PAIRED_OPTIONAL_CTRL_VARS
145+
# and doc/migration.md §3.1). A host that dimensions a variable by
146+
# ``number_of_threads`` MUST declare the pair — otherwise the collapse
147+
# below cannot find ``thread_number`` and raises.
146148
'number_of_threads': 'thread_number',
147149
}
148150

capgen-ng/metadata/variable_resolver.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,13 @@ def _split_local_name(local_name: str):
108108
"""Split a local name into (base, subscript) tuple.
109109
110110
For plain identifiers returns (local_name, '').
111-
For slice expressions like ``chunk_begin(ccpp_chunk_number)`` returns
112-
(``'chunk_begin'``, ``'ccpp_chunk_number'``).
111+
For slice expressions like ``field(idx)`` returns
112+
(``'field'``, ``'idx'``).
113113
114-
>>> _split_local_name('chunk_begin')
115-
('chunk_begin', '')
116-
>>> _split_local_name('chunk_begin(ccpp_chunk_number)')
117-
('chunk_begin', 'ccpp_chunk_number')
114+
>>> _split_local_name('field')
115+
('field', '')
116+
>>> _split_local_name('field(idx)')
117+
('field', 'idx')
118118
>>> _split_local_name('q(:,:,index_of_water_vapor_specific_humidity)')
119119
('q', ':,:,index_of_water_vapor_specific_humidity')
120120
"""
@@ -134,11 +134,11 @@ def _resolve_subscript(subscript: str, host_dict: Dict[str, 'HostVarEntry']) ->
134134
135135
>>> from collections import namedtuple
136136
>>> E = namedtuple('E', ['local_name'])
137-
>>> d = {'ccpp_chunk_number': E('inst_num')}
138-
>>> _resolve_subscript('ccpp_chunk_number', d)
139-
'inst_num'
140-
>>> _resolve_subscript(':, ccpp_chunk_number', d)
141-
':, inst_num'
137+
>>> d = {'thread_number': E('thrd_no')}
138+
>>> _resolve_subscript('thread_number', d)
139+
'thrd_no'
140+
>>> _resolve_subscript(':, thread_number', d)
141+
':, thrd_no'
142142
>>> _resolve_subscript('1', d)
143143
'1'
144144
"""

doc/briefing.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ For each scheme arg:
110110
- `ccpp_validator.py` — the standalone Fortran-vs-metadata checker.
111111
The ONE place capgen-ng parses Fortran. Run by developers /
112112
CMake before generation. Checks per-arg `intent`, `type`, `kind`,
113-
and dimension rank; `character len=*` is a wildcard; DDT and
113+
and dimension rank; character length must match exactly
114+
(`len=*``len=*`, `len=N``len=N`, no wildcard; old-style F77
115+
`character*N` / `character*(*)` parsed); DDT and
114116
`external:<module>:<typename>` types compare against the Fortran
115117
`type(name)` wrapper. `optional` is asymmetric: metadata
116118
`optional=True` against a Fortran-required dummy is an error; the
@@ -200,11 +202,23 @@ Every host MUST declare scalar integers (and one character) with
200202
these CCPP standard names:
201203

202204
- `suite_name`, `horizontal_loop_begin`, `horizontal_loop_end`,
203-
`thread_number`, `number_of_threads`, `number_of_physics_threads`,
204-
`ccpp_error_code`, `ccpp_error_message`.
205+
`number_of_physics_threads`, `ccpp_error_code`, `ccpp_error_message`.
205206

206-
Optional (paired): `instance_number` (control) +
207-
`number_of_instances` (host).
207+
**Two paired-optional control pairs** — declare *both* members of a
208+
pair (in `type = control`) or *neither*; declaring exactly one is a
209+
hard error:
210+
211+
- `instance_number` + `number_of_instances` → opt into the
212+
multi-instance API.
213+
- `thread_number` + `number_of_threads` → opt into the multi-threading
214+
API.
215+
216+
The two pairs are fully symmetric. Declaring a pair makes the index a
217+
per-call control argument; omitting it drops both args and the
218+
framework uses literal `1` where the index would go. A host variable
219+
may be dimensioned by `number_of_instances` / `number_of_threads` only
220+
when its pair is declared (otherwise the scalar-index collapse can't
221+
find the index variable and errors).
208222

209223
### 6.5 DDT-instance variables with scalar-index dims
210224

@@ -387,7 +401,8 @@ don't rebuild downstream objects unless something actually moved.
387401
- **Validator** now checks per-argument `intent`, `type`, `kind`, and
388402
dimension rank in addition to the original name/count check.
389403
Asymmetric `optional` rule, DDT + `external:<module>:<typename>`
390-
type normalisation, character `len=*` wildcard.
404+
type normalisation, exact character-length match (no `len=*`
405+
wildcard; old-style F77 `character*N` parsed).
391406
- **Resolver cross-metadata checks** (late 2026-05-20): host/scheme
392407
(and suite-owned-var first-writer/follow-on) consistency on type,
393408
rank, and per-position dimension entries. Default lower bound has

doc/constituents.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -600,28 +600,28 @@ constituent array into the suite's `<suite>_dynamic_constituents`
600600
buffer (USE'd from `ccpp_host_constituents`):
601601

602602
```fortran
603+
! Outer wrapper sized to number_of_instances on first call (any instance).
603604
if (.not. allocated(<suite>_dynamic_constituents)) then
604-
! First-instance-only two-pass count + populate.
605-
num_consts = 0
606-
call <scheme1>_register(scheme_consts=scheme_consts, ...)
607-
num_consts = num_consts + size(scheme_consts, 1)
608-
deallocate(scheme_consts)
609-
...
610-
allocate(<suite>_dynamic_constituents(num_consts))
611-
num_consts = 0
612-
call <scheme1>_register(scheme_consts=scheme_consts, ...)
613-
do i = 1, size(scheme_consts, 1)
614-
<suite>_dynamic_constituents(num_consts + i) = scheme_consts(i)
615-
end do
616-
num_consts = num_consts + size(scheme_consts, 1)
617-
deallocate(scheme_consts)
618-
...
605+
allocate(<suite>_dynamic_constituents(number_of_instances))
619606
end if
607+
608+
! Single pass: call each scheme's _register EXACTLY ONCE and append its
609+
! returned array to THIS instance's slot.
610+
allocate(<suite>_dynamic_constituents(inst)%items(0))
611+
call <scheme1>_register(dyn_const=scheme_consts, ...)
612+
if (errflg /= 0) return
613+
<suite>_dynamic_constituents(inst)%items = &
614+
[<suite>_dynamic_constituents(inst)%items, scheme_consts]
615+
deallocate(scheme_consts)
616+
! ... one block like the above per constituent-registering scheme ...
620617
```
621618

622-
The buffer is **shared across instances** (registration is identical
623-
per instance); only the first instance to call `<suite>_register`
624-
populates it. The host-wide merge happens in
619+
Each instance owns its own `%items` slot (the per-instance buffer, so
620+
`ccpp_register_constituents` can `set_const_index` independently per
621+
instance); the suite state-machine guard ensures each instance populates
622+
it exactly once. Each scheme's `_register` is called **exactly once**
623+
it may safely allocate persistent module state (the earlier two-pass
624+
count+copy called it twice). The host-wide merge happens in
625625
`ccpp_register_constituents`.
626626

627627
### Group-cap call sites

0 commit comments

Comments
 (0)